DF-1076 / harness.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 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | /* SPDX-License-Identifier: BSD-2-Clause * DF-1076 โ faithful userspace harness for the ichsmb block-read OOB write. * * The vulnerable path (sys/bus/smbus/ichsmb/ichsmb.c) is reachable ONLY when * an ichsmb PCI SMBus controller is present AND a malicious SMBus/I2C slave * is electrically on the bus. The audit QEMU guest has NO ichsmb PCI device * (PCI 0:1:3 is the PIIX4 *ACPI* function, not the SMBus function โ class * 0x068000 not 0x0c05), so the bug cannot be triggered dynamically here. * * This harness reproduces the *exact algorithmic logic* of the vulnerable * ISR block-read branch so the primitive can be demonstrated and the fix * validated without malicious hardware. It: * * 1. Allocates a struct laid out byte-for-byte like `struct ichsmb_softc` * (per sys/bus/smbus/ichsmb/ichsmb_var.h:47-66), so `block_data[32]` * is immediately followed by `struct lock mutex` and a poison canary. * 2. Implements `bus_read_1()` as a controllable stub that returns the * slave-supplied count (255) and payload byte (0xAA) โ exactly what a * malicious peripheral would put on the wire. * 3. Drives the same loop the ISR drives (ichsmb.c:574-585), calling the * stub once per BYTE_DONE_STS, until block_index reaches block_count. * * Run twice: once with CLAMP=0 (the unpatched kernel โ corruption expected), * once with CLAMP=1 (the patched kernel โ clamp at ichsmb.c:575 prevents the * overflow). The harness compares the canary bytes adjacent to block_data[] * in each case to make the primitive and its closure visible. * * Build: cc -O0 -o harness harness.c * or: cc -O0 -DCLAMP_FIX -o harness_clamped harness.c * Run: ./harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> /* ---- Mirror sys/bus/smbus/ichsmb/ichsmb_var.h:47-66 layout exactly ---- */ struct lock { /* stand-in; kernel's struct lock is larger but the * harness only needs the *adjacency* โ what matters * is that overflow past block_data[32] lands in * something the kernel cares about. */ char payload[64]; }; struct ichsmb_softc { /* Device/bus stuff (pointers โ sizes don't matter for adjacency) */ void *dev, *smb, *io_res; int io_rid; void *irq_res; int irq_rid; void *irq_handle; /* Device state */ int ich_cmd; int smb_error; int block_count; /* ichsmb_var.h:61 */ int block_index; /* ichsmb_var.h:62 */ uint8_t block_write; /* ichsmb_var.h:63 */ uint8_t block_data[32]; /* ichsmb_var.h:64 โ the fixed-size buffer */ struct lock mutex; /* ichsmb_var.h:65 โ first victim of OOB */ /* Harness canary: anything that writes past block_data lands here. */ uint8_t canary[192]; }; /* ICH register numbers that the ISR reads from. */ #define ICH_D0 0x05 /* count byte supplied by the slave */ #define ICH_BLOCK_DB 0x07 /* next data byte supplied by the slave */ /* * bus_read_1 stand-in. The harness drives this so that the FIRST read of * ICH_D0 returns 255 (the slave's "I'm sending 255 bytes" lie) and any * read of ICH_BLOCK_DB returns PAYLOAD (0xAA โ the attacker's bytes). * * On real hardware this byte stream is exactly what comes back from a * malicious SMBus slave or a malicious DIMM SPD EEPROM. */ #define COUNT_BYTE 255 #define PAYLOAD_BYTE 0xAA static uint8_t bus_read_1(int reg) { switch (reg) { case ICH_D0: return COUNT_BYTE; case ICH_BLOCK_DB: return PAYLOAD_BYTE; default: return 0; } } /* * Reproduce ichsmb_device_intr block-read branch (ichsmb.c:571-597). * We model only the BYTE_DONE_STS path: each call processes one byte, * incrementing block_index until it reaches block_count, exactly as the * real ISR does across multiple interrupts. */ static void block_read_isr_step(struct ichsmb_softc *sc) { /* ichsmb.c:573-577 โ "First interrupt, get the count also" */ if (sc->block_index == 0) { sc->block_count = bus_read_1(ICH_D0); #ifdef CLAMP_FIX /* The fix: clamp slave-supplied count to sizeof(block_data). */ if (sc->block_count > (int)sizeof(sc->block_data)) sc->block_count = sizeof(sc->block_data); #endif } /* ichsmb.c:579-585 โ "Get next byte, if any" + indexed write */ if (sc->block_index < sc->block_count) { sc->block_data[sc->block_index++] = bus_read_1(ICH_BLOCK_DB); /* (LAST_BYTE handling at ichsmb.c:587-596 omitted โ it only * touches bus_write_1, not block_data, so it can't affect the * primitive.) */ } } int main(void) { struct ichsmb_softc *sc = calloc(1, sizeof(*sc)); /* Simulate ichsmb_bread() setup (ichsmb.c:424-429): * - user-supplied *count was validated to be in [1,32]; * - sc->block_count is ZEROED here (so the user bound does NOT * constrain the slave-supplied value the ISR will read later). */ sc->block_count = 0; /* ichsmb.c:427 */ sc->block_index = 0; /* ichsmb.c:428 */ sc->block_write = 0; /* ichsmb.c:429 โ read direction */ /* Poison the canary so we can detect any OOB write. */ memset(sc->canary, 0xCC, sizeof(sc->canary)); memset(sc->mutex.payload, 0xDD, sizeof(sc->mutex.payload)); printf("sizeof(ichsmb_softc) = %zu\n", sizeof(*sc)); printf("offsetof(block_data) = %zu\n", (size_t)((uint8_t*)sc->block_data - (uint8_t*)sc)); printf("offsetof(mutex) = %zu (first byte AFTER block_data[31])\n", (size_t)((uint8_t*)&sc->mutex - (uint8_t*)sc)); printf("offsetof(canary) = %zu\n", (size_t)((uint8_t*)sc->canary - (uint8_t*)sc)); #ifdef CLAMP_FIX printf("\n[CLAMP_FIX enabled โ modeling the patched kernel]\n"); #else printf("\n[CLAMP_FIX disabled โ modeling the unpatched audit-source kernel]\n"); #endif printf("slave-supplied count byte (ICH_D0) = %u (>32 = %s)\n", (unsigned)COUNT_BYTE, COUNT_BYTE > 32 ? "YES โ OOB" : "no"); printf("payload byte (ICH_BLOCK_DB) = 0x%02x\n", PAYLOAD_BYTE); /* Drive the ISR for up to 256 byte-done events. */ int steps = 0; int prev_index = -1; while (sc->block_index != prev_index && steps < 300) { prev_index = sc->block_index; block_read_isr_step(sc); steps++; } printf("\npost-ISR block_count = %d\n", sc->block_count); printf("post-ISR block_index = %d (== number of bytes written)\n", sc->block_index); /* Diagnostic: dump the first 8 bytes of each region so we can SEE * what landed where. */ printf("\n-- hex dump --\n"); printf("block_data[0..7] :"); for (int i = 0; i < 8; i++) printf(" %02x", sc->block_data[i]); printf("\nblock_data[24..31]:"); for (int i = 24; i < 32; i++) printf(" %02x", sc->block_data[i]); printf("\nmutex.payload[0..7] :"); for (int i = 0; i < 8; i++) printf(" %02x", (uint8_t)sc->mutex.payload[i]); printf("\ncanary[0..7] :"); for (int i = 0; i < 8; i++) printf(" %02x", sc->canary[i]); printf("\ncanary[151..159] :"); for (int i = 151; i < 160 && i < (int)sizeof(sc->canary); i++) printf(" %02x", sc->canary[i]); printf("\n"); /* Tally corruption: how many bytes after block_data[31] are * no longer the poison value? Cast to uint8_t โ `char` is signed * on this platform and 0xAA would sign-extend. */ int mutex_corrupt = 0; for (size_t i = 0; i < sizeof(sc->mutex.payload); i++) if ((uint8_t)sc->mutex.payload[i] != 0xDD) mutex_corrupt++; int canary_corrupt = 0; for (size_t i = 0; i < sizeof(sc->canary); i++) if ((uint8_t)sc->canary[i] != 0xCC) canary_corrupt++; printf("bytes corrupted inside struct lock mutex = %d / %zu\n", mutex_corrupt, sizeof(sc->mutex.payload)); printf("bytes corrupted in trailing canary = %d / %zu\n", canary_corrupt, sizeof(sc->canary)); /* Direct bounds check: how many bytes of block_data, mutex.payload, * and canary equal the payload (0xAA) vs the original poison? */ int bd_payload = 0, mtx_payload = 0, can_payload = 0; for (size_t i = 0; i < sizeof(sc->block_data); i++) if ((uint8_t)sc->block_data[i] == PAYLOAD_BYTE) bd_payload++; for (size_t i = 0; i < sizeof(sc->mutex.payload); i++) if ((uint8_t)sc->mutex.payload[i] == PAYLOAD_BYTE) mtx_payload++; for (size_t i = 0; i < sizeof(sc->canary); i++) if ((uint8_t)sc->canary[i] == PAYLOAD_BYTE) can_payload++; printf("\npayload-bytes (0xAA) found in block_data[32] : %d / 32\n", bd_payload); printf("payload-bytes (0xAA) found in mutex.payload[64]: %d / 64\n", mtx_payload); printf("payload-bytes (0xAA) found in canary[192] : %d / 192\n", can_payload); if (mtx_payload == 0 && can_payload == 0) { printf("\nVERDICT: NO OVERFLOW โ writes stayed inside block_data[32].\n"); return 0; } printf("\nVERDICT: OOB WRITE CONFIRMED โ %d payload-bytes landed past " "block_data[31] (%d in mutex, %d in canary) โ primitive matches " "the ichsmb.c:583 indexed write driven by the unclamped slave " "count read at ichsmb.c:575.\n", mtx_payload + can_payload, mtx_payload, can_payload); printf("On a live kernel, corrupting struct lock mutex causes an " "immediate panic on the next lockmgr op (ichsmb.c:615).\n"); return 0; } |