// Mutation fuzzer for gn_unwrap_its, the one function in this firmware that parses bytes from the // air. See README.md. // // Seeds are every recorded frame in the pcaps given, plus frames built by the firmware's own TX // code. Each iteration takes a seed, applies 1-4 random edits - bit flips, random bytes, boundary // bytes such as COER length markers, 16-bit length fields, truncation, insertion, deletion, // appended bytes - mostly within the first 128 bytes where the headers are, and runs gn_unwrap_its // on the result placed against the guard page. A read past the end crashes and prints the input; // an accepted frame whose payload is not inside the input is reported the same way. MinGW has // neither libFuzzer nor AddressSanitizer, hence this rather than coverage guidance. The same seed // gives the same run. // // Usage: fuzz_gn_unwrap iterations seed [capture.pcap ...] #include #include #include #include #include #include "dot11p.h" #include "geonet.h" #include "gn_unwrap.h" #include "test_util.h" #define MAX_FRAME 2048 // within the guard page's one page, and above any 802.11 frame static uint8_t **s_seeds; static int *s_seed_len; static int s_nseeds; static int s_seed_cap; static void add_seed(const uint8_t *f, int len) { if (len <= 0 || len > MAX_FRAME) { return; } if (s_nseeds == s_seed_cap) { s_seed_cap = s_seed_cap ? 2 * s_seed_cap : 1024; s_seeds = realloc(s_seeds, (size_t)s_seed_cap * sizeof *s_seeds); s_seed_len = realloc(s_seed_len, (size_t)s_seed_cap * sizeof *s_seed_len); if (!s_seeds || !s_seed_len) { fprintf(stderr, "out of memory\n"); exit(2); } } s_seeds[s_nseeds] = malloc((size_t)len); if (!s_seeds[s_nseeds]) { fprintf(stderr, "out of memory\n"); exit(2); } memcpy(s_seeds[s_nseeds], f, (size_t)len); s_seed_len[s_nseeds++] = len; } static void on_frame(const uint8_t *f, int len, int index, void *ctx) { (void)index; (void)ctx; add_seed(f, len); } static void add_own_seeds(void) { static const uint8_t cam[] = {0x02, 0x02, 0x00, 0x0f, 0x42, 0x3f, 0x37, 0x00, 0x40, 0x2a, 0xb2}; gn_lpv_t lpv; memset(&lpv, 0, sizeof lpv); lpv.mac[0] = 0x02; lpv.station_type = 2; uint8_t gn[256]; uint8_t f[512]; const int gn_len = geonet_wrap_shb(cam, (int)sizeof cam, &lpv, 2001, gn, sizeof gn); for (int qos = 0; qos <= 1; qos++) { add_seed(f, dot11p_build_frame(gn, gn_len, lpv.mac, f, sizeof f, qos)); } } static uint64_t s_rng; static uint64_t rnd(void) { s_rng ^= s_rng << 13; s_rng ^= s_rng >> 7; s_rng ^= s_rng << 17; return s_rng; } static int below(int n) { return n <= 0 ? 0 : (int)(rnd() % (uint64_t)n); } // Three times in four inside the headers, otherwise anywhere. static int pick_pos(int len) { return below(4) ? below(len < 128 ? len : 128) : below(len); } static const uint8_t k_bytes[] = {0x00, 0x01, 0x02, 0x03, 0x05, 0x10, 0x12, 0x20, 0x40, 0x50, 0x7F, 0x80, 0x81, 0x82, 0x83, 0xFF}; static const uint16_t k_words[] = {0x0000, 0x0001, 0x0003, 0x0004, 0x0005, 0x007F, 0x0080, 0x00FF, 0x0100, 0x7FFF, 0x8000, 0xFFFF}; static void mutate(uint8_t *b, int *len) { const int edits = 1 + below(4); for (int e = 0; e < edits; e++) { const int n = *len; switch (below(8)) { case 0: // flip a bit if (n) { b[pick_pos(n)] ^= (uint8_t)(1u << below(8)); } break; case 1: // random byte if (n) { b[pick_pos(n)] = (uint8_t)rnd(); } break; case 2: // boundary byte if (n) { b[pick_pos(n)] = k_bytes[below((int)sizeof k_bytes)]; } break; case 3: // truncate *len = below(n + 1); break; case 4: { // append const int add = 1 + below(16); if (n + add <= MAX_FRAME) { for (int i = 0; i < add; i++) { b[n + i] = (uint8_t)rnd(); } *len = n + add; } break; } case 5: // insert a byte if (n < MAX_FRAME) { const int p = below(n + 1); memmove(b + p + 1, b + p, (size_t)(n - p)); b[p] = (uint8_t)rnd(); *len = n + 1; } break; case 6: // delete a byte if (n) { const int p = below(n); memmove(b + p, b + p + 1, (size_t)(n - p - 1)); *len = n - 1; } break; default: // boundary 16-bit big-endian value, e.g. a length field if (n >= 2) { const int p = pick_pos(n - 1); const uint16_t v = k_words[below((int)(sizeof k_words / sizeof k_words[0]))]; b[p] = (uint8_t)(v >> 8); b[p + 1] = (uint8_t)v; } break; } } } int main(int argc, char **argv) { if (argc < 3) { fprintf(stderr, "usage: fuzz_gn_unwrap iterations seed [capture.pcap ...]\n"); return 2; } const unsigned long long iterations = strtoull(argv[1], NULL, 10); s_rng = (strtoull(argv[2], NULL, 10) * 0x9E3779B97F4A7C15ull) | 1; tu_install_crash_handler(); tu_guard_init(); for (int i = 3; i < argc; i++) { if (tu_pcap_foreach(argv[i], on_frame, NULL) < 0) { fprintf(stderr, "cannot read %s as a pcap\n", argv[i]); return 2; } } add_own_seeds(); static uint8_t buf[MAX_FRAME]; int len = 0; tu_set_crash_input(buf, &len); unsigned long long accepted = 0, signed_frames = 0, truncated = 0; for (unsigned long long it = 0; it < iterations; it++) { const int s = below(s_nseeds); len = s_seed_len[s]; memcpy(buf, s_seeds[s], (size_t)len); mutate(buf, &len); tu_set_context("fuzz iteration %llu (seed %s), mutated from seed frame %d", it, argv[2], s); const uint8_t *g = tu_guarded(buf, len); gn_rx_t rx; if (gn_unwrap_its(g, len, &rx)) { accepted++; signed_frames += rx.signed_unverified; truncated += rx.truncated; const uintptr_t lo = (uintptr_t)g; const uintptr_t p = (uintptr_t)rx.payload; if (p < lo || rx.payload_len <= 0 || p + (uintptr_t)rx.payload_len > lo + (uintptr_t)len) { fprintf(stderr, "FAIL during %s: accepted payload lies outside the input\ninput (%d bytes): ", tu_context(), len); for (int i = 0; i < len; i++) { fprintf(stderr, "%02x", buf[i]); } fputc('\n', stderr); return 1; } } } printf("fuzz_gn_unwrap: %llu iterations from %d seed frames, %llu accepted (%llu signed, " "%llu truncated), no crash\n", iterations, s_nseeds, accepted, signed_frames, truncated); return 0; }