Push up exploritory work

This commit is contained in:
rncwnd 2024-07-01 15:24:30 +01:00
commit 1e48775af2
12 changed files with 1230 additions and 0 deletions

32
src/README.md Normal file
View file

@ -0,0 +1,32 @@
# Dipper
A highly experimental pure rust DPI engine.
## Rationale
nDPI exists but it's all C and there's a lot of macros, it's hard to use cleanly
from rust.
Commercial DPI systems exist, but are prohibitivley expensive.
Alternative "Kind of DPI" systems like Suricata exist and are great, but are
only part rust.
## Tools used
- Nom is used extensivley in order to parse wire formats.
- Etherparse is used to "chunk" packets into their various components.
## Goals
### Short Term
- Functional offline packet inspection.
- DNS, ICMP, HTTP, maybe SSH parsing and inspection.
- Standardised output format.
### Long Term
- Online analysis
- Plugin system?

73
src/main.rs Normal file
View file

@ -0,0 +1,73 @@
mod protocols;
mod util;
use std::path::PathBuf;
use clap::Parser;
use pcap::*;
use tracing::*;
use crate::protocols::*;
use crate::util::Stats;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Opts {
#[arg(short, long)]
pcap_file: Option<PathBuf>,
#[arg(long, default_value_t = false)]
print_analysis: bool,
}
#[tracing::instrument]
fn main() {
tracing_subscriber::fmt()
.pretty()
.with_thread_names(true)
.with_max_level(tracing::Level::TRACE)
.init();
let opt = Opts::parse();
let mut pcap = match opt.pcap_file {
Some(pf) => Capture::from_file(pf).expect("Invalid pcap file provided"),
None => {
warn!("Using example.pcap as no pcap was provided");
Capture::from_file(PathBuf::from("./pcaps/ssh_test.pcap"))
.expect("./pcaps/ssh_test.pcap does not exist")
}
};
info!("Pcap loaded");
let mut stats = Stats::new();
while let Ok(packet) = pcap.next_packet() {
stats.total_packets += 1;
match etherparse::SlicedPacket::from_ethernet(packet.data) {
Err(e) => {
error!(
"Slicing packet {} resulted in error {:?}",
stats.total_packets, e
);
stats.errored_packets += 1;
continue;
}
Ok(sliced) => {
if !sliced.payload.is_empty() {
match match_protocol(sliced.payload.to_vec()) {
Ok(x) => {
stats.known_packets += 1;
trace!("Known packet type found: {:?}", x);
let extracted = extract_info(x, sliced.payload.to_vec());
if opt.print_analysis {
info!("{:?}", extracted);
}
}
Err(_) => stats.unknown_packets += 1,
}
}
}
}
}
println!("pcap processing complete.");
println!("{}", stats);
//info!("There were {} packets in the pcap", pkts);
}

202
src/protocols/dns.rs Normal file
View file

