DF-0795 / df0795_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 | /* * DF-0795 deterministic harness. * * Replicates the stack layout of msdosfs_readdir exactly as the gcc-8.3 * compiler lays it out in /boot/kernel/msdos.ko (verified by objdump): * * -0x250(%rbp) : struct mbnambuf nb (272 bytes) * +0 nb_len (size_t / 8 bytes) * +8 nb_last_id (int / 4 bytes + 4 pad) * +16 nb_buf[256] * -0x140(%rbp) : struct dirent dirbuf (272 bytes) <-- overflow target * +0 d_ino (ino_t / 8 bytes) * +8 d_namlen (uint16) * +10 d_type (uint8) * +11 d_unused1 * +12 d_unused2 (uint32) * +16 d_name[256] * * The 4-byte (ASCII) overflow at nb_buf[247..259] lands in dirbuf.d_ino[0..3]. * The 17-byte (KICONV, all 13 chars 2-byte) overflow lands in * dirbuf.d_ino[0..7], d_namlen, d_type, d_unused1, d_unused2, d_name[0]. * * The harness reproduces mbnambuf_write() verbatim with the malicious * id=19, count=13 (and count=26 KICONV variant), with sentinel-poisoned * stack frames, and prints exactly which bytes are corrupted. * * Build (userland, 64-bit): cc -O2 -o df0795_harness df0795_harness.c * Run: ./df0795_harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <stddef.h> #define WIN_CHARS 13 #define WIN_MAXLEN 255 struct mbnambuf { size_t nb_len; int nb_last_id; char nb_buf[WIN_MAXLEN + 1]; }; struct dirent_h { uint64_t d_ino; uint16_t d_namlen; uint8_t d_type; uint8_t d_unused1; uint32_t d_unused2; char d_name[256]; }; /* Verbatim transcription of msdosfs_conv.c:1031-1067 (mbnambuf_write). */ static int mbnambuf_write(struct mbnambuf *nbp, char *name, int id) { char *slot; size_t count, newlen; if (nbp->nb_len != 0 && id != nbp->nb_last_id - 1) { return -1; } slot = &nbp->nb_buf[id * WIN_CHARS]; count = strlen(name); newlen = nbp->nb_len + count; if (newlen > WIN_MAXLEN || newlen > 127) { return -1; } if (count > WIN_CHARS && nbp->nb_len != 0) { if ((id * WIN_CHARS + count + nbp->nb_len) > sizeof(nbp->nb_buf)) return -1; memmove(slot + count, slot + WIN_CHARS, nbp->nb_len); } memcpy(slot, name, count); nbp->nb_len = newlen; nbp->nb_last_id = id; return 0; } static void dump_bytes(const char *label, const unsigned char *p, size_t n) { printf(" %-30s", label); for (size_t i = 0; i < n; i++) printf("%02x", p[i]); printf("\n"); } int main(void) { /* Mimic msdosfs_readdir stack: nb immediately followed by dirbuf. */ struct mbnambuf nb; struct dirent_h dirbuf; /* ---- ASCII variant: count=13, 4-byte overflow into dirbuf.d_ino ---- */ memset(&nb, 0xAA, sizeof(nb)); /* poison */ memset(&dirbuf, 0xBB, sizeof(dirbuf)); /* poison */ nb.nb_len = 0; nb.nb_last_id = -1; char name13[14]; memset(name13, 0, sizeof(name13)); for (int i = 0; i < 13; i++) name13[i] = 'A' + i; /* ABCDEFGHIJKLM */ printf("== DF-0795 ASCII variant (weCnt=0x54, id=19, count=13) ==\n"); int rc = mbnambuf_write(&nb, name13, 19); printf("mbnambuf_write returned %d (0 = OK, overflow happened)\n", rc); printf("nb_buf size = %zu\n", sizeof(nb.nb_buf)); printf("slot offset = 19*13 = %d (writes nb_buf[247..259])\n", 19*13); printf("overflow bytes = nb_buf[256..259] = 4 bytes past end\n"); printf("These 4 bytes land in dirbuf.d_ino low 4 bytes.\n\n"); dump_bytes("nb_buf[244..259] =", (unsigned char *)&nb.nb_buf[244], 16); /* Walk the 4 overflow bytes and report what they hit in dirbuf. */ printf("byte-by-byte corruption map:\n"); for (int i = 0; i < 4; i++) { unsigned char *p = (unsigned char *)&nb.nb_buf[256 + i]; /* nb_buf[256+i] is at offset offsetof(dirbuf)+i, i.e. dirbuf byte i */ ptrdiff_t dir_off = p - (unsigned char *)&dirbuf; const char *field = dir_off < 8 ? "dirbuf.d_ino" : dir_off < 10 ? "dirbuf.d_namlen" : dir_off == 10 ? "dirbuf.d_type" : dir_off == 11 ? "dirbuf.d_unused1" : dir_off < 16 ? "dirbuf.d_unused2" : "dirbuf.d_name"; printf(" nb_buf[%d] -> dirbuf byte %td (%s) = 0x%02x\n", 256+i, dir_off, field, *p); } printf("\n"); printf("Result: dirbuf.d_ino low 32 bits corrupted with 'HIJK' " "(0x4a4b4849 little-endian).\n"); printf("-> NO control-flow primitive corrupted (no fn ptr, no rbp, no ret addr).\n"); printf("-> In msdosfs_readdir, dirbuf.d_ino is unconditionally OVERWRITTEN\n"); printf(" at msdosfs_vnops.c:1684/1694/1698 before vop_write_dirent, so the\n"); printf(" corruption is discarded. Bug is real (CWE-787) but contained.\n\n"); /* ---- KICONV variant: count=26, 17-byte overflow ---- */ memset(&nb, 0xAA, sizeof(nb)); memset(&dirbuf, 0xBB, sizeof(dirbuf)); nb.nb_len = 0; nb.nb_last_id = -1; char name26[27]; memset(name26, 0, sizeof(name26)); for (int i = 0; i < 26; i++) name26[i] = 'a' + (i % 26); /* abc...z */ printf("== DF-0795 KICONV variant (weCnt=0x54, id=19, count=26) ==\n"); /* NB: in real msdosfs, count>WIN_CHARS would trip the shift-check on a * SECOND call (nb_len!=0). On the FIRST call (nb_len==0) the shift * check is skipped, so the unchecked memcpy(slot, name, 26) fires. */ rc = mbnambuf_write(&nb, name26, 19); printf("mbnambuf_write returned %d (0 = OK, overflow happened)\n", rc); printf("slot offset = 247, count = 26 -> writes nb_buf[247..272]\n"); printf("overflow bytes = nb_buf[256..272] = 17 bytes past end\n"); printf("These 17 bytes land in:\n"); printf(" dirbuf.d_ino (8), d_namlen (2), d_type (1),\n"); printf(" d_unused1 (1), d_unused2 (4), d_name[0] (1)\n\n"); dump_bytes("nb_buf[244..271] =", (unsigned char *)&nb.nb_buf[244], 28); for (int i = 0; i < 17; i++) { unsigned char *p = (unsigned char *)&nb.nb_buf[256 + i]; ptrdiff_t dir_off = p - (unsigned char *)&dirbuf; const char *field = dir_off < 8 ? "dirbuf.d_ino" : dir_off < 10 ? "dirbuf.d_namlen" : dir_off == 10 ? "dirbuf.d_type" : dir_off == 11 ? "dirbuf.d_unused1" : dir_off < 16 ? "dirbuf.d_unused2" : "dirbuf.d_name"; printf(" nb_buf[%d] -> dirbuf byte %td (%s) = 0x%02x\n", 256+i, dir_off, field, *p); } printf("\nConclusion: KICONV variant corrupts more of dirbuf but STILL\n"); printf("no control-flow primitive (no fn ptr / rbp / ret addr).\n"); printf("All corrupted dirbuf fields are unconditionally overwritten by\n"); printf("msdosfs_readdir before vop_write_dirent uses them.\n"); return 0; } |