diff --git a/Cargo.toml b/Cargo.toml index e36074e..dadd6a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ja3" version = "0.5.0" authors = ["Joshua Abraham "] -edition = "2018" +edition = "2021" description = "JA3 hash library in Rust" keywords = ["ja3", "ssl", "tls", "networking", "network-analysis"] homepage = "https://github.com/jabedude/ja3-rs/" @@ -24,7 +24,7 @@ log = "0.4.8" md5 = "0.7.0" pcap = { version = "0.7.0", optional = true } pcap-parser = "0.9.2" -pnet = "0.28.0" +pnet = "0.34.0" tls-parser = "0.9.2" [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 3bf091a..09c1385 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,9 +44,10 @@ use std::fs::File; use std::ffi::{OsStr, OsString}; use std::fmt; use std::net::IpAddr; +use std::time::Duration; use lazy_static::*; -use log::{info, debug}; +use log::{info, debug, warn}; use md5::{self, Digest}; #[cfg(feature = "live-capture")] use pcap::{Active, Capture}; @@ -63,6 +64,7 @@ use tls_parser::tls_extensions::{parse_tls_extensions, TlsExtension, TlsExtensio mod errors; use errors::*; use failure::Error; +use pnet::packet::tcp::TcpFlags; lazy_static! { static ref IPTYPE: IpNextHeaderProtocol = IpNextHeaderProtocol::new(6); @@ -92,11 +94,19 @@ pub struct Ja3Hash { /// See the original [JA3 specification](https://github.com/salesforce/ja3#how-it-works) for more info. pub ja3_str: String, /// The MD5 hash of `ja3_str`. - pub hash: Digest, + pub hash: Option, /// The destination IP address of the TLS handshake. pub source: IpAddr, /// The source IP address of the TLS handshake. pub destination: IpAddr, + /// The packet size + pub packet_size: usize, + // is this a handshake + pub is_handshake: bool, + pub ethernet_frame_size: usize, + pub is_syn: bool, + pub is_fin: bool, + pub is_rst: bool, } /// Iterator of JA3 hashes captured during a live capture. @@ -113,9 +123,12 @@ impl Iterator for Ja3Live { fn next(&mut self) -> Option { while let Ok(packet) = self.cap.next() { match self.ja3_inner.process_packet_common(&packet) { - Ok(s) => return Some(s), + Ok(ja3) => { + // You can now use ja3.packet_size and ja3.is_handshake + return Some(ja3); + } Err(_) => continue, - }; + } } None @@ -211,6 +224,9 @@ impl Ja3 { let saddr; let daddr; let ether = ethernet::EthernetPacket::new(&packet).ok_or(Ja3Error::ParseError)?; + let packet_size = ether.payload().len(); + let ethernet_frame_size = packet.len(); + info!("\nether packet: {:?} len: {}", ether, ether.packet_size()); let tcp_start = match ether.get_ethertype() { EtherType(0x0800) => { @@ -248,33 +264,89 @@ impl Ja3 { } } + let is_syn = tcp.get_flags() & TcpFlags::SYN != 0; + let is_fin = tcp.get_flags() & TcpFlags::FIN != 0; + let is_rst = tcp.get_flags() & TcpFlags::RST != 0; + info!("pack size: {}", tcp.packet_size()); let handshake_start = tcp_start + tcp.packet_size(); info!("handshake_start: {}", handshake_start); let handshake = &packet[handshake_start..]; - if handshake.len() <= 0 { - return Err(Ja3Error::NotHandshake)?; - } - if handshake[0] != 0x16 { - return Err(Ja3Error::NotHandshake)?; - } + // if handshake.len() <= 0 { + // return Err(Ja3Error::NotHandshake)?; + // } + // if handshake[0] != 0x16 { + // return Err(Ja3Error::NotHandshake)?; + // } info!("handshake: {:x?}", handshake); + // mark if its a handshake, might not be needed + let is_handshake = if handshake.len() > 0 && handshake[0] == 0x16 { + true // It's a TLS handshake + } else { + false + }; + info!("sending handshake {:?}", handshake); - let ja3_string = self.ja3_string_client_hello(&handshake).unwrap(); - if ja3_string == "" { - return Err(Ja3Error::NotHandshake)?; + match self.ja3_string_client_hello(&handshake) { + Some(ja3_string) if !ja3_string.is_empty() => { + // Proceed with processing the non-empty JA3 string + let hash = md5::compute(&ja3_string.as_bytes()); + let ja3_res = Ja3Hash { + ja3_str: ja3_string, + hash: Some(hash), + source: saddr, + destination: daddr, + packet_size: packet_size, + is_handshake: is_handshake, + ethernet_frame_size: ethernet_frame_size, + is_syn: is_syn, + is_fin: is_fin, + is_rst: is_rst, + }; + + Ok(ja3_res) + } + _ => { + // return Err(Ja3Error::NotHandshake)?; + // warn!("setting ja3 to none"); + // Handle the case where the JA3 string is None or empty + // You can either skip this packet, log it, or handle it differently + let ja3_res = Ja3Hash { + ja3_str: "".to_owned(), + hash: Option::None, + source: saddr, + destination: daddr, + packet_size: packet_size, + is_handshake: is_handshake, + ethernet_frame_size: ethernet_frame_size, + is_syn: is_syn, + is_fin: is_fin, + is_rst: is_rst, + }; + + Ok(ja3_res) + } } - let hash = md5::compute(&ja3_string.as_bytes()); - let ja3_res = Ja3Hash { - ja3_str: ja3_string, - hash: hash, - source: saddr, - destination: daddr, - }; + // info!("sending handshake {:?}", handshake); + // let ja3_string = self.ja3_string_client_hello(&handshake).unwrap(); + // if ja3_string == "" { + // return Err(Ja3Error::NotHandshake)?; + // } + - Ok(ja3_res) + // let hash = md5::compute(&ja3_string.as_bytes()); + // let ja3_res = Ja3Hash { + // ja3_str: ja3_string, + // hash: hash, + // source: saddr, + // destination: daddr, + // packet_size: packet_size, + // is_handshake: is_handshake, + // }; + + // Ok(ja3_res) } fn process_extensions(&self, extensions: &[u8]) -> Option { @@ -364,8 +436,8 @@ impl fmt::Display for Ja3Hash { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "[{} --> {}] {} {:x}", - self.source, self.destination, self.ja3_str, self.hash + "[{} --> {}] {} {:?}", + self.source, self.destination, self.ja3_str, Option::Some(self.hash) ) } } @@ -404,7 +476,7 @@ mod tests { .process_live().unwrap(); if let Some(x) = ja3.next() { assert_eq!(x.ja3_str, expected_str); - assert_eq!(format!("{:x}", x.hash), expected_hash); + assert_eq!(format!("{:x}", x.hash.unwrap()), expected_hash); assert_eq!(expected_daddr, x.destination); std::process::exit(0); } @@ -434,7 +506,7 @@ mod tests { .unwrap(); let ja3_hash = ja3.pop().unwrap(); assert_eq!(ja3_hash.ja3_str, expected_str); - assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash); + assert_eq!(format!("{:x}", ja3_hash.hash.unwrap()), expected_hash); assert_eq!(expected_daddr, ja3_hash.destination); } @@ -447,7 +519,7 @@ mod tests { let mut ja3 = Ja3::new("tests/test.pcap").process_pcap().unwrap(); let ja3_hash = ja3.pop().unwrap(); assert_eq!(ja3_hash.ja3_str, expected_str); - assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash); + assert_eq!(format!("{:x}", ja3_hash.hash.unwrap()), expected_hash); assert_eq!(expected_daddr, ja3_hash.destination); } @@ -458,12 +530,27 @@ mod tests { let expected_daddr = IpAddr::V4("93.184.216.34".parse().unwrap()); let mut ja3s = Ja3::new("tests/curl.pcap").process_pcap().unwrap(); - let ja3 = ja3s.pop().unwrap(); + let mut found_ja3_hash = None; + + // Iterate through all JA3 hashes until we find one from a TLS handshake + for ja3_hash in ja3s { + if ja3_hash.is_handshake { + found_ja3_hash = Some(ja3_hash); + break; // Exit the loop once we find the first TLS handshake + } + } + + // Ensure we found a JA3 hash from a TLS handshake + assert!(found_ja3_hash.is_some(), "No JA3 hash from a TLS handshake was found"); + let ja3 = found_ja3_hash.unwrap(); + + // Perform your assertions assert_eq!(ja3.ja3_str, expected_str); - assert_eq!(format!("{:x}", ja3.hash), expected_hash); - assert_eq!(expected_daddr, ja3.destination); + assert_eq!(format!("{:x}", ja3.hash.unwrap()), expected_hash); + assert_eq!(ja3.destination, expected_daddr); } + #[test] fn test_ja3_curl_full_stream_ipv6() { let expected_str = "771,4866-4867-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,0-11-10-13172-16-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2"; @@ -471,27 +558,51 @@ mod tests { let expected_daddr = IpAddr::V6("2606:2800:220:1:248:1893:25c8:1946".parse().unwrap()); let mut ja3s = Ja3::new("tests/curl-ipv6.pcap").process_pcap().unwrap(); - let ja3 = ja3s.pop().unwrap(); + let mut found_ja3_hash = None; + + for ja3_hash in ja3s { + if ja3_hash.is_handshake { + found_ja3_hash = Some(ja3_hash); + break; + } + } + + assert!(found_ja3_hash.is_some(), "No JA3 hash from a TLS handshake was found"); + let ja3 = found_ja3_hash.unwrap(); + assert_eq!(ja3.ja3_str, expected_str); - assert_eq!(format!("{:x}", ja3.hash), expected_hash); - assert_eq!(expected_daddr, ja3.destination); + assert_eq!(format!("{:x}", ja3.hash.unwrap()), expected_hash); + assert_eq!(ja3.destination, expected_daddr); } + #[test] fn test_ja3_client_hello_ncat_full_stream_non_tls_port() { let expected_str = "771,4866-4867-4865-49196-49200-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-49188-49192-107-106-49267-49271-196-195-49162-49172-57-56-136-135-157-49313-49309-49233-61-192-53-132-49195-49199-162-158-49326-49324-49314-49310-49244-49248-49238-49234-49187-49191-103-64-49266-49270-190-189-49161-49171-51-50-154-153-69-68-156-49312-49308-49232-60-186-47-150-65-255,0-11-10-35-22-23-13-43-45-51-21,29-23-30-25-24,0-1-2"; let expected_hash = "10a6b69a81bac09072a536ce9d35dd43"; - let mut ja3 = Ja3::new("tests/ncat-port-4450.pcap") + let mut ja3s = Ja3::new("tests/ncat-port-4450.pcap") .any_port() .process_pcap() .unwrap(); - let ja3_hash = ja3.pop().unwrap(); + let mut found_ja3_hash = None; + + for ja3_hash in ja3s { + if ja3_hash.is_handshake { + found_ja3_hash = Some(ja3_hash); + break; + } + } + + assert!(found_ja3_hash.is_some(), "No JA3 hash from a TLS handshake was found"); + let ja3_hash = found_ja3_hash.unwrap(); + assert_eq!(ja3_hash.ja3_str, expected_str); - assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash); + assert_eq!(format!("{:x}", ja3_hash.hash.unwrap()), expected_hash); assert_eq!( - IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), - ja3_hash.destination + ja3_hash.destination, + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)) ); } + }