@ -0,0 +1,202 @@
use crate::util::*;
use nom::{
bytes::complete::{tag, take},
combinator::map,
multi::{length_data, many_till},
sequence::tuple,
IResult, Parser,
};
use tracing::*;
#[derive(Debug)]
pub enum DNSType {
Query,
Response,
}
#[derive(Debug, PartialEq, Eq)]
pub struct DNSValue {
pub txid: u16,
pub flags: u16,
pub question_count: u16,
pub answer_rrs: u16,
pub auth_rrs: u16,
pub additional_rrs: u16,
pub questions: Option<Vec<String>>,
pub answers: Option<Vec<String>>,
pub question_type: u16,
pub question_class: u16,
pub remainder: Option<Vec<u8>>,
}
pub fn is_dns(payload: Vec<u8>) -> Result<DNSType, ()> {
if is_dns_query(&payload) {
return Ok(DNSType::Query);
}
if is_dns_response(&payload) {
return Ok(DNSType::Response);
}
Err(())
}
fn is_dns_query(payload: &[u8]) -> bool {
payload[2] == 0x01 && payload[3] == 0x00
}
fn is_dns_response(payload: &[u8]) -> bool {
payload[2] == 0x81 && payload[3] == 0x80
}
fn ld(s: &[u8]) -> IResult<&[u8], &[u8]> {
length_data(nom::number::complete::u8)(s)
}
fn parse_dns_string(payload: &[u8]) -> IResult<&[u8], (Vec<&[u8]>, &[u8])> {
let mut parser = many_till(ld, tag([0x00]));
parser.parse(payload)
}
fn take_two_as_u16(payload: &[u8]) -> IResult<&[u8], u16> {
let mut parser = map(take(2_u8), |s: &[u8]| as_u16(s[0], s[1]));
parser.parse(payload)
}
fn parse_dns_preamble(payload: &[u8]) -> IResult<&[u8], (u16, u16, u16, u16, u16, u16)> {
let mut parser = tuple((
take_two_as_u16,
take_two_as_u16,
take_two_as_u16,
take_two_as_u16,
take_two_as_u16,
take_two_as_u16,
));
parser.parse(payload)
}
fn parse_query_postamble(payload: &[u8]) -> IResult<&[u8], (u16, u16)> {
let mut parser = tuple((take_two_as_u16, take_two_as_u16));
parser.parse(payload)
}
fn parsed_dns_string_to_real_string(data: Vec<Vec<u8>>) -> String {
let mut domain_name = String::from("");
for (i, subpart) in data.into_iter().enumerate() {
let as_str = String::from_utf8_lossy(&subpart);
if i == 0 {
domain_name = as_str.to_string()
} else {
domain_name = format!("{}.{}", domain_name, as_str);
}
}
domain_name
}
pub fn analyse_dns_query(payload: Vec<u8>) -> DNSValue {
let mut parser = tuple((parse_dns_preamble, parse_dns_string, parse_query_postamble));
let result = parser.parse(&payload).unwrap();
let remainder = result.0;
let preamble = result.1 .0;
let string_fragments = result.1 .1 .0;
let postamble = result.1 .2;
trace!("Preamble : {:02X?}", preamble);
trace!("strings {:02X?}", string_fragments);
trace!("postamble {:02X?}", postamble);
let mut string_parts: Vec<Vec<u8>> = Vec::new();
for substring in string_fragments {
string_parts.push(substring.to_vec());
}
let real_query_string = parsed_dns_string_to_real_string(string_parts);
let real_remainder = if remainder.is_empty() {
None
} else {
Some(remainder.to_vec())
};
DNSValue {
txid: preamble.0,
flags: preamble.1,
question_count: preamble.2,
answer_rrs: preamble.3,
auth_rrs: preamble.4,
additional_rrs: preamble.5,
question_type: postamble.0,
question_class: postamble.1,
remainder: real_remainder,
questions: Some(vec![real_query_string]),
answers: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dns_txt_query_parse() {
let packet_bytes: [u8; 28] = [
0x10, 0x32, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x67,
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x10, 0x00, 0x01,
];
let parsed_query = analyse_dns_query(packet_bytes.to_vec());
let expected = DNSValue {
txid: 0x1032,
flags: 0x0100,
question_count: 1,
answer_rrs: 0,
auth_rrs: 0,
additional_rrs: 0,
questions: Some(vec!["google.com".to_string()]),
answers: None,
question_type: 16,
question_class: 0x0001,
remainder: None,
};
assert!(parsed_query == expected);
}
#[test]
fn test_parse_dns_string() {
let packet_bytes: [u8; 13] = [
0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x01,
];
let (_, result) = parse_dns_string(&packet_bytes).unwrap();
let mut string_parts: Vec<Vec<u8>> = Vec::new();
for substring in result.0 {
string_parts.push(substring.to_vec());
}
let real_string = parsed_dns_string_to_real_string(string_parts);
assert!(real_string == *"google.com")
}
#[test]
fn test_dns_a_record_query_parse() {
let packet_bytes: [u8; 32] = [
0x75, 0xc0, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x77,
0x77, 0x77, 0x06, 0x6e, 0x65, 0x74, 0x62, 0x73, 0x64, 0x03, 0x6f, 0x72, 0x67, 0x00,
0x00, 0x01, 0x00, 0x01,
];
let parsed_query = analyse_dns_query(packet_bytes.to_vec());
let expected = DNSValue {
txid: 0x75c0,
flags: 0x0100,
question_count: 1,
answer_rrs: 0,
auth_rrs: 0,
additional_rrs: 0,
questions: Some(vec!["www.netbsd.org".to_string()]),
answers: None,
question_type: 1,
question_class: 0x0001,
remainder: None,
};
assert!(parsed_query == expected);
}
}

1
src/protocols/icmp.rs Normal file
View file

@ -0,0 +1 @@

32
src/protocols/mod.rs Normal file
View file

@ -0,0 +1,32 @@
use dns::{DNSType, DNSValue};
mod dns;
#[derive(Debug)]
pub enum ProtocolType {
DNS(DNSType),
}
#[derive(Debug)]
pub enum ExtractedInfo {
DNSQuery(DNSValue),
}
pub fn extract_info(ptype: ProtocolType, payload: Vec<u8>) -> Option<ExtractedInfo> {
match ptype {
ProtocolType::DNS(x) => match x {
DNSType::Query => {
return Some(ExtractedInfo::DNSQuery(dns::analyse_dns_query(payload)));
}
DNSType::Response => return None,
},
}
}
pub fn match_protocol(payload: Vec<u8>) -> Result<ProtocolType, ()> {
match dns::is_dns(payload) {
Ok(x) => return Ok(ProtocolType::DNS(x)),
Err(_) => {}
};
return Err(());
}

1
src/protocols/ssh.rs Normal file
View file

@ -0,0 +1 @@

48
src/util.rs Normal file
View file

@ -0,0 +1,48 @@
use std::fmt;
use std::fmt::Display;
pub fn as_u16(a: u8, b: u8) -> u16 {
(a as u16) << 8 | b as u16
}
pub struct Stats {
pub total_packets: usize,
pub known_packets: usize,
pub unknown_packets: usize,
pub errored_packets: usize,
pub empty_payload: usize,
}
impl Stats {
pub fn new() -> Stats {
Stats {
total_packets: 0,
known_packets: 0,
unknown_packets: 0,
errored_packets: 0,
empty_payload: 0,
}
}
fn percent_known(self) -> f64 {
self.total_packets as f64 / self.known_packets as f64
}
fn percent_error(self) -> f64 {
self.total_packets as f64 / self.errored_packets as f64
}
}
impl Display for Stats {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Total: {}\nKnown: {}\nUnknown: {}\nErrored: {}\nEmpty: {}",
self.total_packets,
self.known_packets,
self.unknown_packets,
self.errored_packets,
self.empty_payload
)
}
}