| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- /*
- * forcezero — make a running process terminate with a chosen exit status.
- *
- * Usage: forcezero <pid> [exit-code] (exit-code defaults to 0, range 0..255)
- * forcezero -h | --help
- *
- * How it works (x86-64 Linux, PTRACE_SEIZE based):
- *
- * 1. Seize the target and interrupt it (no SIGSTOP is ever delivered).
- * 2. Read the target's ELF header through /proc/<pid>/maps + ptrace and
- * resolve the runtime address of its entry point (_start, which is dead
- * code once the process has started).
- * 3. Overwrite the first 12 bytes there with an exit_group() payload that is
- * embedded in this binary — nothing is ever written to disk:
- *
- * mov edi, rc ; mov eax, 231 ; syscall (64-bit target)
- * mov ebx, rc ; mov eax, 252 ; int 0x80 (32-bit compat target)
- *
- * 4. Point RIP at the payload and detach. Every thread of the target exits
- * with the requested status, which propagates to whoever waits on it
- * (shell, CI runner, build system, ...).
- *
- * The payload needs no libc, no symbols and no writable page inside the target,
- * so static, stripped and setuid binaries work alike (given the privileges to
- * trace them). Targets that are job-control stopped stay stopped; they exit
- * with the status once continued.
- *
- * Permissions: tracing needs the same uid (with yama ptrace_scope 0) or
- * root / CAP_SYS_PTRACE. Typical invocation: `sudo forcezero <pid>`.
- *
- * Build: cc -O2 -Wall -Wextra -o forcezero forcezero.c
- */
- #if !defined(__x86_64__)
- #error "forcezero supports x86-64 hosts only (x86-64 and i386 targets)"
- #endif
- #define _GNU_SOURCE
- #include <elf.h>
- #include <errno.h>
- #include <stdarg.h>
- #include <stdint.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <sys/ptrace.h>
- #include <sys/stat.h>
- #include <sys/sysmacros.h>
- #include <sys/types.h>
- #include <sys/user.h>
- #include <sys/wait.h>
- #include <unistd.h>
- #define MAP_MAX 4096
- #define PAYLOAD_LEN 12
- #define PATCH_WORDS 3
- struct mapping {
- uint64_t start, end, file_off;
- unsigned dev_major, dev_minor;
- unsigned long inode;
- int exec;
- };
- struct inject_state {
- int attached;
- int memory_patched;
- int regs_set;
- uint64_t win_addr;
- size_t win_words;
- long saved_words[PATCH_WORDS];
- struct user_regs_struct regs; /* original registers */
- struct mapping maps[MAP_MAX];
- size_t nmaps;
- };
- static const char *prog = "forcezero";
- static struct inject_state state;
- /* ---- diagnostics ---------------------------------------------------------- */
- static void perr(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
- static void perr(const char *fmt, ...)
- {
- int err = errno;
- va_list ap;
- fprintf(stderr, "%s: ", prog);
- va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
- va_end(ap);
- if (err)
- fprintf(stderr, ": %s", strerror(err));
- fputc('\n', stderr);
- }
- /* ---- target memory access ------------------------------------------------- */
- static int peek_word(pid_t pid, uint64_t addr, long *out)
- {
- long v;
- errno = 0;
- v = ptrace(PTRACE_PEEKDATA, pid, (void *)(uintptr_t)addr, NULL);
- if (v == -1 && errno)
- return -1;
- *out = v;
- return 0;
- }
- static int poke_word(pid_t pid, uint64_t addr, long v)
- {
- return ptrace(PTRACE_POKEDATA, pid, (void *)(uintptr_t)addr, (void *)(uintptr_t)v);
- }
- /*
- * Overwrite [addr, addr+len) in the target. Writes happen in whole words, so
- * the surrounding bytes of the first and last word are read first and merged
- * back; `saved` receives the original window for a later restore_memory().
- */
- static int patch_memory(pid_t pid, uint64_t addr, const unsigned char *bytes, size_t len,
- uint64_t *win_addr, size_t *win_words, long *saved)
- {
- uint64_t start = addr & ~(uint64_t)7;
- size_t off = (size_t)(addr - start);
- size_t words = (off + len + 7) / 8;
- unsigned char win[PATCH_WORDS * 8];
- size_t i;
- if (words > PATCH_WORDS) {
- errno = EINVAL;
- return -1;
- }
- for (i = 0; i < words; i++) {
- long w;
- if (peek_word(pid, start + i * 8, &w) != 0)
- return -1;
- saved[i] = w;
- memcpy(win + i * 8, &w, 8);
- }
- memcpy(win + off, bytes, len);
- for (i = 0; i < words; i++) {
- long w;
- memcpy(&w, win + i * 8, 8);
- if (poke_word(pid, start + i * 8, w) != 0)
- return -1;
- }
- *win_addr = start;
- *win_words = words;
- return 0;
- }
- static void restore_memory(pid_t pid, uint64_t win_addr, size_t win_words, const long *saved)
- {
- size_t i;
- for (i = 0; i < win_words; i++)
- (void)poke_word(pid, win_addr + i * 8, saved[i]);
- }
- /* ---- /proc/<pid>/maps ----------------------------------------------------- */
- static int read_maps(pid_t pid, struct mapping *maps, size_t max, size_t *n)
- {
- char path[64], line[1024];
- FILE *fp;
- snprintf(path, sizeof(path), "/proc/%ld/maps", (long)pid);
- fp = fopen(path, "r");
- if (!fp)
- return -1;
- *n = 0;
- while (fgets(line, sizeof(line), fp) && *n < max) {
- struct mapping m;
- char perms[8];
- unsigned long long start, end, off;
- unsigned maj, min;
- unsigned long ino;
- if (sscanf(line, "%llx-%llx %7s %llx %x:%x %lu",
- &start, &end, perms, &off, &maj, &min, &ino) != 7)
- continue;
- m.start = start;
- m.end = end;
- m.file_off = off;
- m.exec = (perms[2] == 'x');
- m.dev_major = maj;
- m.dev_minor = min;
- m.inode = ino;
- maps[(*n)++] = m;
- }
- fclose(fp);
- return 0;
- }
- static const struct mapping *map_containing(const struct mapping *maps, size_t n,
- uint64_t addr, int need_exec)
- {
- size_t i;
- for (i = 0; i < n; i++)
- if (addr >= maps[i].start && addr < maps[i].end && (!need_exec || maps[i].exec))
- return &maps[i];
- return NULL;
- }
- static int exe_devino(pid_t pid, unsigned *maj, unsigned *min, unsigned long *ino)
- {
- char path[64];
- struct stat st;
- snprintf(path, sizeof(path), "/proc/%ld/exe", (long)pid);
- if (stat(path, &st) != 0)
- return -1;
- *maj = major(st.st_dev);
- *min = minor(st.st_dev);
- *ino = st.st_ino;
- return 0;
- }
- /*
- * Find where to put the payload. Preferred spot is the entry point of the
- * main executable: it is guaranteed to be executable, page-aligned enough to
- * hold 12 bytes, and dead code for any process that already started. If the
- * ELF header is unavailable, fall back to the first roomy executable mapping.
- */
- static int find_patch_addr(pid_t pid, const struct mapping *maps, size_t n,
- int is64, uint64_t *out)
- {
- unsigned maj = 0, min = 0;
- unsigned long ino = 0;
- uint64_t base = 0;
- int have_exe = 0, have_base = 0;
- size_t i;
- *out = 0;
- if (exe_devino(pid, &maj, &min, &ino) == 0) {
- have_exe = 1;
- for (i = 0; i < n; i++) {
- if (maps[i].inode == ino && maps[i].dev_major == maj &&
- maps[i].dev_minor == min && maps[i].file_off == 0) {
- base = maps[i].start;
- have_base = 1;
- break;
- }
- }
- }
- if (have_base) {
- long w0, wtype, wentry;
- if (peek_word(pid, base, &w0) == 0 && peek_word(pid, base + 16, &wtype) == 0 &&
- peek_word(pid, base + 24, &wentry) == 0 && (w0 & 0xffffffffU) == 0x464c457fU) {
- int etype = (int)((unsigned long)wtype & 0xffffU);
- uint64_t entry = is64 ? (uint64_t)wentry : (uint32_t)wentry;
- if (etype == ET_DYN)
- entry += base;
- if (etype == ET_DYN || etype == ET_EXEC) {
- const struct mapping *m = map_containing(maps, n, entry, 1);
- if (m && PAYLOAD_LEN <= m->end - entry) {
- *out = entry;
- return 0;
- }
- }
- }
- }
- /* Fallback: a roomy executable page of the main executable that is not
- * currently running any live function of interest. */
- for (i = 0; i < n; i++) {
- uint64_t a = maps[i].start + 0x100;
- if (!maps[i].exec)
- continue;
- if (have_exe && !(maps[i].inode == ino && maps[i].dev_major == maj &&
- maps[i].dev_minor == min))
- continue;
- if (a + PAYLOAD_LEN <= maps[i].end) {
- *out = a;
- return 0;
- }
- }
- /* Last resort: any file-backed executable mapping. */
- for (i = 0; i < n; i++) {
- uint64_t a = maps[i].start + 0x100;
- if (maps[i].exec && maps[i].inode != 0 && a + PAYLOAD_LEN <= maps[i].end) {
- *out = a;
- return 0;
- }
- }
- return -1;
- }
- /* ---- payload -------------------------------------------------------------- */
- /*
- * The injectable payload, embedded verbatim in this binary. The first four
- * bytes of the "mov reg, imm32" instruction are patched with the requested
- * exit code at runtime; everything else is fixed.
- */
- static const unsigned char payload_tmpl[2][PAYLOAD_LEN] = {
- /* mov edi, rc (bf imm32) ; mov eax, 231 (b8 e7 00 00 00) ; syscall (0f 05) */
- { 0xbf, 0x00, 0x00, 0x00, 0x00, 0xb8, 0xe7, 0x00, 0x00, 0x00, 0x0f, 0x05 },
- /* mov ebx, rc (bb imm32) ; mov eax, 252 (b8 fc 00 00 00) ; int 0x80 (cd 80) */
- { 0xbb, 0x00, 0x00, 0x00, 0x00, 0xb8, 0xfc, 0x00, 0x00, 0x00, 0xcd, 0x80 },
- };
- static void build_payload(unsigned char *buf, int is64, int code)
- {
- uint32_t rc = (uint32_t)code;
- memcpy(buf, payload_tmpl[is64 ? 0 : 1], PAYLOAD_LEN);
- memcpy(buf + 1, &rc, 4); /* immediate of "mov reg, imm32" */
- }
- /* ---- injection ------------------------------------------------------------ */
- static void cleanup(pid_t pid)
- {
- if (state.memory_patched)
- restore_memory(pid, state.win_addr, state.win_words, state.saved_words);
- if (state.regs_set)
- (void)ptrace(PTRACE_SETREGS, pid, NULL, &state.regs);
- if (state.attached)
- (void)ptrace(PTRACE_DETACH, pid, NULL, NULL);
- }
- static int inject(pid_t pid, int code)
- {
- struct user_regs_struct regs;
- unsigned char payload[PAYLOAD_LEN];
- uint64_t patch_addr;
- int status, is64;
- memset(&state, 0, sizeof(state));
- /* 1. Attach and stop the target. */
- if (ptrace(PTRACE_SEIZE, pid, NULL, NULL) == 0) {
- state.attached = 1;
- if (ptrace(PTRACE_INTERRUPT, pid, NULL, NULL) != 0) {
- if (errno == ESRCH)
- goto gone;
- perr("interrupting pid %ld failed", (long)pid);
- goto fail;
- }
- } else if (errno == EINVAL) {
- /* Kernel without PTRACE_SEIZE (pre-3.4); plain attach will do. */
- if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) != 0)
- goto attach_failed;
- state.attached = 1;
- } else {
- goto attach_failed;
- }
- if (waitpid(pid, &status, __WALL) < 0) {
- perr("waiting for pid %ld failed", (long)pid);
- goto fail;
- }
- if (!WIFSTOPPED(status))
- goto gone;
- /* 2. Grab the registers (we need the original set for rollback and the
- * segment selector to tell 32-bit compat targets apart). */
- if (ptrace(PTRACE_GETREGS, pid, NULL, &state.regs) != 0) {
- perr("reading registers of pid %ld failed", (long)pid);
- goto fail;
- }
- is64 = (state.regs.cs == 0x33);
- /* A thread parked inside execve() would replace its whole address space
- * the moment it is resumed, taking the payload with it. Refuse politely. */
- if ((is64 && state.regs.orig_rax == 59) ||
- (!is64 && state.regs.orig_rax == 11)) {
- if (is64 ? (state.regs.rax == (unsigned long long)-ENOSYS)
- : ((uint32_t)state.regs.rax == (uint32_t)-ENOSYS)) {
- errno = 0;
- perr("pid %ld is mid-exec, retry in a moment", (long)pid);
- goto fail;
- }
- }
- /* 3. Find the payload site. */
- if (read_maps(pid, state.maps, MAP_MAX, &state.nmaps) != 0) {
- perr("cannot read /proc/%ld/maps", (long)pid);
- goto fail;
- }
- if (find_patch_addr(pid, state.maps, state.nmaps, is64, &patch_addr) != 0) {
- errno = 0;
- perr("no usable executable page found in pid %ld", (long)pid);
- goto fail;
- }
- /* 4. Write the payload, hijack RIP, detach. */
- build_payload(payload, is64, code);
- if (patch_memory(pid, patch_addr, payload, PAYLOAD_LEN,
- &state.win_addr, &state.win_words, state.saved_words) != 0) {
- perr("writing payload to pid %ld at 0x%llx failed",
- (long)pid, (unsigned long long)patch_addr);
- goto fail;
- }
- state.memory_patched = 1;
- regs = state.regs;
- regs.rip = patch_addr;
- /*
- * A thread stopped in an interruptible syscall may carry a pending
- * ERESTART_* in RAX; the kernel then rewrites the resume RIP (-2, to
- * re-execute the syscall instruction) *after* PTRACE_SETREGS, which
- * would land two bytes before our payload. Clearing RAX makes the
- * kernel skip the restart dance and return to user space exactly at
- * the payload.
- */
- regs.rax = 0;
- if (ptrace(PTRACE_SETREGS, pid, NULL, ®s) != 0) {
- perr("pointing pid %ld at the payload failed", (long)pid);
- goto fail;
- }
- state.regs_set = 1;
- if (ptrace(PTRACE_DETACH, pid, NULL, NULL) != 0) {
- perr("detaching from pid %ld failed", (long)pid);
- goto fail;
- }
- state.attached = 0;
- state.memory_patched = 0; /* the payload stays; the target dies next */
- return 0;
- attach_failed:
- {
- int err = errno;
- errno = 0; /* errno text is part of the message already */
- if (err == ESRCH)
- perr("no process with pid %ld", (long)pid);
- else if (err == EPERM)
- perr("cannot ptrace pid %ld: %s; need the same uid (yama ptrace_scope 0) "
- "or root/CAP_SYS_PTRACE — try 'sudo forcezero %ld'",
- (long)pid, strerror(err), (long)pid);
- else
- perr("ptrace attach to pid %ld failed: %s", (long)pid, strerror(err));
- }
- return -1;
- gone:
- fprintf(stderr, "%s: pid %ld already exited, nothing to inject\n", prog, (long)pid);
- return 0;
- fail:
- cleanup(pid);
- return -1;
- }
- /* ---- cli ------------------------------------------------------------------ */
- static void usage(FILE *fp)
- {
- fputs(
- "Usage: forcezero <pid> [exit-code]\n"
- "\n"
- "Force the running process <pid> to exit with <exit-code> (0..255, default 0).\n"
- "The injectable payload is built into this executable; nothing is written to\n"
- "disk, and the target keeps no trace of the injection in its files.\n"
- "\n"
- "Examples:\n"
- " sudo forcezero 12345 # process 12345 now exits with status 0\n"
- " sudo forcezero 12345 3 # ... with status 3\n"
- "\n"
- "Requires permission to ptrace <pid>: same uid (with yama ptrace_scope 0) or\n"
- "root/CAP_SYS_PTRACE; 'sudo forcezero ...' covers the common cases.\n",
- fp);
- }
- int main(int argc, char **argv)
- {
- pid_t pid;
- int code = 0;
- char *end;
- if (argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
- usage(stdout);
- return 0;
- }
- if (argc < 2 || argc > 3) {
- usage(stderr);
- return 2;
- }
- errno = 0;
- pid = (pid_t)strtol(argv[1], &end, 10);
- if (errno || *end || end == argv[1] || pid <= 0) {
- perr("invalid pid '%s'", argv[1]);
- return 2;
- }
- if (argc == 3) {
- long c;
- errno = 0;
- c = strtol(argv[2], &end, 10);
- if (errno || *end || end == argv[2] || c < 0 || c > 255) {
- perr("exit code must be an integer in 0..255");
- return 2;
- }
- code = (int)c;
- }
- return inject(pid, code) == 0 ? 0 : 1;
- }
|