Skip to main content

nullsec_carfuzz/
lib.rs

1/// NullSec CarFuzz — Automotive Protocol Fuzzer
2/// Core library: protocol definitions, frame builders, mutation engine
3pub mod protocols;
4
5use rand::Rng;
6use rand::seq::SliceRandom;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// A raw CAN frame (11-bit or 29-bit arbitration ID, up to 8 bytes data).
11#[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/// Fuzzing result for a single frame.
38#[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/// Mutation strategies.
46#[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
57/// Mutate a byte vector with bit flips, byte substitution, boundary values.
58pub 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            // bit flip
66            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            // byte replacement
72            let idx = rng.gen_range(0..out.len());
73            out[idx] = rng.gen();
74        }
75        2 => {
76            // boundary insert
77            let idx = rng.gen_range(0..out.len());
78            out[idx] = *[0x00u8, 0xFF, 0x7F, 0x80, 0x01, 0xFE].choose(rng).unwrap();
79        }
80        _ => {
81            // truncate or extend
82            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
92/// Detect anomalies in a response frame.
93pub fn detect_anomaly(request: &CanFrame, response: &CanFrame) -> Option<String> {
94    // NRC 0x78 (response pending) or negative response service 0x7F
95    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    // Unexpected silence or short data
100    if response.data.is_empty() {
101        return Some("Empty response — possible ECU hang".into());
102    }
103    // Timeout indicator (data = [0xDE, 0xAD])
104    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}