1use std::fs::File;
44use std::ffi::{OsStr, OsString};
45use std::fmt;
46use std::net::IpAddr;
47
48use lazy_static::*;
49use log::{info, debug};
50use md5::{self, Digest};
51#[cfg(feature = "live-capture")]
52use pcap::{Active, Capture};
53use pcap_parser::{LegacyPcapReader, PcapBlockOwned, PcapError};
54use pcap_parser::traits::PcapReaderIterator;
55use pnet::packet::ethernet::EtherType;
56use pnet::packet::ip::IpNextHeaderProtocol;
57use pnet::packet::ip::IpNextHeaderProtocols;
58use pnet::packet::*;
59use tls_parser::parse_tls_plaintext;
60use tls_parser::tls::{TlsMessage, TlsMessageHandshake, TlsRecordType};
61use tls_parser::tls_extensions::{parse_tls_extensions, TlsExtension, TlsExtensionType};
62
63mod errors;
64use errors::*;
65use failure::Error;
66
67lazy_static! {
68 static ref IPTYPE: IpNextHeaderProtocol = IpNextHeaderProtocol::new(6);
69 static ref GREASE: Vec<u16> = vec![
70 0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa,
71 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa
72 ];
73}
74
75#[derive(Debug)]
77pub struct Ja3 {
78 i: Ja3Inner,
79}
80
81#[derive(Debug)]
83struct Ja3Inner {
84 path: OsString,
85 tls_port: u16,
86}
87
88#[derive(Debug, Eq)]
90pub struct Ja3Hash {
91 pub ja3_str: String,
94 pub hash: Digest,
96 pub source: IpAddr,
98 pub destination: IpAddr,
100}
101
102#[cfg(feature = "live-capture")]
104pub struct Ja3Live {
105 cap: Capture<Active>,
106 ja3_inner: Ja3,
107}
108
109#[cfg(feature = "live-capture")]
110impl Iterator for Ja3Live {
111 type Item = Ja3Hash;
112
113 fn next(&mut self) -> Option<Self::Item> {
114 while let Ok(packet) = self.cap.next() {
115 match self.ja3_inner.process_packet_common(&packet) {
116 Ok(s) => return Some(s),
117 Err(_) => continue,
118 };
119 }
120
121 None
122 }
123}
124
125impl Ja3 {
126 pub fn new<S: AsRef<OsStr>>(pcap_path: S) -> Self {
132 let mut path = OsString::new();
133 path.push(pcap_path);
134 let i = Ja3Inner {
135 path: path,
136 tls_port: 443,
137 };
138
139 Ja3 { i: i }
140 }
141
142 pub fn any_port<'a>(&'a mut self) -> &'a mut Self {
145 self.i.tls_port = 0;
146 self
147 }
148
149 pub fn process_pcap(&self) -> Result<Vec<Ja3Hash>, Error> {
151 let mut results: Vec<Ja3Hash> = Vec::new();
152
153 let file = File::open(&self.i.path)?;
154 let mut reader = LegacyPcapReader::new(65536, file).expect("LegacyPcapReader");
155 loop {
156 match reader.next() {
157 Ok((offset, block)) => {
158 match block {
159 PcapBlockOwned::LegacyHeader(_hdr) => {
160 },
162 PcapBlockOwned::Legacy(block) => {
163 let ja3_hash = match self.process_packet_common(&block.data) {
164 Ok(s) => s,
165 Err(_) => {
166 reader.consume(offset);
167 continue;
168 },
169 };
170 debug!("Adding JA3: {:?}", ja3_hash);
171 results.push(ja3_hash);
172 },
173 PcapBlockOwned::NG(_) => unreachable!(),
174 }
175 reader.consume(offset);
176 },
177 Err(PcapError::Eof) => break,
178 Err(PcapError::Incomplete) => {
179 reader.refill().unwrap();
180 },
181 Err(e) => return Err(e.into()),
182 }
183 }
184
185 Ok(results)
186 }
187
188 #[cfg(feature = "live-capture")]
191 pub fn process_live(self) -> Result<Ja3Live, Error> {
192 let cap = Capture::from_device(self.i.path.to_str().unwrap())?.open()?;
193 info!("cap: {:?}", self.i.path);
194 Ok(Ja3Live {
205 cap: cap,
206 ja3_inner: self,
207 })
208 }
209
210 fn process_packet_common(&self, packet: &[u8]) -> Result<Ja3Hash, Error> {
211 let saddr;
212 let daddr;
213 let ether = ethernet::EthernetPacket::new(&packet).ok_or(Ja3Error::ParseError)?;
214 info!("\nether packet: {:?} len: {}", ether, ether.packet_size());
215 let tcp_start = match ether.get_ethertype() {
216 EtherType(0x0800) => {
217 let ip = ipv4::Ipv4Packet::new(&packet[ether.packet_size()..])
218 .ok_or(Ja3Error::ParseError)?;
219 info!("\nipv4 packet: {:?}", ip);
220 if ip.get_next_level_protocol() != *IPTYPE {
221 return Err(Ja3Error::ParseError)?;
222 }
223 let iphl = ip.get_header_length() as usize * 4;
224 saddr = IpAddr::V4(ip.get_source());
225 daddr = IpAddr::V4(ip.get_destination());
226 iphl + ether.packet_size()
227 }
228 EtherType(0x86dd) => {
229 let ip = ipv6::Ipv6Packet::new(&packet[ether.packet_size()..])
230 .ok_or(Ja3Error::ParseError)?;
231 info!("\nipv6 packet: {:?}", ip);
232 saddr = IpAddr::V6(ip.get_source());
233 daddr = IpAddr::V6(ip.get_destination());
234 if ip.get_next_header() != IpNextHeaderProtocols::Tcp {
235 return Err(Ja3Error::NotHandshake)?;
236 }
237 let iphl = 40;
238 iphl + ether.packet_size()
239 }
240 _ => return Err(Ja3Error::ParseError)?,
241 };
242
243 let tcp = tcp::TcpPacket::new(&packet[tcp_start..]).ok_or(Ja3Error::ParseError)?;
244 info!("tcp: {:?}", tcp);
245 if self.i.tls_port != 0 {
246 if tcp.get_destination() != 443 {
247 return Err(Ja3Error::NotHandshake)?;
248 }
249 }
250
251 info!("pack size: {}", tcp.packet_size());
252 let handshake_start = tcp_start + tcp.packet_size();
253 info!("handshake_start: {}", handshake_start);
254 let handshake = &packet[handshake_start..];
255 if handshake.len() <= 0 {
256 return Err(Ja3Error::NotHandshake)?;
257 }
258 if handshake[0] != 0x16 {
259 return Err(Ja3Error::NotHandshake)?;
260 }
261 info!("handshake: {:x?}", handshake);
262
263 info!("sending handshake {:?}", handshake);
264 let ja3_string = self.ja3_string_client_hello(&handshake).unwrap();
265 if ja3_string == "" {
266 return Err(Ja3Error::NotHandshake)?;
267 }
268
269 let hash = md5::compute(&ja3_string.as_bytes());
270 let ja3_res = Ja3Hash {
271 ja3_str: ja3_string,
272 hash: hash,
273 source: saddr,
274 destination: daddr,
275 };
276
277 Ok(ja3_res)
278 }
279
280 fn process_extensions(&self, extensions: &[u8]) -> Option<String> {
281 let mut ja3_exts = String::new();
282 let mut supported_groups = String::new();
283 let mut ec_points = String::new();
284 let (_, exts) = parse_tls_extensions(extensions).unwrap();
285 for extension in exts {
286 let ext_val = u16::from(TlsExtensionType::from(&extension));
287 if GREASE.contains(&ext_val) {
288 continue;
289 }
290 info!("Ext: {:?}", ext_val);
291 ja3_exts.push_str(&format!("{}-", ext_val));
292 match extension {
293 TlsExtension::EllipticCurves(curves) => {
294 for curve in curves {
295 if !GREASE.contains(&curve.0) {
296 info!("curve: {}", curve.0);
297 supported_groups.push_str(&format!("{}-", curve.0));
298 }
299 }
300 }
301 TlsExtension::EcPointFormats(points) => {
302 info!("Points: {:x?}", points);
303 for point in points {
304 ec_points.push_str(&format!("{}-", point));
305 }
306 }
307 _ => {}
308 }
309 }
310 ja3_exts.pop();
311 supported_groups.pop();
312 ec_points.pop();
313 info!("Supported groups: {}", supported_groups);
314 info!("EC Points: {}", ec_points);
315 let ret = format!("{},{},{}", ja3_exts, supported_groups, ec_points);
316 Some(ret)
317 }
318
319 fn ja3_string_client_hello(&self, packet: &[u8]) -> Option<String> {
320 info!("PACKET: {:?}", packet);
321 let mut ja3_string = String::new();
322 let res = parse_tls_plaintext(packet);
323 match res {
324 Ok((rem, record)) => {
325 info!("Rem: {:?}, record: {:?}", rem, record);
326 info!("record type: {:?}", record.hdr.record_type);
327 if record.hdr.record_type != TlsRecordType::Handshake {
328 return None;
329 }
330 for rec in record.msg {
331 if let TlsMessage::Handshake(handshake) = rec {
332 if let TlsMessageHandshake::ClientHello(contents) = handshake {
333 info!("handshake contents: {:?}", contents);
334 info!("handshake tls version: {:?}", u16::from(contents.version));
335 ja3_string.push_str(&format!("{},", u16::from(contents.version)));
336 for cipher in contents.ciphers {
337 info!("handshake cipher: {}", u16::from(cipher));
338 if !GREASE.contains(&cipher) {
339 ja3_string.push_str(&format!("{}-", u16::from(cipher)));
340 }
341 }
342 ja3_string.pop();
343 ja3_string.push(',');
344 if let Some(extensions) = contents.ext {
345 let ext = self.process_extensions(extensions).unwrap();
346 ja3_string.push_str(&ext);
347 }
348 }
349 }
350 }
351 }
352 _ => {
353 info!("ERROR");
354 return None;
355 }
356 }
357
358 info!("ja3_string: {}", ja3_string);
359 Some(ja3_string)
360 }
361}
362
363impl fmt::Display for Ja3Hash {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 write!(
366 f,
367 "[{} --> {}] {} {:x}",
368 self.source, self.destination, self.ja3_str, self.hash
369 )
370 }
371}
372
373impl PartialEq for Ja3Hash {
374 fn eq(&self, other: &Self) -> bool {
375 self.hash == other.hash
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use env_logger;
383 use nix::unistd::{fork, ForkResult};
384 use pretty_assertions::assert_eq;
385 use rusty_fork::rusty_fork_id;
386 use rusty_fork::rusty_fork_test;
387 use rusty_fork::rusty_fork_test_name;
388 use std::net::{IpAddr, Ipv4Addr};
389 use std::process::Command;
390
391 #[cfg(feature = "live-capture")]
394 rusty_fork_test! {
395 #[test] #[ignore]
396 fn test_ja3_client_hello_chrome_grease_single_packet_live() {
397 let expected_str = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53-10,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0";
398 let expected_hash = "66918128f1b9b03303d77c6f2eefd128";
399 let expected_daddr = IpAddr::V6("2607:f8b0:4004:814::2002".parse().unwrap());
400
401 match fork() {
402 Ok(ForkResult::Parent { child: _, .. }) => {
403 let mut ja3 = Ja3::new("lo")
404 .process_live().unwrap();
405 if let Some(x) = ja3.next() {
406 assert_eq!(x.ja3_str, expected_str);
407 assert_eq!(format!("{:x}", x.hash), expected_hash);
408 assert_eq!(expected_daddr, x.destination);
409 std::process::exit(0);
410 }
411 },
412 Ok(ForkResult::Child) => {
413 let _out = Command::new("tcpreplay")
414 .arg("-i")
415 .arg("lo")
416 .arg("chrome-grease-single.pcap")
417 .output()
418 .expect("failed to execute process");
419 },
420 Err(_) => println!("Fork failed"),
421 }
422
423 }
424 }
425
426 #[test]
427 fn test_ja3_client_hello_chrome_grease_single_packet() {
428 let expected_str = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53-10,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0";
429 let expected_hash = "66918128f1b9b03303d77c6f2eefd128";
430 let expected_daddr = IpAddr::V6("2607:f8b0:4004:814::2002".parse().unwrap());
431
432 let mut ja3 = Ja3::new("tests/chrome-grease-single.pcap")
433 .process_pcap()
434 .unwrap();
435 let ja3_hash = ja3.pop().unwrap();
436 assert_eq!(ja3_hash.ja3_str, expected_str);
437 assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash);
438 assert_eq!(expected_daddr, ja3_hash.destination);
439 }
440
441 #[test]
442 fn test_ja3_client_hello_firefox_single_packet() {
443 let expected_str = "771,49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-51-57-47-53-10,0-23-65281-10-11-35-16-5-13-28,29-23-24-25,0";
444 let expected_hash = "839bbe3ed07fed922ded5aaf714d6842";
445 let expected_daddr = IpAddr::V4("34.209.18.179".parse().unwrap());
446
447 let mut ja3 = Ja3::new("tests/test.pcap").process_pcap().unwrap();
448 let ja3_hash = ja3.pop().unwrap();
449 assert_eq!(ja3_hash.ja3_str, expected_str);
450 assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash);
451 assert_eq!(expected_daddr, ja3_hash.destination);
452 }
453
454 #[test]
455 fn test_ja3_curl_full_stream() {
456 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";
457 let expected_hash = "456523fc94726331a4d5a2e1d40b2cd7";
458 let expected_daddr = IpAddr::V4("93.184.216.34".parse().unwrap());
459
460 let mut ja3s = Ja3::new("tests/curl.pcap").process_pcap().unwrap();
461 let ja3 = ja3s.pop().unwrap();
462 assert_eq!(ja3.ja3_str, expected_str);
463 assert_eq!(format!("{:x}", ja3.hash), expected_hash);
464 assert_eq!(expected_daddr, ja3.destination);
465 }
466
467 #[test]
468 fn test_ja3_curl_full_stream_ipv6() {
469 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";
470 let expected_hash = "456523fc94726331a4d5a2e1d40b2cd7";
471 let expected_daddr = IpAddr::V6("2606:2800:220:1:248:1893:25c8:1946".parse().unwrap());
472
473 let mut ja3s = Ja3::new("tests/curl-ipv6.pcap").process_pcap().unwrap();
474 let ja3 = ja3s.pop().unwrap();
475 assert_eq!(ja3.ja3_str, expected_str);
476 assert_eq!(format!("{:x}", ja3.hash), expected_hash);
477 assert_eq!(expected_daddr, ja3.destination);
478 }
479
480 #[test]
481 fn test_ja3_client_hello_ncat_full_stream_non_tls_port() {
482 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";
483 let expected_hash = "10a6b69a81bac09072a536ce9d35dd43";
484
485 let mut ja3 = Ja3::new("tests/ncat-port-4450.pcap")
486 .any_port()
487 .process_pcap()
488 .unwrap();
489 let ja3_hash = ja3.pop().unwrap();
490 assert_eq!(ja3_hash.ja3_str, expected_str);
491 assert_eq!(format!("{:x}", ja3_hash.hash), expected_hash);
492 assert_eq!(
493 IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
494 ja3_hash.destination
495 );
496 }
497}