DF-1869 / 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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | /* * DF-1869 โ userspace harness port of scsi_decap() ISCSI_READ_DATA path. * * Faithfully replays sys/dev/disk/iscsi/initiator/iscsi_subr.c:566-574 in * userspace. Proves: * - The only bounds check at L566 is `ntohl(cmd->edtlen) >= pq->pdu.ds_len`, * i.e. it validates ONLY the segment length, never offset+len vs edtlen. * - `offset = ntohl(rcmd->bo)` is fully attacker-controlled (bo is u_int, * iscsi.h:256; cast to signed `int offset`, so high-bit bo sign-extends). * - `dp = csio->data_ptr + offset` has NO bounds check; i_mbufcopy then * writes `len = ds_len` attacker bytes at the attacker-chosen offset. * * The harness allocates a csio->data_ptr-equivalent buffer of EDTLEN bytes * surrounded by REDZONE canaries. It then runs the exact same arithmetic as * the kernel for a chosen attacker (bo, ds_len) and reports: * - whether the (offset, offset+ds_len) write window is inside the buffer, * - how many attacker bytes landed outside the buffer (OOB write), * - in the negative-offset case, that dp lands BELOW data_ptr. * * Two builds of the same source: * harness โ unfixed kernel logic (verbatim from iscsi_subr.c) * harness_fixed โ same logic with the proposed fix (offset+len <= edtlen) * * Exit code 0 = no OOB write observed; non-zero = OOB write happened. */ #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> /* ---- kernel-faithful types (from iscsi.h / iscsivar.h) ---- */ typedef uint32_t u_int; typedef unsigned char u_char; typedef char * caddr_t; struct mbuf { struct mbuf *m_next; int m_len; char *m_data; }; #define mtod(m, ty) ((ty)((m)->m_data)) /* the buggy kernel inline, verbatim from iscsivar.h:565-583 */ static void i_mbufcopy(struct mbuf *mp, caddr_t dp, int len) { struct mbuf *m; caddr_t bp; for (m = mp; m != NULL; m = m->m_next) { bp = mtod(m, caddr_t); memcpy(dp, bp, (len < m->m_len ? len : m->m_len)); dp += m->m_len; len -= m->m_len; if (len <= 0) break; } } #define RED 0xA5 #define REDZONE 4096 /* large enough to catch every test-case OOB write */ /* * Reproduce scsi_decap() ISCSI_READ_DATA branch. * Returns number of attacker bytes that landed outside [buf, buf+edtlen). */ static long run_decap(char *buf, long edtlen, long attacker_bo, long ds_len, const char *attacker_bytes, int fixed) { /* Line 566: `if(ntohl(cmd->edtlen) >= pq->pdu.ds_len)` */ if (!(edtlen >= ds_len)) { return -1; /* kernel: xdebug("edtlen < ds_len"), falls through */ } /* Line 567: `int offset, len = pq->pdu.ds_len;` */ int offset; /* signed int, like the kernel local */ int len = (int)ds_len; /* PROPOSED FIX: bounds check the window against edtlen */ if (fixed) { u_int uoffset = (u_int)attacker_bo; u_int ulen = (u_int)ds_len; u_int uedtlen = (u_int)edtlen; if (uoffset > uedtlen || ulen > uedtlen - uoffset) { return -2; /* fix rejects bad PDU */ } } /* Line 572: `offset = ntohl(rcmd->bo);` */ offset = (int)(u_int)attacker_bo; /* kernel: signed int = u_int (impl-defined but usual 2's complement) */ /* Line 573: `dp = csio->data_ptr + offset;` */ caddr_t dp = buf + offset; /* Line 574: `i_mbufcopy(pq->mp, dp, len);` โ attacker bytes via one mbuf */ struct mbuf m; char data[4096]; memset(data, 0x5a, sizeof data); /* "attacker" pattern */ if (attacker_bytes) memcpy(data, attacker_bytes, ds_len < (long)sizeof data ? ds_len : (long)sizeof data); m.m_next = NULL; m.m_len = len; m.m_data = data; i_mbufcopy(&m, dp, len); /* count redzone-sentinel corruption outside [buf, buf+edtlen) */ long oob = 0; /* below: redzone-before (any non-RED byte = corrupted by attacker write) */ for (char *p = buf - REDZONE; p < buf; p++) if ((unsigned char)*p != RED) oob++; /* above: redzone-after */ for (char *p = buf + edtlen; p < buf + edtlen + REDZONE; p++) if ((unsigned char)*p != RED) oob++; return oob; } int main(int argc, char **argv) { int fixed = (argc > 1 && !strcmp(argv[1], "fixed")); long edtlen = 512; /* csio->dxfer_len for a 512B SCSI read */ long base_size = edtlen + 2*REDZONE; /* REDZONE bytes on each side */ char *base = (char *)malloc(base_size); if (!base) { perror("malloc"); return 2; } /* sub-cases: (label, attacker_bo, ds_len, attacker_bytes_or_NULL) */ struct { const char *name; long bo; long ds; const char *bytes; } cases[] = { /* Case A: legal, in-bounds โ both unfixed and fixed should be silent */ {"in_bounds_bo=0_ds=512", 0, 512, NULL}, /* Case B: positive OOB โ bo exactly at end of data_ptr; classic heap overrun into next slab object */ {"OOB_pos_bo=0x200_ds=0x100", 0x200, 0x100, NULL}, /* Case C: large positive OOB โ write far past allocation */ {"OOB_pos_bo=0x300_ds=0x80", 0x300, 0x80, NULL}, /* Case D: negative offset (high-bit bo) โ write BELOW data_ptr (sign-extended int offset) */ {"OOB_neg_bo=0xFFFFFC00_ds=16",0xFFFFFC00UL, 16, NULL}, /* Case E: OOB with crafted bytes (slab-groom / function-ptr pattern) */ {"OOB_pos_bo=0x200_ds=16_pattern", 0x200, 16, "\x10\x20\x30\x40\x50\x60\x70\x80\x11\x21\x31\x41\x51\x61\x71\x81"}, }; int ncases = (int)(sizeof(cases)/sizeof(cases[0])); int any_oob = 0; long total_oob_bytes = 0; for (int i = 0; i < ncases; i++) { /* reset: redzone both sides, clear interior */ memset(base, RED, base_size); memset(base + REDZONE, 0, edtlen); char *buf = base + REDZONE; /* == csio->data_ptr */ long oob = run_decap(buf, edtlen, cases[i].bo, cases[i].ds, cases[i].bytes, fixed); const char *verdict; if (oob == -1) verdict = "skip(edtlen<ds_len)"; else if (oob == -2) verdict = "REJECTED by fix"; else if (oob == 0) verdict = "in-bounds (no OOB)"; else verdict = "OOB WRITE"; printf("[%s] mode=%s %-30s bo=0x%lx ds_len=%ld -> %s", cases[i].name, fixed ? "FIXED" : "UNFIXED", cases[i].name, (long)(u_int)cases[i].bo, cases[i].ds, verdict); if (oob > 0) { printf(" oob_bytes=%ld", oob); any_oob = 1; total_oob_bytes += oob; } printf("\n"); } free(base); printf("\nSUMMARY mode=%s total_oob_bytes=%ld exit=%d\n", fixed ? "FIXED" : "UNFIXED", total_oob_bytes, any_oob ? 1 : 0); return any_oob ? 1 : 0; } |