DF-1918 / harness.c
/* * DF-1918 source-confirmation harness โ mrsas_passthru unbounded sense_off * yields an OOB pointer-sized (8-byte) write past the 1024-byte cmd->frame. * * sys/dev/raid/mrsas/mrsas_ioctl.c:291-293 * sense_ptr = (unsigned long *) * ((unsigned long)cmd->frame + user_ioc->sense_off); * *sense_ptr = ioctl_sense_phys_addr; * * user_ioc->sense_off is a u_int32_t (mrsas_ioctl.h:85) taken verbatim * from userspace and NEVER validated. The store writes 8 bytes * (sizeof(unsigned long) on amd64) of a kernel physical DMA address at * offset sense_off from cmd->frame. cmd->frame is bus_dmamem_alloc'd at * MRSAS_MFI_FRAME_SIZE=1024 bytes (mrsas.h:876, mrsas.c:451-462). With * sense_off>=1016 (1024 - sizeof(unsigned long)) the 8-byte store spills * past the allocation. sense_off=0xFFFFFF F8 wraps to (cmd->frame - 8). * * Pre-condition: mrsas_ioctl.c:267 `if (user_ioc->sense_len)` gates the * whole block, so the attacker must also set sense_len>0 โ trivial. * * The primitive is more powerful than DF-1917 in one sense (the offset is * arbitrary, no SGE-count multiplier), less powerful in another (the * written VALUE is a fixed kernel physical address the attacker cannot * choose, only the OFFSET). Combined with an info leak of the sense * buffer physical address it becomes a write-what-where primitive; absent * that, it is an arbitrary-offset 8-byte write of a low-entropy value. * * Why a harness: same Phase-6 blocker as DF-1917 โ no LSI MegaRAID SAS * HBA in the QEMU audit guest (verified: pciconf -lv lists none; * /dev/mrsas* absent), so mrsas_passthru is unreachable at runtime, and * /dev/mrsas0 would be 0660 root:operator (mrsas.c:790) which maxx * (uid 1001, not in operator) cannot open anyway. * * Build: cc -O2 -o harness harness.c * Run: ./harness */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #define MRSAS_MFI_FRAME_SIZE 1024 /* mrsas.h:876 */ /* Verbatim mrsas_ioctl.c:291-293 store against a 1024-byte cmd->frame * model. Returns the number of bytes written past the end. */ static unsigned int spill_bytes(const uint32_t sense_off) { unsigned char *frame = calloc(1, MRSAS_MFI_FRAME_SIZE + 16); unsigned char *base = frame; unsigned long phys = 0xDEADBEEFul; /* model ioctl_sense_phys_addr */ /* sense_ptr = (unsigned long *)((unsigned long)cmd->frame + sense_off) */ unsigned long *sense_ptr = (unsigned long *)(base + sense_off); /* *sense_ptr = ioctl_sense_phys_addr; -- the unchecked store */ unsigned char *p = (unsigned char *)sense_ptr; unsigned int oob = 0; for (unsigned int b = 0; b < sizeof(unsigned long); b++) if (p - base + b >= MRSAS_MFI_FRAME_SIZE) oob++; *sense_ptr = phys; /* exactly what the kernel does */ free(frame); return oob; } static void case_(const char *label, uint32_t sense_off) { unsigned int oob = spill_bytes(sense_off); printf(" sense_off=0x%08x -> %u/8 bytes past 1024-byte frame %s\n", sense_off, oob, oob ? "(OOB WRITE)" : "(in-bounds)"); (void)label; } int main(void) { printf("DF-1918: mrsas_passthru unbounded sense_off (mrsas_ioctl.c:291-293)\n"); printf(" *sense_ptr = ioctl_sense_phys_addr writes sizeof(unsigned long)\n"); printf(" = 8 bytes at cmd->frame + sense_off. sense_off is u32 from\n"); printf(" userspace (mrsas_ioctl.h:85), never validated.\n\n"); case_("in-bounds", 128u); case_("edge", MRSAS_MFI_FRAME_SIZE - (unsigned)sizeof(unsigned long)); /* fully in-bounds */ case_("oob-7", MRSAS_MFI_FRAME_SIZE - 7u); /* 7 of 8 bytes OOB */ case_("oob-full", MRSAS_MFI_FRAME_SIZE); /* all 8 bytes OOB */ case_("oob-deep", MRSAS_MFI_FRAME_SIZE + 4096u);/* 1 page past */ case_("wrap", 0xFFFFFFF8u); /* cmd->frame - 8 */ printf("\n Confirmed: sense_off >= 1017 spills 1..8 bytes; sense_off\n"); printf(" >= 1024 spills entirely; 0xFFFFFFF8 wraps to cmd->frame - 8.\n"); return 0; } |