1pub mod protocols;
4
5use rand::Rng;
6use rand::seq::SliceRandom;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct CanFrame {
13 pub arb_id: u32,
14 pub extended: bool,
15 pub data: Vec<u8>,
16}
17
18impl CanFrame {
19 pub fn new(arb_id: u32, data: Vec<u8>) -> Self {
20 Self { arb_id, extended: arb_id > 0x7FF, data }
21 }
22
23 pub fn random(rng: &mut impl Rng) -> Self {
24 let arb_id: u32 = rng.gen_range(0..0x800);
25 let len: usize = rng.gen_range(1..=8);
26 let data: Vec<u8> = (0..len).map(|_| rng.gen()).collect();
27 Self::new(arb_id, data)
28 }
29}
30
31impl fmt::Display for CanFrame {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(f, "[{:03X}] {:?}", self.arb_id, self.data)
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct FuzzResult {
40 pub frame: CanFrame,
41 pub anomaly: Option<String>,
42 pub timestamp_ms: u64,
43}
44
45#[derive(Debug, Clone, PartialEq)]
47pub enum FuzzMode {
48 Random,
49 Dict(Vec<Vec<u8>>),
50 Mutate(Vec<Vec<u8>>),
51 Protocol,
52 Discovery { start: u32, end: u32 },
53 Bruteforce { target: u32, service: u8 },
54 Extract { target: u32, start: u32, end: u32 },
55}
56
57pub fn mutate(data: &[u8], rng: &mut impl Rng) -> Vec<u8> {
59 let mut out = data.to_vec();
60 if out.is_empty() {
61 return out;
62 }
63 match rng.gen_range(0u8..4) {
64 0 => {
65 let idx = rng.gen_range(0..out.len());
67 let bit = rng.gen_range(0u8..8);
68 out[idx] ^= 1 << bit;
69 }
70 1 => {
71 let idx = rng.gen_range(0..out.len());
73 out[idx] = rng.gen();
74 }
75 2 => {
76 let idx = rng.gen_range(0..out.len());
78 out[idx] = *[0x00u8, 0xFF, 0x7F, 0x80, 0x01, 0xFE].choose(rng).unwrap();
79 }
80 _ => {
81 if out.len() > 1 && rng.gen_bool(0.5) {
83 out.pop();
84 } else if out.len() < 8 {
85 out.push(rng.gen());
86 }
87 }
88 }
89 out
90}
91
92pub fn detect_anomaly(request: &CanFrame, response: &CanFrame) -> Option<String> {
94 if response.data.first() == Some(&0x7F) {
96 let nrc = response.data.get(2).copied().unwrap_or(0);
97 return Some(format!("UDS Negative Response NRC=0x{:02X}", nrc));
98 }
99 if response.data.is_empty() {
101 return Some("Empty response — possible ECU hang".into());
102 }
103 if response.data == [0xDE, 0xAD] {
105 return Some(format!(
106 "Timeout after request to 0x{:03X}",
107 request.arb_id
108 ));
109 }
110 None
111}