diff options
Diffstat (limited to 'rust/src/dhcp/parser.rs')
-rw-r--r-- | rust/src/dhcp/parser.rs | 317 |
1 files changed, 317 insertions, 0 deletions
diff --git a/rust/src/dhcp/parser.rs b/rust/src/dhcp/parser.rs new file mode 100644 index 0000000..48acccf --- /dev/null +++ b/rust/src/dhcp/parser.rs @@ -0,0 +1,317 @@ +/* Copyright (C) 2018 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use std::cmp::min; + +use crate::dhcp::dhcp::*; +use nom7::bytes::streaming::take; +use nom7::combinator::verify; +use nom7::number::streaming::{be_u16, be_u32, be_u8}; +use nom7::IResult; + +pub struct DHCPMessage { + pub header: DHCPHeader, + + pub options: Vec<DHCPOption>, + + // Set to true if the options were found to be malformed. That is + // failing to parse with enough data. + pub malformed_options: bool, + + // Set to true if the options failed to parse due to not enough + // data. + pub truncated_options: bool, +} + +pub struct DHCPHeader { + pub opcode: u8, + pub htype: u8, + pub hlen: u8, + pub hops: u8, + pub txid: u32, + pub seconds: u16, + pub flags: u16, + pub clientip: Vec<u8>, + pub yourip: Vec<u8>, + pub serverip: Vec<u8>, + pub giaddr: Vec<u8>, + pub clienthw: Vec<u8>, + pub servername: Vec<u8>, + pub bootfilename: Vec<u8>, + pub magic: Vec<u8>, +} + +pub struct DHCPOptClientId { + pub htype: u8, + pub data: Vec<u8>, +} + +/// Option type for time values. +pub struct DHCPOptTimeValue { + pub seconds: u32, +} + +pub struct DHCPOptGeneric { + pub data: Vec<u8>, +} + +pub enum DHCPOptionWrapper { + ClientId(DHCPOptClientId), + TimeValue(DHCPOptTimeValue), + Generic(DHCPOptGeneric), + End, +} + +pub struct DHCPOption { + pub code: u8, + pub data: Option<Vec<u8>>, + pub option: DHCPOptionWrapper, +} + +pub fn parse_header(i: &[u8]) -> IResult<&[u8], DHCPHeader> { + let (i, opcode) = be_u8(i)?; + let (i, htype) = be_u8(i)?; + let (i, hlen) = be_u8(i)?; + let (i, hops) = be_u8(i)?; + let (i, txid) = be_u32(i)?; + let (i, seconds) = be_u16(i)?; + let (i, flags) = be_u16(i)?; + let (i, clientip) = take(4_usize)(i)?; + let (i, yourip) = take(4_usize)(i)?; + let (i, serverip) = take(4_usize)(i)?; + let (i, giaddr) = take(4_usize)(i)?; + let (i, clienthw) = take(16_usize)(i)?; + let (i, servername) = take(64_usize)(i)?; + let (i, bootfilename) = take(128_usize)(i)?; + let (i, magic) = take(4_usize)(i)?; + Ok(( + i, + DHCPHeader { + opcode, + htype, + hlen, + hops, + txid, + seconds, + flags, + clientip: clientip.to_vec(), + yourip: yourip.to_vec(), + serverip: serverip.to_vec(), + giaddr: giaddr.to_vec(), + clienthw: clienthw[0..min(hlen as usize, 16)].to_vec(), + servername: servername.to_vec(), + bootfilename: bootfilename.to_vec(), + magic: magic.to_vec(), + }, + )) +} + +pub fn parse_clientid_option(i: &[u8]) -> IResult<&[u8], DHCPOption> { + let (i, code) = be_u8(i)?; + let (i, len) = verify(be_u8, |&v| v > 1)(i)?; + let (i, _htype) = be_u8(i)?; + let (i, data) = take(len - 1)(i)?; + Ok(( + i, + DHCPOption { + code, + data: None, + option: DHCPOptionWrapper::ClientId(DHCPOptClientId { + htype: 1, + data: data.to_vec(), + }), + }, + )) +} + +pub fn parse_address_time_option(i: &[u8]) -> IResult<&[u8], DHCPOption> { + let (i, code) = be_u8(i)?; + let (i, _len) = be_u8(i)?; + let (i, seconds) = be_u32(i)?; + Ok(( + i, + DHCPOption { + code, + data: None, + option: DHCPOptionWrapper::TimeValue(DHCPOptTimeValue { seconds }), + }, + )) +} + +pub fn parse_generic_option(i: &[u8]) -> IResult<&[u8], DHCPOption> { + let (i, code) = be_u8(i)?; + let (i, len) = be_u8(i)?; + let (i, data) = take(len)(i)?; + Ok(( + i, + DHCPOption { + code, + data: None, + option: DHCPOptionWrapper::Generic(DHCPOptGeneric { + data: data.to_vec(), + }), + }, + )) +} + +// Parse a single DHCP option. When option 255 (END) is parsed, the remaining +// data will be consumed. +pub fn parse_option(i: &[u8]) -> IResult<&[u8], DHCPOption> { + let (_, opt) = be_u8(i)?; + match opt { + DHCP_OPT_END => { + // End of options case. We consume the rest of the data + // so the parser is not called again. But is there a + // better way to "break"? + let (data, code) = be_u8(i)?; + Ok(( + &[], + DHCPOption { + code, + data: Some(data.to_vec()), + option: DHCPOptionWrapper::End, + }, + )) + } + DHCP_OPT_CLIENT_ID => parse_clientid_option(i), + DHCP_OPT_ADDRESS_TIME => parse_address_time_option(i), + DHCP_OPT_RENEWAL_TIME => parse_address_time_option(i), + DHCP_OPT_REBINDING_TIME => parse_address_time_option(i), + _ => parse_generic_option(i), + } +} + +pub fn dhcp_parse(input: &[u8]) -> IResult<&[u8], DHCPMessage> { + match parse_header(input) { + Ok((rem, header)) => { + let mut options = Vec::new(); + let mut next = rem; + let malformed_options = false; + let mut truncated_options = false; + loop { + match parse_option(next) { + Ok((rem, option)) => { + let done = option.code == DHCP_OPT_END; + options.push(option); + next = rem; + if done { + break; + } + } + Err(_) => { + truncated_options = true; + break; + } + } + } + let message = DHCPMessage { + header, + options, + malformed_options, + truncated_options, + }; + return Ok((next, message)); + } + Err(err) => { + return Err(err); + } + } +} + +#[cfg(test)] +mod tests { + use crate::dhcp::dhcp::*; + use crate::dhcp::parser::*; + + #[test] + fn test_parse_discover() { + let pcap = include_bytes!("discover.pcap"); + let payload = &pcap[24 + 16 + 42..]; + + match dhcp_parse(payload) { + Ok((_rem, message)) => { + let header = message.header; + assert_eq!(header.opcode, BOOTP_REQUEST); + assert_eq!(header.htype, 1); + assert_eq!(header.hlen, 6); + assert_eq!(header.hops, 0); + assert_eq!(header.txid, 0x00003d1d); + assert_eq!(header.seconds, 0); + assert_eq!(header.flags, 0); + assert_eq!(header.clientip, &[0, 0, 0, 0]); + assert_eq!(header.yourip, &[0, 0, 0, 0]); + assert_eq!(header.serverip, &[0, 0, 0, 0]); + assert_eq!(header.giaddr, &[0, 0, 0, 0]); + assert_eq!( + &header.clienthw[..(header.hlen as usize)], + &[0x00, 0x0b, 0x82, 0x01, 0xfc, 0x42] + ); + assert!(header.servername.iter().all(|&x| x == 0)); + assert!(header.bootfilename.iter().all(|&x| x == 0)); + assert_eq!(header.magic, &[0x63, 0x82, 0x53, 0x63]); + + assert!(!message.malformed_options); + assert!(!message.truncated_options); + + assert_eq!(message.options.len(), 5); + assert_eq!(message.options[0].code, DHCP_OPT_TYPE); + assert_eq!(message.options[1].code, DHCP_OPT_CLIENT_ID); + assert_eq!(message.options[2].code, DHCP_OPT_REQUESTED_IP); + assert_eq!(message.options[3].code, DHCP_OPT_PARAMETER_LIST); + assert_eq!(message.options[4].code, DHCP_OPT_END); + } + _ => { + assert!(false); + } + } + } + + #[test] + fn test_parse_client_id_too_short() { + // Length field of 0. + let buf: &[u8] = &[ + 0x01, 0x00, // Length of 0. + 0x01, 0x01, // Junk data start here. + 0x02, 0x03, + ]; + let r = parse_clientid_option(buf); + assert!(r.is_err()); + + // Length field of 1. + let buf: &[u8] = &[ + 0x01, 0x01, // Length of 1. + 0x01, 0x41, + ]; + let r = parse_clientid_option(buf); + assert!(r.is_err()); + + // Length field of 2 -- OK. + let buf: &[u8] = &[ + 0x01, 0x02, // Length of 2. + 0x01, 0x41, + ]; + let r = parse_clientid_option(buf); + match r { + Ok((rem, _)) => { + assert_eq!(rem.len(), 0); + } + _ => { + panic!("failed"); + } + } + } +} |