DF-1870 / 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 164 165 166 167 168 169 170 171 172 173 174 175 176 | /* * harness.c -- userspace proof of the OOB read in iscsi_r2t() * (sys/dev/disk/iscsi/initiator/iscsi_subr.c:60-138) * * The kernel allocates csio->data_ptr as a buffer of edtlen bytes * (the SCSI WRITE transfer length). iscsi_r2t() then trusts r2t->ddtl * (attacker-controlled from the wire) as the loop bound and walks bp * forward, shipping `bs`-sized chunks of kernel memory to the malicious * iSCSI target via isc_qout(). * * This harness reproduces that loop verbatim, modelling: * - csio->data_ptr as a heap allocation of size edtlen (filled with a * recognisable pattern) * - the adjacent kernel heap as 'OTHERHEAP' bytes (so we can see what * gets leaked) * - isc_qout() as a sink that just records what the attacker would see * * Build: cc -O2 -Wall -o harness harness.c * Run: ./harness * * Expected output (bug present): * edtlen=512 ddtl=1048576 -> leak=1047552 bytes past buffer end * [OOB READ CONFIRMED] 1st leaked byte past buffer = 0xDE (sent to attacker) * ... (printed only the first few leaked bytes from each Data-Out PDU) * * Expected output (after fix): * [REJECTED by bounds check] bo=0 ddtl=1048576 edtl=512 * no leak */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <assert.h> #define OTHERHEAP 0x100000 /* pretend adjacent kernel heap region */ /* model of isc_qout's network sink: what the attacker target receives */ static size_t g_leaked_bytes_total = 0; static int g_first_oob_byte = -1; static int g_oob_observed = 0; static void isc_qout_sink(const void *ds, size_t len, const void *buf_start, size_t buf_len) { const unsigned char *p = ds; const unsigned char *end = p + len; const unsigned char *bufend = (const unsigned char *)buf_start + buf_len; for (; p < end; p++) { if (p >= bufend) { /* this byte is past the legitimate buffer */ if (!g_oob_observed) { g_first_oob_byte = *p; g_oob_observed = 1; } g_leaked_bytes_total++; } } } /* * Verbatim port of iscsi_r2t() (iscsi_subr.c:60-138), with the kernel * plumbing (pdu_alloc, sessions, mbufs) elided. The control flow and * the *missing* bounds check are preserved exactly. * * Returns 0 normally; with the fix, returns -1 when the bounds check * rejects the R2T. */ static int iscsi_r2t_MODEL(unsigned char *csio_data_ptr, /* edtlen-sized buf */ size_t edtlen, /* sizeof csio->data_ptr */ uint32_t bo_wire, /* r2t->bo, attacker */ uint32_t ddtl_wire, /* r2t->ddtl, attacker */ size_t maxXmitDS, /* sp->opt.maxXmitDataSegmentLength */ int apply_fix) { unsigned char *bp = csio_data_ptr; /* L82 */ uint32_t bo = bo_wire; /* L84 */ uint32_t ddtl = ddtl_wire; /* L79 */ uint32_t edtl = (uint32_t)edtlen; uint32_t bleft, bs, dsn; /* THE FIX -- not present upstream */ if (apply_fix) { if (bo > edtl || ddtl > edtl - bo) { fprintf(stderr, "[REJECTED by bounds check] bo=%u ddtl=%u edtl=%u\n", bo, ddtl, edtl); return -1; } } bleft = ddtl; /* L85 */ if (maxXmitDS > 0) bs = (maxXmitDS < ddtl) ? maxXmitDS : ddtl; /* L88 */ else bs = ddtl; /* L90 */ dsn = 0; while (bleft > 0) { /* L94 */ uint32_t this_bs = (bs < bleft) ? bs : bleft; /* L116/L117 */ /* wpq->pdu.ds = bp; isc_qout(sp, wpq); -- L120/L122 */ isc_qout_sink(bp, this_bs, csio_data_ptr, edtlen); bo += this_bs; /* L126 */ bp += this_bs; /* L127 */ bleft -= this_bs; /* L128 */ dsn++; } return 0; } int main(int argc, char **argv) { size_t edtlen = 512; /* e.g. SCSI WRITE of one 512B sector */ uint32_t ddtl = 0x100000; /* attacker: 1 MiB */ uint32_t bo = 0; size_t maxXmitDS = 64 * 1024; /* typical default */ if (argc > 1) edtlen = (size_t)strtoul(argv[1], 0, 0); if (argc > 2) ddtl = (uint32_t)strtoul(argv[2], 0, 0); if (argc > 3) maxXmitDS= (size_t)strtoul(argv[3], 0, 0); /* Allocate a buffer that is edtlen bytes (the legitimate CCB data * buffer) PLUS a tail of "adjacent kernel heap" so the OOB walk has * something readable to model the leak into. */ unsigned char *heap = calloc(1, edtlen + OTHERHEAP); assert(heap); /* fill legitimate buffer with 0xAA; fill adjacent "heap" with 0xDE * to model recognizable kernel memory that should NOT be sent. */ memset(heap, 0xAA, edtlen); memset(heap + edtlen, 0xDE, OTHERHEAP); /* ---- BUG PRESENT (no fix) ---- */ fprintf(stderr, "=== iscsi_r2t() WITHOUT fix ===\n"); fprintf(stderr, "edtlen=%zu ddtl=%u maxXmitDS=%zu\n", edtlen, ddtl, maxXmitDS); iscsi_r2t_MODEL(heap, edtlen, bo, ddtl, maxXmitDS, /*apply_fix=*/0); if (g_oob_observed) { size_t leaked = g_leaked_bytes_total; fprintf(stderr, "[OOB READ CONFIRMED] attacker received %zu bytes " "PAST the %zu-byte CCB buffer\n", leaked, edtlen); fprintf(stderr, "first leaked byte past buffer = 0x%02X (was 0xDE in our " "model of adjacent kernel heap)\n", g_first_oob_byte); if (leaked > 0) { fprintf(stderr, "VERDICT: iscsi_r2t() walks csio->data_ptr past " "its allocation -> kernel heap info leak.\n"); } } else { fprintf(stderr, "no OOB (unexpected for bug-present case)\n"); } /* ---- BUG FIXED (bounds check) ---- */ g_leaked_bytes_total = 0; g_oob_observed = 0; g_first_oob_byte = -1; fprintf(stderr, "\n=== iscsi_r2t() WITH proposed fix ===\n"); iscsi_r2t_MODEL(heap, edtlen, bo, ddtl, maxXmitDS, /*apply_fix=*/1); if (!g_oob_observed) { fprintf(stderr, "VERDICT: bounds check rejects the over-long R2T; " "0 bytes leaked past buffer.\n"); } free(heap); return g_oob_observed ? 0 : 1; } |