From: Jason Ish Date: Thu, 20 Apr 2017 22:46:34 +0000 (-0600) Subject: rust: dns: nom DNS parsers X-Git-Tag: suricata-4.0.0-beta1~30 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9449739dd50a38150e43edeac888afa7d7178fe6;p=thirdparty%2Fsuricata.git rust: dns: nom DNS parsers --- diff --git a/rust/src/dns/dns.rs b/rust/src/dns/dns.rs new file mode 100644 index 0000000000..967bfa1e6a --- /dev/null +++ b/rust/src/dns/dns.rs @@ -0,0 +1,66 @@ +/* Copyright (C) 2017 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. + */ + +/// DNS record types. +pub const DNS_RTYPE_A: u16 = 1; +pub const DNS_RTYPE_CNAME: u16 = 5; +pub const DNS_RTYPE_SOA: u16 = 6; +pub const DNS_RTYPE_PTR: u16 = 12; +pub const DNS_RTYPE_MX: u16 = 15; +pub const DNS_RTYPE_SSHFP: u16 = 44; +pub const DNS_RTYPE_RRSIG: u16 = 46; + +#[derive(Debug,PartialEq)] +pub struct DNSHeader { + pub tx_id: u16, + pub flags: u16, + pub questions: u16, + pub answer_rr: u16, + pub authority_rr: u16, + pub additional_rr: u16, +} + +#[derive(Debug)] +pub struct DNSQueryEntry { + pub name: Vec, + pub rrtype: u16, + pub rrclass: u16, +} + +#[derive(Debug,PartialEq)] +pub struct DNSAnswerEntry { + pub name: Vec, + pub rrtype: u16, + pub rrclass: u16, + pub ttl: u32, + pub data_len: u16, + pub data: Vec, +} + +#[derive(Debug)] +pub struct DNSRequest { + pub header: DNSHeader, + pub queries: Vec, +} + +#[derive(Debug)] +pub struct DNSResponse { + pub header: DNSHeader, + pub queries: Vec, + pub answers: Vec, + pub authorities: Vec, +} diff --git a/rust/src/dns/mod.rs b/rust/src/dns/mod.rs index 68a02e53e1..d1821c3c4f 100644 --- a/rust/src/dns/mod.rs +++ b/rust/src/dns/mod.rs @@ -18,6 +18,12 @@ use log::*; use conf; +pub mod parser; +pub use self::parser::*; + +pub mod dns; +pub use self::dns::*; + #[no_mangle] pub extern "C" fn rs_dns_init() { SCLogNotice!("Initializing DNS analyzer"); diff --git a/rust/src/dns/parser.rs b/rust/src/dns/parser.rs new file mode 100644 index 0000000000..c72ac97b40 --- /dev/null +++ b/rust/src/dns/parser.rs @@ -0,0 +1,466 @@ +/* Copyright (C) 2017 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. + */ + +//! Nom parsers for DNS. + +use nom::{be_u8, be_u16, be_u32}; +use nom; +use dns::*; + +/// Parse a DNS header. +named!(pub dns_parse_header, + do_parse!( + tx_id: be_u16 >> + flags: be_u16 >> + questions: be_u16 >> + answer_rr: be_u16 >> + authority_rr: be_u16 >> + additional_rr: be_u16 >> + ( + DNSHeader{ + tx_id: tx_id, + flags: flags, + questions: questions, + answer_rr: answer_rr, + authority_rr: authority_rr, + additional_rr: additional_rr, + } + ) + ) +); + +/// Parse a DNS name. +/// +/// Parameters: +/// start: the start of the name +/// message: the complete message that start is a part of +pub fn dns_parse_name<'a, 'b>(start: &'b [u8], + message: &'b [u8]) + -> nom::IResult<&'b [u8], Vec> { + let mut pos = start; + let mut pivot = start; + let mut name: Vec = Vec::with_capacity(32); + let mut count = 0; + + loop { + if pos.len() == 0 { + break; + } + + let len = pos[0]; + + if len == 0x00 { + pos = &pos[1..]; + break; + } else if len & 0b1100_0000 == 0 { + match length_bytes!(pos, be_u8) { + nom::IResult::Done(rem, label) => { + if name.len() > 0 { + name.push('.' as u8); + } + name.extend(label); + pos = rem; + } + _ => { + return nom::IResult::Error( + error_position!(nom::ErrorKind::OctDigit, input)); + } + } + } else if len & 0b1100_0000 == 0b1100_0000 { + match be_u16(pos) { + nom::IResult::Done(rem, leader) => { + let offset = leader & 0x3fff; + if offset as usize > message.len() { + return nom::IResult::Error( + error_position!(nom::ErrorKind::OctDigit, input)); + } + pos = &message[offset as usize..]; + if pivot == start { + pivot = rem; + } + } + _ => { + return nom::IResult::Error( + error_position!(nom::ErrorKind::OctDigit, input)); + } + } + } else { + return nom::IResult::Error( + error_position!(nom::ErrorKind::OctDigit, input)); + } + + // Return error if we've looped a certain number of times. + count += 1; + if count > 255 { + return nom::IResult::Error( + error_position!(nom::ErrorKind::OctDigit, input)); + } + + } + + // If we followed a pointer we return the position after the first + // pointer followed. Is there a better way to see if these slices + // diverged from each other? A straight up comparison would + // actually check the contents. + if pivot.len() != start.len() { + return nom::IResult::Done(pivot, name); + } + return nom::IResult::Done(pos, name); + +} + +/// Parse a DNS response. +pub fn dns_parse_response<'a>(slice: &'a [u8]) + -> nom::IResult<&[u8], DNSResponse> { + let answer_parser = closure!(&'a [u8], do_parse!( + name: apply!(dns_parse_name, slice) >> + rrtype: be_u16 >> + rrclass: be_u16 >> + ttl: be_u32 >> + data_len: be_u16 >> + data: flat_map!(take!(data_len), + apply!(dns_parse_rdata, slice, rrtype)) >> + ( + DNSAnswerEntry{ + name: name, + rrtype: rrtype, + rrclass: rrclass, + ttl: ttl, + data_len: data_len, + data: data.to_vec(), + } + ) + )); + + let response = closure!(&'a [u8], do_parse!( + header: dns_parse_header >> + queries: count!(apply!(dns_parse_query, slice), + header.questions as usize) >> + answers: count!(answer_parser, header.answer_rr as usize) >> + authorities: count!(answer_parser, header.authority_rr as usize) >> + ( + DNSResponse{ + header: header, + queries: queries, + answers: answers, + authorities: authorities, + } + ) + ))(slice); + + return response; +} + +/// Parse a single DNS query. +/// +/// Arguments are suitable for using with apply!: +/// +/// apply!(complete_dns_message_buffer) +pub fn dns_parse_query<'a>(input: &'a [u8], + message: &'a [u8]) + -> nom::IResult<&'a [u8], DNSQueryEntry> { + return closure!(&'a [u8], do_parse!( + name: apply!(dns_parse_name, message) >> + rrtype: be_u16 >> + rrclass: be_u16 >> + ( + DNSQueryEntry{ + name: name, + rrtype: rrtype, + rrclass: rrclass, + } + ) + ))(input); +} + +pub fn dns_parse_rdata<'a>(data: &'a [u8], message: &'a [u8], rrtype: u16) + -> nom::IResult<&'a [u8], Vec> +{ + match rrtype { + DNS_RTYPE_CNAME | + DNS_RTYPE_PTR | + DNS_RTYPE_SOA => { + dns_parse_name(data, message) + }, + DNS_RTYPE_MX => { + // For MX we we skip over the preference field before + // parsing out the name. + closure!(do_parse!( + be_u16 >> + name: apply!(dns_parse_name, message) >> + (name) + ))(data) + }, + _ => nom::IResult::Done(data, data.to_vec()) + } +} + +/// Parse a DNS request. +pub fn dns_parse_request<'a>(input: &'a [u8]) -> nom::IResult<&[u8], DNSRequest> { + return closure!(&'a [u8], do_parse!( + header: dns_parse_header >> + queries: count!(apply!(dns_parse_query, input), + header.questions as usize) >> + ( + DNSRequest{ + header: header, + queries: queries, + } + ) + ))(input); +} + +#[cfg(test)] +mod tests { + + use dns::parser::*; + use nom::IResult; + + /// Parse a simple name with no pointers. + #[test] + fn test_dns_parse_name() { + let buf: &[u8] = &[ + 0x09, 0x63, /* .......c */ + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2d, 0x63, 0x66, /* lient-cf */ + 0x07, 0x64, 0x72, 0x6f, 0x70, 0x62, 0x6f, 0x78, /* .dropbox */ + 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, /* .com.... */ + ]; + let expected_remainder: &[u8] = &[0x00, 0x01, 0x00]; + let res = dns_parse_name(buf, buf); + match res { + IResult::Done(remainder, name) => { + assert_eq!("client-cf.dropbox.com".as_bytes(), &name[..]); + assert_eq!(remainder, expected_remainder); + } + _ => { + assert!(false); + } + } + } + + /// Test parsing a name with pointers. + #[test] + fn test_dns_parse_name_with_pointer() { + let buf: &[u8] = &[ + 0xd8, 0xcb, 0x8a, 0xed, 0xa1, 0x46, 0x00, 0x15 /* 0 - .....F.. */, + 0x17, 0x0d, 0x06, 0xf7, 0x08, 0x00, 0x45, 0x00 /* 8 - ......E. */, + 0x00, 0x7b, 0x71, 0x6e, 0x00, 0x00, 0x39, 0x11 /* 16 - .{qn..9. */, + 0xf4, 0xd9, 0x08, 0x08, 0x08, 0x08, 0x0a, 0x10 /* 24 - ........ */, + 0x01, 0x0b, 0x00, 0x35, 0xe1, 0x8e, 0x00, 0x67 /* 32 - ...5...g */, + 0x60, 0x00, 0xef, 0x08, 0x81, 0x80, 0x00, 0x01 /* 40 - `....... */, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x03, 0x77 /* 48 - .......w */, + 0x77, 0x77, 0x0c, 0x73, 0x75, 0x72, 0x69, 0x63 /* 56 - ww.suric */, + 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, 0x73, 0x03 /* 64 - ata-ids. */, + 0x6f, 0x72, 0x67, 0x00, 0x00, 0x01, 0x00, 0x01 /* 72 - org..... */, + 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00 /* 80 - ........ */, + 0x0e, 0x0f, 0x00, 0x02, 0xc0, 0x10, 0xc0, 0x10 /* 88 - ........ */, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x2b /* 96 - .......+ */, + 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x19, 0xc0, 0x10 /* 104 - ....N... */, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x2b /* 112 - .......+ */, + 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x18, 0x00, 0x00 /* 120 - ....N... */, + 0x29, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 /* 128 - )....... */, + 0x00, /* 136 - . */ + ]; + + // The DNS payload starts at offset 42. + let message = &buf[42..]; + + // The name at offset 54 is the complete name. + let start1 = &buf[54..]; + let res1 = dns_parse_name(start1, message); + assert_eq!(res1, + IResult::Done(&start1[22..], + "www.suricata-ids.org".as_bytes().to_vec())); + + // The second name starts at offset 80, but is just a pointer + // to the first. + let start2 = &buf[80..]; + let res2 = dns_parse_name(start2, message); + assert_eq!(res2, + IResult::Done(&start2[2..], + "www.suricata-ids.org".as_bytes().to_vec())); + + // The third name starts at offset 94, but is a pointer to a + // portion of the first. + let start3 = &buf[94..]; + let res3 = dns_parse_name(start3, message); + assert_eq!(res3, + IResult::Done(&start3[2..], + "suricata-ids.org".as_bytes().to_vec())); + + // The fourth name starts at offset 110, but is a pointer to a + // portion of the first. + let start4 = &buf[110..]; + let res4 = dns_parse_name(start4, message); + assert_eq!(res4, + IResult::Done(&start4[2..], + "suricata-ids.org".as_bytes().to_vec())); + } + + #[test] + fn test_dns_parse_name_double_pointer() { + let buf: &[u8] = &[ + 0xd8, 0xcb, 0x8a, 0xed, 0xa1, 0x46, 0x00, 0x15 /* 0: .....F.. */, + 0x17, 0x0d, 0x06, 0xf7, 0x08, 0x00, 0x45, 0x00 /* 8: ......E. */, + 0x00, 0x66, 0x5e, 0x20, 0x40, 0x00, 0x40, 0x11 /* 16: .f^ @.@. */, + 0xc6, 0x3b, 0x0a, 0x10, 0x01, 0x01, 0x0a, 0x10 /* 24: .;...... */, + 0x01, 0x0b, 0x00, 0x35, 0xc2, 0x21, 0x00, 0x52 /* 32: ...5.!.R */, + 0x35, 0xc5, 0x0d, 0x4f, 0x81, 0x80, 0x00, 0x01 /* 40: 5..O.... */, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x05, 0x62 /* 48: .......b */, + 0x6c, 0x6f, 0x63, 0x6b, 0x07, 0x64, 0x72, 0x6f /* 56: lock.dro */, + 0x70, 0x62, 0x6f, 0x78, 0x03, 0x63, 0x6f, 0x6d /* 64: pbox.com */, + 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00 /* 72: ........ */, + 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x09, 0x00 /* 80: ........ */, + 0x0b, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x02 /* 88: ..block. */, + 0x67, 0x31, 0xc0, 0x12, 0xc0, 0x2f, 0x00, 0x01 /* 96: g1.../.. */, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04 /* 104: ........ */, + 0x2d, 0x3a, 0x46, 0x21 /* 112: -:F! */ + ]; + + // The start of the DNS message in the above packet. + let message: &[u8] = &buf[42..]; + + // The start of the name we want to parse, 0xc0 0x2f, a + // pointer to offset 47 in the message (or 89 in the full + // packet). + let start: &[u8] = &buf[100..]; + + let res = dns_parse_name(start, message); + assert_eq!(res, + IResult::Done(&start[2..], + "block.g1.dropbox.com".as_bytes().to_vec())); + } + + #[test] + fn test_dns_parse_request() { + // DNS request from dig-a-www.suricata-ids.org.pcap. + let pkt: &[u8] = &[ + 0x8d, 0x32, 0x01, 0x20, 0x00, 0x01, /* ...2. .. */ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x77, /* .......w */ + 0x77, 0x77, 0x0c, 0x73, 0x75, 0x72, 0x69, 0x63, /* ww.suric */ + 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, 0x73, 0x03, /* ata-ids. */ + 0x6f, 0x72, 0x67, 0x00, 0x00, 0x01, 0x00, 0x01, /* org..... */ + 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, 0x00, /* ..)..... */ + 0x00, 0x00, 0x00 /* ... */ + ]; + + let res = dns_parse_request(pkt); + match res { + IResult::Done(rem, request) => { + + // For now we have some remainder data as there is an + // additional record type we don't parse yet. + assert!(rem.len() > 0); + + assert_eq!(request.header, DNSHeader { + tx_id: 0x8d32, + flags: 0x0120, + questions: 1, + answer_rr: 0, + authority_rr: 0, + additional_rr: 1, + }); + + assert_eq!(request.queries.len(), 1); + + let query = &request.queries[0]; + assert_eq!(query.name, + "www.suricata-ids.org".as_bytes().to_vec()); + assert_eq!(query.rrtype, 1); + assert_eq!(query.rrclass, 1); + } + _ => { + assert!(false); + } + } + } + + #[test] + fn test_dns_parse_response() { + // DNS response from dig-a-www.suricata-ids.org.pcap. + let pkt: &[u8] = &[ + 0x8d, 0x32, 0x81, 0xa0, 0x00, 0x01, /* ...2.... */ + 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77, /* .......w */ + 0x77, 0x77, 0x0c, 0x73, 0x75, 0x72, 0x69, 0x63, /* ww.suric */ + 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, 0x73, 0x03, /* ata-ids. */ + 0x6f, 0x72, 0x67, 0x00, 0x00, 0x01, 0x00, 0x01, /* org..... */ + 0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, /* ........ */ + 0x0d, 0xd8, 0x00, 0x12, 0x0c, 0x73, 0x75, 0x72, /* .....sur */ + 0x69, 0x63, 0x61, 0x74, 0x61, 0x2d, 0x69, 0x64, /* icata-id */ + 0x73, 0x03, 0x6f, 0x72, 0x67, 0x00, 0xc0, 0x32, /* s.org..2 */ + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf4, /* ........ */ + 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x18, 0xc0, 0x32, /* ....N..2 */ + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xf4, /* ........ */ + 0x00, 0x04, 0xc0, 0x00, 0x4e, 0x19 /* ....N. */ + ]; + + let res = dns_parse_response(pkt); + match res { + IResult::Done(rem, response) => { + + // The response should be full parsed. + assert_eq!(rem.len(), 0); + + assert_eq!(response.header, DNSHeader{ + tx_id: 0x8d32, + flags: 0x81a0, + questions: 1, + answer_rr: 3, + authority_rr: 0, + additional_rr: 0, + }); + + assert_eq!(response.answers.len(), 3); + + let answer1 = &response.answers[0]; + assert_eq!(answer1.name, + "www.suricata-ids.org".as_bytes().to_vec()); + assert_eq!(answer1.rrtype, 5); + assert_eq!(answer1.rrclass, 1); + assert_eq!(answer1.ttl, 3544); + assert_eq!(answer1.data_len, 18); + assert_eq!(answer1.data, + "suricata-ids.org".as_bytes().to_vec()); + + let answer2 = &response.answers[1]; + assert_eq!(answer2, &DNSAnswerEntry{ + name: "suricata-ids.org".as_bytes().to_vec(), + rrtype: 1, + rrclass: 1, + ttl: 244, + data_len: 4, + data: [192, 0, 78, 24].to_vec(), + }); + + let answer3 = &response.answers[2]; + assert_eq!(answer3, &DNSAnswerEntry{ + name: "suricata-ids.org".as_bytes().to_vec(), + rrtype: 1, + rrclass: 1, + ttl: 244, + data_len: 4, + data: [192, 0, 78, 25].to_vec(), + }) + + }, + _ => { + assert!(false); + } + } + } + +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index cf439d4607..f88c8a8298 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -15,10 +15,13 @@ * 02110-1301, USA. */ +#[macro_use] +extern crate nom; + #[macro_use] pub mod log; pub mod core; pub mod conf; -pub mod dns; pub mod json; +pub mod dns;