DF-0878 / 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 | /* * DF-0878 — Deterministic harness transcribing cd9660_rrip_loop() + * cd9660_rrip_altname() (NM handler) verbatim, with a poisoned allocator. * * Proves the missing upper-bound check lets the NM handler bcopy() * (h.length - 5) bytes starting at p+5, reading FAR past the SUSP entry's * declared extent (past `pend`, i.e. past the directory record) into * adjacent heap. The OOB region is pre-filled with a recognizable * sentinel so the leak extent is measured exactly. * * This mirrors: * sys/vfs/isofs/cd9660/cd9660_rrip.c:503-539 (cd9660_rrip_loop inner loop) * sys/vfs/isofs/cd9660/cd9660_rrip.c:225-283 (cd9660_rrip_altname, cflag=0) * sys/vfs/isofs/cd9660/cd9660_rrip.h:41-45 (ISO_SUSP_HEADER) * sys/vfs/isofs/cd9660/cd9660_rrip.h:81-84 (ISO_RRIP_ALTNAME) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> /* ---- transcribed kernel types (exact layout) ---- */ typedef struct { char type[2]; uint8_t length; /* 7.11 */ uint8_t version; } ISO_SUSP_HEADER; /* 4 bytes */ typedef struct { ISO_SUSP_HEADER h; char flags; /* offset 4 */ } ISO_RRIP_ALTNAME; /* 5 byte header; name follows at offset 5 */ /* 7.11 decode: a single unsigned byte */ static unsigned i711(const uint8_t *p) { return p[0]; } /* * Faithful transcription of cd9660_rrip_altname() cflag==0 path * (cd9660_rrip.c:256-277). We only exercise the cflag=0 branch because * that is the one that uses h.length to size the bcopy. * * Returns number of bytes copied into outbuf (== wlen), and sets *oob_flag * if the source range [inbuf, inbuf+wlen) extends past `pend`. */ static int rrip_altname_nm(ISO_SUSP_HEADER *p, char *pend, char *outbuf, int maxlen, int *leaked_oob_bytes) { char *inbuf; int wlen; char flags = ((char *)p)[4]; /* ISO_RRIP_ALTNAME.flags at offset 4 */ if (flags != 0) /* only cflag==0 path modeled */ return 0; wlen = i711((uint8_t*)&p->length) - 5; /* :258 */ inbuf = (char *)p + 5; /* :259 */ if (wlen > maxlen) wlen = maxlen; /* mimic the ana->maxlen clamp :268 */ /* how far past pend does the source read go? */ const char *src_end = inbuf + wlen; *leaked_oob_bytes = (src_end > pend) ? (int)(src_end - pend) : 0; /* the actual bcopy from :276 — copies wlen bytes from inbuf */ memcpy(outbuf, inbuf, wlen); return wlen; } /* * Faithful transcription of the cd9660_rrip_loop inner while-loop * (cd9660_rrip.c:509-538). We model a single NM entry and show the * dispatch happens WITHOUT any `phead + h.length <= pend` check. * * Returns total leaked OOB bytes. */ static int rrip_loop_transcribe(char *su_start, char *pend, char *outbuf, int maxlen) { ISO_SUSP_HEADER *phead = (ISO_SUSP_HEADER *)su_start; int leaked_total = 0; /* :509 while (pend >= phead + 1) -- only checks the 4B header fits */ while (pend >= (char *)phead + 1) { if (phead->version == 1) { /* :510 */ /* :511-518 match 'NM' and dispatch IMMEDIATELY */ if (phead->type[0] == 'N' && phead->type[1] == 'M') { int leaked = 0; int n = rrip_altname_nm(phead, pend, outbuf, maxlen, &leaked); printf("[loop] dispatched NM handler: copied %d " "bytes; %d bytes read PAST pend (OOB)\n", n, leaked); leaked_total += leaked; } } /* :530 plausibility check -- LOWER bound only, no upper bound */ if (i711((uint8_t*)&phead->length) < sizeof(*phead)) break; /* :537 advance by h.length */ phead = (ISO_SUSP_HEADER *)((char *)phead + i711((uint8_t*)&phead->length)); if ((char *)phead >= pend) /* end of modeled single-entry case */ break; } return leaked_total; } #define SENTINEL 0x5a /* recognizable poison byte in the OOB region */ int main(void) { /* * Simulate a small directory record tail near a block boundary. * The "valid" record region is `RECORD_LEN` bytes; everything after * is adjacent kernel heap (poisoned). We place an NM entry whose * declared length grossly exceeds the available System Use space. */ const int RECORD_LEN = 16; /* tiny record: 5B NM header + 3B name + 8B slack */ const int HEAP_TAIL = 512; /* simulated adjacent heap to observe leak into */ const int CLAIMED_LEN = 255; /* h.length the attacker forges */ char *region = calloc(1, RECORD_LEN + HEAP_TAIL); if (!region) { perror("calloc"); return 1; } /* poison the adjacent-heap region with the sentinel */ memset(region + RECORD_LEN, SENTINEL, HEAP_TAIL); /* build the directory record: a single NM entry at offset 0 */ ISO_SUSP_HEADER *nm = (ISO_SUSP_HEADER *)region; nm->type[0] = 'N'; nm->type[1] = 'M'; nm->length = CLAIMED_LEN; /* :515 dispatch trusts this */ nm->version = 1; region[4] = 0; /* flags = 0 (component name) :236 */ /* put 3 legitimate name bytes in the valid region */ region[5] = 'f'; region[6] = 'o'; region[7] = 'o'; /* `pend` = end of the directory record (the kernel's bound) */ char *pend = region + RECORD_LEN; char outbuf[300]; memset(outbuf, 0xee, sizeof(outbuf)); int maxlen = 255; /* ana->maxlen */ printf("=== DF-0878 harness: cd9660_rrip_loop + NM handler OOB read ===\n"); printf("record region: [%p, %p) (%d bytes valid)\n", region, pend, RECORD_LEN); printf("NM h.length: %d (claimed)\n", CLAIMED_LEN); printf("NM handler wlen: %d (h.length - 5)\n", CLAIMED_LEN - 5); printf("valid name bytes in record: 3 ('foo')\n"); printf("adjacent-heap poison byte: 0x%02x\n", SENTINEL); printf("--\n"); int leaked = rrip_loop_transcribe(region, pend, outbuf, maxlen); printf("--\n"); printf("OUTPUT (filename) bytes copied into outbuf:\n "); /* show first 40 bytes as hex + ascii */ for (int i = 0; i < 40 && i < (CLAIMED_LEN-5); i++) { printf("%02x", (unsigned char)outbuf[i]); if (i == 39) printf("..."); } printf("\n ascii: "); for (int i = 0; i < 40 && i < (CLAIMED_LEN-5); i++) { unsigned char c = outbuf[i]; putchar((c >= 32 && c < 127) ? c : '.'); if (i == 39) printf("..."); } printf("\n"); printf("--\n"); int poison_count = 0; for (int i = 0; i < (CLAIMED_LEN-5); i++) if ((unsigned char)outbuf[i] == SENTINEL) poison_count++; printf("RESULT: %d bytes read PAST the directory-record boundary (pend)\n", leaked); printf(" %d of those leaked bytes carry the heap sentinel 0x%02x " "(kernel heap info disclosure)\n", poison_count, SENTINEL); printf(" Only 3 bytes ('foo') were legitimately inside the record.\n"); printf("BUG CONFIRMED: cd9660_rrip_loop dispatches the NM handler at\n"); printf(" cd9660_rrip.c:515 with NO check that phead+h.length<=pend;\n"); printf(" the plausibility check at :530 is lower-bound only.\n"); free(region); return (leaked > 0) ? 0 : 1; } |