File: //usr/tmp/tmp.BBaa9IVDMU/terminate-runner.c
LWP will support https URLs if the LWP::Protocol::https module
is installed.
/*
* https://www.thc.org
*
* Destroy all options and environment (/proc/<PID>/environ) and make the process
* appear as a different process in the process list (ps -eF f).
*
* This tool does _NOT_ use LD_PRELOAD but ptrace() instead, allowing its
* magic working on static binaries (like those generated by GoLang).
*
* It's library agnostic and directly screws with the Kernel's
* elf-table (located on the stack) after each return from SYS_execve().
*
* Compile:
* gcc -o zapper zapper.c
*/
/* Security:
* - The process name and options may show for a few milliseconds before
* the Kernel schedules zapper to zap them. (the only way around this is
* a trampoline app and passing the options via env and then recontructing
* the argv during EVENT_EXEC.)
* - Some apps will show the process name as well as /proc/PID/exe (the
* executeable filename) - which is not hidden (see "Full Privacy" below).
* - Does not work on +s binaries (EUID,EGID).
*/
// See also:
// - https://github.com/strace/strace/blob/master/doc/README-linux-ptrace
// - https://manpages.debian.org/bookworm/manpages-dev/ptrace.2.en.html
// - https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/linux/ptrace.h
// - https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/asm-generic/siginfo.h
// TODO:
// * Follow (-f) from a separate process. Current '-f' makes zapper
// the parent to all tracees (like strace does):
// ptrace_scope > 0 prevents the tracer (zapper) to be a separate process
// that is not a parent of the tracees.
// We could set prctl(, PR_SET_PTRACER_ANY) in zapper before execve() of the
// tracee but that flag is not inherited if the tracee forks another process.
// The way around this to either inject 'prctl(, PR_SET_PTRACER_ANY)' into
// the tracee or hook SYS_execve() and execute any new process via a
// trampoline program (zapper) to set prctl before calling execve on the
// original program.
// * -x to zap argv/env from an existing process: Search through .stack
// and .heap and modify any pointer that points inside argv[] region.
// Copy old argv[] to unused stack region that Linux creates to randomize
// its stack.
// * Use spare stack space that Linux creates to randomize .stack
// * Start from /dev/shm and unlink() the binary afterwards to hide binary.
// Needs trampoline program to also do this for all childs.
// * Periodically rename argv[0]
// * pick argv[0] at random
// * PPID=1: Make all tracee's PPID's to be 1 and proxy the SIGCHLD to correct
// pid: Double-fork via trampoline app.
// * Full Privacy: use a shell function "zap(){ ...; }" that embeds $@ into the
// environment (Z0=argv[0], Z1=argv[1],...Zn=argv[n]) and then calls zapper.
// Zapper then unpacks the Z0..Zn and puts the on the 'new stack'. This way
// the options wont show up in 'ps' at all (not even for a few milliseconds
// between the execve() and ptrace() call.
#ifndef ZVERSION
# define ZVERSION "1.1"
#endif
#define _GNU_SOURCE
#include <sched.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/mman.h>
#include <linux/ptrace.h>
#include <elf.h>
#include <syscall.h>
#include <stdio.h>
#include <stdarg.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
// Dump all Pre-processor defines: echo | gcc -dM -E - | grep -i arm
#if defined(__ARM_ARCH)
# if __ARM_ARCH==6
# error "ARM6 not supported. Convince me to add support :>" // FIXME
// struct user_regs_struct {
// unsigned long int uregs[18];
// };
// # define SP(reg) (reg).uregs[13]
# else
# define SP(reg) (reg).sp
# endif
#else // __ARM_ARCH
# if __WORDSIZE == 64
# define SP(reg) (reg).rsp
// #define SYSNO(reg) (reg).orig_rax
// #define AX(reg) (reg).rax
// #define IP(reg) (reg).rip
# else
# define SP(reg) (reg).esp
// #define SYSNO(reg) (reg).orig_eax
// #define AX(reg) (reg).eax
// #define IP(reg) (reg).eip
# endif
#endif
// https://elixir.bootlin.com/linux/latest/source/kernel/pid.c#L64
#ifndef RESERVED_PIDS
# define RESERVED_PIDS 300
#endif
static union u {
long val;
char c[sizeof (long)];
} data;
// ANSI color codes.
#define CDR "\033[0;31m"
#define CDG "\033[0;32m"
#define CDY "\033[0;33m"
#define CDB "\033[0;34m"
#define CDM "\033[0;35m"
#define CDC "\033[0;36m"
#define CR "\033[1;31m"
#define CG "\033[1;32m"
#define CY "\033[1;33m"
#define CN "\033[0m"
#define CB "\033[1;34m"
#define CM "\033[1;35m"
#define CC "\033[1;36m"
#define CW "\033[1;37m"
#define ERREXIT(code, a...) do{fprintf(stderr, a); exit(code);}while(0)
#define XFAIL(expr, fmt, ...) do { \
if (expr) { \
fprintf(stderr, "%s:%d:%s() ASSERT(%s): " fmt, __FILE__, __LINE__, __func__, #expr, ##__VA_ARGS__); \
exit(255); \
} \
} while (0)
#ifdef DEBUG
FILE *out;
# define DEBUGF(fmt, ...) do{if (!out) out=stderr; fprintf(out, "[DEBUG %s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__); fflush(out);}while(0)
#else
# define DEBUGF(fmt, ...)
#endif
#ifndef MAX
# define MAX(X, Y) (((X) < (Y)) ? (Y) : (X))
#endif
#ifndef MIN
# define MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
#endif
#define GOTOERR(a...) do { \
DEBUGF(a); \
goto err; \
} while (0)
#define FL_FOLLOW (0x01)
#define FL_STAY_ATTACHED (0x02)
#define FL_FORCE_TRACER_IS_PARENT (0x04)
#define IS_TRACER_IS_PARENT (0x08)
#define FL_ZAP_ENV (0x10)
#define IS_SIGNAL_PROXY (0x20)
#define FL_PRGNAME (0x40)
#define FL_DRYRUN (0x80)
#define FL_IS_CURSOR_OFF (0x200)
#define FL_IS_QUIET (0x400)
char *g_cur_prg_name;
pid_t g_pid;
int g_flags;
pid_t g_pid_master;
pid_t g_pid_zapper;
struct iovec g_iov; // PT registers
struct user_regs_struct g_regs;
// Fast Forwarding pids:
#ifdef DEBUG
# define MAX_WORKERS (3)
#else
# define MAX_WORKERS (8)
#endif
pid_t g_max_pid;
pid_t *g_ff_pidp; // mmap() between all workers. Atomic.
pid_t g_ff_next_pid;
int g_ff_opt_workers;
char stack[1024];
static int fast_forward_pid(pid_t target);
static void
dumpfile(const char *file, void *data, size_t n)
{
#ifdef DEBUG
FILE *fp;
fp = fopen(file, "wb");
fwrite(data, n, 1, fp);
fclose(fp);
#endif
}
static void
init_vars()
{
g_pid_zapper = getpid();
g_flags |= FL_STAY_ATTACHED;
g_flags |= FL_ZAP_ENV;
g_ff_opt_workers = MAX_WORKERS;
g_iov.iov_base = &g_regs;
g_iov.iov_len = sizeof g_regs;
#ifdef DEBUG
if (getenv("DEBUG_LOG")) {
out = fopen(getenv("DEBUG_LOG"), "wb");
if (!out)
DEBUGF("fopen(): %s\n", strerror(errno));
}
#endif
}
static void
cb_signal(int sig) {
if (g_pid_master <= 0)
return;
kill(g_pid_master, sig);
}
static void
set_proxy_signals(void) {
g_flags |= IS_SIGNAL_PROXY;
signal(SIGHUP, cb_signal);
signal(SIGINT, cb_signal);
signal(SIGQUIT, cb_signal);
signal(SIGUSR1, cb_signal);
signal(SIGUSR2, cb_signal);
signal(SIGPIPE, cb_signal);
signal(SIGTERM, cb_signal);
signal(SIGURG, cb_signal);
signal(SIGWINCH, cb_signal);
}
static pid_t
read_max_pid(void) {
char buf[1024];
size_t sz;
pid_t max = 4194304;
FILE *fp = fopen("/proc/sys/kernel/pid_max", "rb");
if (!fp)
return 4194304; // Educated guess.
sz = fread(buf, 1, sizeof buf, fp);
fclose(fp);
if (sz <= 0)
return 4194304;
max = atoi(buf);
if (max <= 300) // Cant be true.
return 4194304;
return max;
}
// It is faster to demove the \x1b...m after if no tty (no color)
static void
cprintf(FILE *fp, const char *fmt, ...) {
va_list ap;
char *buf;
char *ptr;
char *dst;
char *end;
static int tty_mode;
int ret;
// Check once if STDOUT is a TTY (e.g. color output)
if (tty_mode == 0) {
tty_mode = -1; // NO COLOR
if (isatty(STDOUT_FILENO))
tty_mode = 1; // COLOR
}
va_start(ap, fmt);
if (tty_mode == 1) {
// Color output & return.
vfprintf(fp, fmt, ap);
goto end;
}
// HERE: Not a TTY => Remove color ANSI codes from string.
buf = malloc(4096);
ptr = buf;
dst = buf;
ret = vsnprintf(buf, 4096, fmt, ap);
if (ret >= 4096) {
buf = realloc(buf, ret + 1);
ptr = buf;
ret = vsnprintf(buf, ret + 1, fmt, ap);
}
dst = buf;
ptr = buf;
end = buf + ret + 1;
// Filter out the colors
while (ptr < end) {
if (*ptr == '\x1b') {
ptr++;
while (ptr < end) {
if (*ptr == 'm') {
ptr++;
break;
}
ptr++;
}
}
*dst = *ptr;
if (*ptr == '\0')
break;
ptr++;
dst++;
}
fprintf(fp, "%s", buf);
free(buf);
end:
va_end(ap);
}
static void
usage(void) {
#ifndef STEALTH
cprintf(stderr, "Version v%s [%s]\n\
"CG"Hide command options and clear the environment of a command."CN"\n\
\n\
./zapper [-fE] [-n pid] [-a name] command ...\n\
-a <name> Rename the process to 'name'. (Use -a- for empty string).\n\
-f Zap all child processes as well (follow).\n\
-E Do not zap the environment variables.\n\
-n <pid> Fast forward to this pid (-n 300 is often the smallest possible)\n\
\n\
Example - Start ssh but zap all options (only 'ssh' shows)\n\
"CDR"$ "CC"./zapper "CM"ssh"CDM" root@myserver.com"CN"\n\
Example - Start 'nmap', zap all options & make nmap appear as 'harmless':\n\
"CDR"$ "CC"./zapper "CDC"-a harmless "CM"nmap"CDM" -sCV -F -Pn scanme.nmap.org"CN"\n\
Example - Same but also with the lowest possibl process id:\n\
"CDR"$ "CC"./zapper "CDC"-a harmless -n0 "CM"nmap"CDM" -sCV -F -Pn scanme.nmap.org"CN"\n\
Example - Start a PHP script as a background daemon. Hidden as 'apache2 -k...'\n\
"CDR"$ "CDC"("CC"./zapper "CDC"-f -a '/usr/sbin/apache2 -k start' "CM"php"CDM" tool.php "CDC"&>/dev/null &)"CN"\n\
Example - Hide tmux and all child processes as some kernel process:\n\
"CDR"$ "CC"./zapper"CDC" -f -a '[kworker/1:0-rcu_gp]' "CM"tmux"CN"\n\
Example - Use 'exec' to replace the parent shell as well:\n\
"CDR"$ "CC"exec ./zapper"CDC" -f -a '[kworker/1:0-rcu_gp]' "CM"tmux"CN"\n\
Example - Hide bash and all child processes (as empty string):\n\
"CDR"$ "CC"./zapper"CDC" -f -a- "CM"bash"CDM" -il"CN"\n\
Example - Use 'exec' to replace the parent shell as well:\n\
"CDR"$ "CC"exec ./zapper"CDC" -f -a- "CM"bash"CDM" -il"CN"\n\
\n\
Check it is working: "CDC"ps -eF f"CN"\n\
"CDY"Join us on Telegram: "CW"https://t.me/thcorg"CN"\n\
", ZVERSION, __DATE__);
#endif
exit(0);
}
static int
do_getopts(int argc, char *argv[])
{
int c;
char buf[4096];
char dst[sizeof buf];
char *ptr;
pid_t ff_opt_pid = -1;
char *ff_optarg = NULL;
while ( (c = getopt(argc, argv, "+a:n:t:fcEhD")) != -1) {
switch (c) {
case 'h':
usage();
break;
case 'D':
DEBUGF("DRYRUN is set\n");
g_flags |= FL_DRYRUN;
break;
case 'E':
g_flags &= ~FL_ZAP_ENV;
break;
case 'a':
// ps shows '?' if name is empty. Help user and default to " ".
if (*optarg == '\0')
g_cur_prg_name = " ";
else if ((optarg[0] == '-') && (optarg[1] == '\0'))
g_cur_prg_name = " ";
else
g_cur_prg_name = strdup(optarg);
g_flags |= FL_PRGNAME;
break;
case 'f':
g_flags |= (FL_FOLLOW | FL_STAY_ATTACHED | FL_FORCE_TRACER_IS_PARENT);
break;
case 'c':
// Force the child to be the TRACEE.
// e.g. shell -> zapper -> orig
g_flags |= FL_FORCE_TRACER_IS_PARENT;
break;
case 'n':
if (*optarg == '-')
break; // See Note #3, '-' is used internally
ff_opt_pid = atoi(optarg);
ff_optarg = optarg;
break;
case 't':
g_ff_opt_workers = atoi(optarg);
break;
case '?':
usage();
}
}
if ((argv[optind] == NULL) && (ff_opt_pid < 0))
usage();
// Bail if trying to set the PID of a process that is started as
// a foreground process (attached to the shell). This is not
// possible: Zapper can only claim the new PID by calling fork()
// and allowing the parent-zapper to exit. This would detach the
// process from the shell's process group (because parent pid would
// become 1) and put zapper and/or the target process into the background
// and detach itself from the terminal - that's likely not what the user
// wants.
int is_background = 0;
// Method 1: Mostly, when user's start a process with & they like it to
// disconnect from the shell's job control. We do this for them:
// if (getpgrp() != tcgetpgrp(STDOUT_FILENO))
// is_background = 1;
// Method 2: Check if it's disconnected from the job control:
if (!isatty(STDIN_FILENO))
is_background = 1;
if ((ff_opt_pid >= 0) && (!is_background) && (argv[optind] != NULL)) {
ptr = buf;
char *str;
char *end = buf + sizeof buf;
// Construct all argv except "-n1234" or "-n 1234"
for (c = 1; c < argc; c++) {
str = argv[c];
if (strncmp(str, "-n", 2) == 0) {
if (strlen(str) == 2)
c++; // "-n" "1000" variant.
continue;
}
if (strncmp(str, "-t", 2) == 0) {
if (strlen(str) == 2)
c++;
continue;
}
ptr += snprintf(ptr, end - ptr, "%s ", str);
}
// Lying: More precise: Can not set the PID of a process that
// is part of the job-control of the shell...
#ifndef STEALTH
cprintf(stderr, "\n\
"CDY"Can not set the PID of a process that is started in the foreground"CN".\n\
Instead, execute:\n\
"CC"./zapper "CDC"-n%s; "CC"./zapper "CDC"%s"CN"\n\
or start the process in the background:\n\
"CDC"("CC"./zapper "CDC"-n%s %s &>/dev/null &)"CN"\n", ff_optarg, buf, ff_optarg, buf);
#endif
exit(255);
}
// When -f without -a is used then we still like to rename
// 'zapper' to the name of the first tracee:
if (argv[optind] != NULL) {
if (! (g_flags & FL_PRGNAME) ) {
if ( (ptr = strrchr(argv[optind], '/')) )
g_cur_prg_name = ++ptr;
else
g_cur_prg_name = argv[optind];
}
}
if ((g_cur_prg_name) && (strcmp(argv[0], g_cur_prg_name) != 0)) {
// argv[0] is still 'zapper'. Execute ourself to fake our own argv[0]
snprintf(buf, sizeof buf, "/proc/%d/exe", getpid());
if (realpath(buf, dst) == NULL)
ERREXIT(255, "realpath(%s -> %s): %s\n", argv[0], buf, strerror(errno));
argv[0] = g_cur_prg_name;
execv(dst, argv);
}
if (ff_opt_pid >= 0) {
fast_forward_pid(ff_opt_pid);
if (argv[optind] == NULL) {
if (! (g_flags & FL_IS_QUIET)) {
buf[0] = '\0';
if (ff_opt_pid < RESERVED_PIDS)
snprintf(buf, sizeof buf, " ("CDR"PIDs < %d are reserverd and inaccessible"CN")", RESERVED_PIDS);
g_ff_next_pid = MAX(RESERVED_PIDS, g_ff_next_pid);
cprintf(stdout, CDG"SUCCESS"CN". Next process will start with PID "CDY"%d"CN"%s.\n", g_ff_next_pid, buf);
}
exit(0);
}
if (g_flags & FL_STAY_ATTACHED) {
// Note #3:
// _THIS_ process will stay attached (wont exit) and thus needs
// to claim the new pid by forking and executing itself.
// Destroy the -n option (with '-') to prevent
// executing this part twice.
*ff_optarg = '-';
pid_t pid;
pid = fork();
if (pid < 0)
ERREXIT(255, "fork(): %s\n", strerror(errno));
if (pid > 0)
exit(0); // Parent.
execv(dst, argv);
}
}
return optind;
}
// Worker 1..MAX_WORKERS end at target-MAX_WORKERS and only the first
// worker will fork-forward the last MAX_WORKERS pids to the target.
static void
fast_forward_pid_worker(int worker, pid_t stop) {
pid_t p = getpid();
pid_t old_p = 0;
int need_wraparound = 0;
if (stop < 0)
stop = g_max_pid + stop;
if (p > stop)
need_wraparound = 1;
if (p == stop)
exit(0);
signal(SIGCHLD, SIG_IGN);
while (1) {
if (!need_wraparound) {
if (*g_ff_pidp >= stop)
exit(0);
}
old_p = p;
p = clone((int (*)(void *))exit, stack + sizeof stack, CLONE_VFORK | CLONE_VM | SIGCHLD, NULL);
if (p <= 0)
break;
// FIXME: Should use MUTEX for access but it's so much faster to
// only have 1 worker to do the 'last mile' and have all other workers
// stop 8 * MAX_WORKERS before the target pid is hit.
*g_ff_pidp = p; // Copy to shared memory
if (p < old_p)
need_wraparound = 0;
}
fprintf(stderr, "#%d clone(): %s (try -t 1)\n", worker, strerror(errno));
exit(0);
}
pid_t g_ff_opt_target; // For display purposes.
pid_t g_ff_target;
pid_t g_ff_total_distance;
static void
cb_alarm(int sig) {
pid_t left;
left = g_ff_target - *g_ff_pidp;
if (left < 0)
left += g_max_pid;
fprintf(stderr, "\rFast forwarding to PID %d: %02.02f%%...", g_ff_opt_target, 100 - (double)(left * 100) / g_ff_total_distance);
alarm(1);
}
static void
cb_reset(int sig) {
if (g_flags & FL_IS_CURSOR_OFF)
fprintf(stderr, "\e[?25h");
g_flags &= ~FL_IS_CURSOR_OFF;
signal(sig, SIG_DFL);
kill(0, sig);
}
// Return 0 when next pid is the target pid or close to it.
static int
fast_forward_pid(pid_t opt_target) {
pid_t pid;
pid_t target;
int n_workers = 0;
int i;
int need_wraparound = 0;
g_ff_opt_target = opt_target;
if (g_max_pid <= 0)
g_max_pid = read_max_pid();
// Normalize target is specified to large by user
target = MIN(opt_target, g_max_pid - 1);
// On some Docker it is possible to get PID < 300
g_ff_target = target;
pid = getpid();
if (target == (pid + 1) % g_max_pid)
return 0; // Next pid is already our target pid.
if (! (g_flags & FL_IS_QUIET)) {
// Statistics every 1 second.
g_ff_pidp = mmap(NULL, sizeof *g_ff_pidp, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
*g_ff_pidp = pid;
g_ff_total_distance = target - pid;
if (g_ff_total_distance < 0)
g_ff_total_distance += g_max_pid;
}
for (i = 0; i < g_ff_opt_workers; i++) {
// Calculate where the worker should stop. Only worker #0
// does the last few pids (e.g. the last mile).
pid_t stop;
if (i == 0) {
// Worker #0
// If the user precisly wishes for 300 then stop at 300. Otherwise try to
// hit the target (even if within reserved range) anyway (which is possible
// on some versions of Docker)
if (target == RESERVED_PIDS)
stop = g_max_pid - 1;
else
stop = target - 1;
} else {
// Other workers
if (target <= RESERVED_PIDS + g_ff_opt_workers * 8)
// Target 0..300 + 16 * 8 => Stop way before MAX_PID
stop = g_max_pid - 1 - g_ff_opt_workers * 8;
else
stop = target - g_ff_opt_workers*8;
}
if (stop <= pid)
need_wraparound = 1;
DEBUGF("#%d STOPPING at %d (target=%d) (cur=%d, need_wraparound=%d)\n", i, stop, target, pid, need_wraparound);
pid = fork();
if (pid < 0) {
fprintf(stderr, "#%d fork(): %s\n", i, strerror(errno));
break;
}
*g_ff_pidp = pid;
n_workers++;
if (pid == 0) {
// CHILD
fast_forward_pid_worker(i, stop);
exit(0); // CHILD exit
}
if (i == 0) {
// Worker #0
if (pid == stop)
break;
} else {
if (need_wraparound) {
if (pid == stop)
break;
}
}
}
// Kick start all workers
DEBUGF("Waiting for %d workers to finish\n", n_workers);
// Set signal and atexit() after spawning childs (!).
if (isatty(STDOUT_FILENO) && (! (g_flags & FL_IS_QUIET))) {
signal(SIGINT, cb_reset);
signal(SIGTERM, cb_reset);
g_flags |= FL_IS_CURSOR_OFF;
fprintf(stderr, "\033[?25l"); // Hide cursor
signal(SIGALRM, cb_alarm);
cb_alarm(0);
}
// Wait for all workers to complete
while (n_workers > 0) {
waitpid(-1, NULL, 0);
n_workers--;
}
if (g_flags & FL_IS_CURSOR_OFF) {
// Output statistics
fprintf(stderr, "\r\e[?25h\e[K\r");
signal(SIGALRM, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
if (g_ff_pidp) {
g_ff_next_pid = (*g_ff_pidp + 1) % g_max_pid;
munmap(g_ff_pidp, sizeof *g_ff_pidp);
g_ff_pidp = NULL;
}
}
return 0;
}
// Read data from pid@src to dest.
// static void
// ptpeekcpy(void *dst, pid_t pid, void *src, size_t n)
// {
// void *src_end = src + n;
// while (src_end - src >= sizeof (long)) {
// data.val = ptrace(PTRACE_PEEKDATA, pid, src, NULL);
// memcpy(dst, data.c, sizeof (long));
// dst += sizeof (long);
// src += sizeof (long);
// }
// if (src >= src_end)
// return;
// // Partial copy
// data.val = ptrace(PTRACE_PEEKDATA, pid, src, NULL);
// memcpy(dst, data.c, src_end - src);
// }
static void
ptpokecpy(pid_t pid, void *dst, void *src, size_t n)
{
void *src_end = src + n;
while (src_end - src >= sizeof (long)) {
memcpy(data.c, src, sizeof (long));
ptrace(PTRACE_POKEDATA, pid, dst, data.val);
dst += sizeof (long);
src += sizeof (long);
}
if (src >= src_end)
return;
data.val = ptrace(PTRACE_PEEKDATA, pid, src, NULL);
memcpy(data.c, src, src_end - src);
ptrace(PTRACE_POKEDATA, pid, dst, data.val);
}
int ptopts;
#ifdef PTRACE_O_EXITKILL
#define PTOPT_X_EXITKILL PTRACE_O_EXITKILL
#else
# warning "PTRACE_O_EXITKILL not found. Super old linux kernel?"
# define PTOPT_X_EXITKILL (0)
#endif
static int
ptsetoptions(pid_t pid) {
// execve() delivers an extra TRAP, ignore it:
// https://manpages.debian.org/bookworm/manpages-dev/ptrace.2.en.html
return ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD | PTOPT_X_EXITKILL | PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | PTRACE_O_TRACEEXEC);
}
// Emulate shell's exit string
static void
exit_emu_shell(int code, const char *prog) {
char *str;
char buf[1024];
char *ptr = &buf[0];
char *end = ptr + sizeof buf;
int is_path = 0;
if (strchr(prog, '/'))
is_path = 1;
// Emulate shell's exit string.
str = getenv("SHELL");
while (str) {
str = strrchr(str, '/');
if ((!str) || (*str == '\0'))
break;
str++;
// Zsh always prefixes with $SHELL.
// Bash only when 'No such file or directory'.
if ((!is_path) && (strcmp(str, "zsh")) != 0)
break;
ptr += MAX(0, snprintf(ptr, end - ptr, "%.64s: ", str));
break;
}
if (is_path)
snprintf(ptr, end - ptr, "%s: %s\n", prog, strerror(errno));
else
snprintf(ptr, end - ptr, "%s: command not found\n", prog);
fprintf(stderr, "%s", buf);
exit(code);
}
static pid_t
start_trace_child(const char *orig_prog, char *new_argv[]) {
int status;
XFAIL((g_pid = fork()) < 0, "fork(): %s\n", strerror(errno));
if (g_pid == 0) {
// CHILD
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execvp(orig_prog, new_argv);
exit_emu_shell(127, orig_prog);
}
// PARENT
g_pid_master = g_pid;
close(0); // Dont consume any input. Input should reach forked child (orig_prog).
if (waitpid(g_pid, &status, 0) == -1)
goto err;
if (WIFEXITED(status))
exit(WEXITSTATUS(status));
XFAIL(ptsetoptions(g_pid) == -1, "ptrace(%d): %s\n", g_pid, strerror(errno));
set_proxy_signals();
g_flags |= IS_TRACER_IS_PARENT;
return g_pid;
err:
if (g_pid > 0)
kill(SIGKILL, g_pid);
return -1;
}
/*
* Return SYS_execve on success.
* Return -1 on error
* Return -2 if g_pid_master exited.
*/
static int
ptrace_until_execve(pid_t *pidp, int *status) {
int signum;
siginfo_t sigi;
pid_t pid = *pidp;
void *data = NULL;
static pid_t ss_pids[16 * 1024]; // Stray Stops
static int ss_idx;
*status = 0;
while(1) {
if ((pid > 0) && (ptrace(PTRACE_CONT, pid, NULL, data) == -1))
GOTOERR("ptrace(%d): %s\n", pid, strerror(errno));
data = NULL;
if ( (pid = waitpid(-1, status, WUNTRACED)) == -1)
GOTOERR("waitpid()=%d: %s\n", pid, strerror(errno));
*pidp = pid;
if (WIFEXITED(*status)) {
DEBUGF("pid="CY"%d "CG"exited"CN".\n", pid);
if (pid == g_pid_master)
exit(WEXITSTATUS(*status)); // tracee exited. Exit with same error code.
pid = 0;
continue;
}
if (WIFSIGNALED(*status)) {
// Tracee was termianted with a signal
signum = WTERMSIG(*status);
DEBUGF(CY"%d "CDY"terminated"CN" by SIG-%d\n", pid, signum);
if (pid == g_pid_master) {
if (signum == SIGSEGV)
exit(128 + signum); // Do not generate core dump of zapper.
// Tracer to commit suicide with same signal as tracee died.
if (g_flags & IS_SIGNAL_PROXY)
signal(signum, SIG_DFL);
DEBUGF(CR"SUICIDE\n"CN);
kill(getpid(), signum);
}
pid = 0;
continue;
}
if (!WIFSTOPPED(*status)) {
ERREXIT(255, "SHOULD NOT HAPEN?\n");
// SHOULD NOT HAPPEN
pid = 0;
continue;
}
// 5 = SIGTRAP
// 17 = SIGCHLD
// 19 = SIGSTOP
signum = WSTOPSIG(*status);
if (! (signum & 0x80)) {
// Signal was for TRACEE (not tracer)
if (signum == SIGTRAP) {
DEBUGF("Event for "CY"%d"CN" ("CDG"event=%d"CN")\n", pid, (*status >> 16) & 0xffff);
// NOTE: Stop occures in parent, not the newly created thread.
switch ((*status >> 16) & 0xffff) {
case PTRACE_EVENT_CLONE: // 3
DEBUGF(CDR"CLONE()"CN" not implemented\n");
break;
case PTRACE_EVENT_EXIT: // 6
// EVENT_EXIT should never trigger before EVENT_FORK (?). See Note #3.
break;
case PTRACE_EVENT_FORK: // 1
case PTRACE_EVENT_VFORK: ; // 2
unsigned long cpid;
XFAIL(ptrace(PTRACE_GETEVENTMSG, pid, NULL, &cpid) == -1, "ptrace(%d): %s\n", pid, strerror(errno));
DEBUGF(CDY"FORK "CY"%d"CDY" to cpid="CY"%lu\n"CN, pid, cpid);
// Wait for the child to be stopped:
// It can happen that SIGSTOP for this cpid arrived before the EVENT_FORK.
// In that case, by the time the EVENT_FORK happens, we can no longer
// wait() for the SIGSTOP signal (because it has already been delivered).
// Thus we must use WNOHANG and waitpid() will return 0 indicating that
// the child has already been stopped.
// On the other hand, EVEN_FORK may be triggered before the cpid is stopped. Thus
// we need to waitpid normally.
// waitpid(,,WUNTRACED) => Will hang forever if SIGSTOP got delivered before EVENT_FORK
// waitpid(,,WNOHANG) => May return 0 (child exists) but PTRACE_SETOPTIONS will
// fail with -ENOENT (No such process))
// => The only way around this is to track all stray SIGSTOPs. A good test is to
// use './zapper -n 500' within a zapped tmux.
int opt = WUNTRACED;
int i;
for (i = 0; i < sizeof ss_pids / sizeof *ss_pids; i++) {
if (ss_pids[i] != cpid)
continue;
ss_pids[i] = 0;
opt |= WNOHANG;
break;
}
waitpid(cpid, NULL, opt);
// Note #3: cpid may have already exited (and before we received its EVENT_EXIT).
// The only thing we can hope for is trying to call ptsetoptions() and dont
// fail hard if ptsetoptions() fails (e.g. when client has already exited).
ptsetoptions(cpid);
if (ptrace(PTRACE_CONT, cpid, NULL, NULL) == -1) {
DEBUGF("ERROR zapper: ptrace(%ld): %s\n", cpid, strerror(errno));
// Child may have exited already
// if (errno != EIO)
// fprintf(stderr, "ERROR zapper: ptrace(%ld): %s\n", cpid, strerror(errno));
}
break;
case PTRACE_EVENT_EXEC: // 4
// Catch execve() after returning from syscall.
// Trap when the SYSCALL exit occurs...
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
pid = 0; // ...and to not call PTRACE_CONT.
break;
}
continue;
}
// SIGSTOP may arrive _before_ we receive the fork() event (above).
// [DEBUG zapper.c:455] Stray SIGSTOP for (untracked?) pid=42056 (status=4991)
// [DEBUG zapper.c:426] Event for 42054 (event=1)
// [DEBUG zapper.c:436] FORK 42054 to cpid=42056
if (signum == SIGSTOP) {
DEBUGF(CR"Stray SIGSTOP for (untracked?) pid=%d (status=%d)\n"CN, pid, *status);
ss_pids[ss_idx] = pid;
ss_idx = (ss_idx + 1) % (sizeof ss_pids / sizeof *ss_pids);
pid = 0; // Do not continue cpid. Continue cpid after EVENT_FORK.
continue;
}
// Forward signal to offending process.
if (ptrace(PTRACE_GETSIGINFO, pid, NULL, &sigi) == -1) {
DEBUGF("SHOULD NOT HAPPEN\n");
continue;
}
// Do not forward SIGCHLD to report when child has stopped (by trap).
if (signum == SIGCHLD) {
switch (sigi.si_code) {
case CLD_DUMPED:
case CLD_TRAPPED:
case CLD_STOPPED:
case CLD_CONTINUED:
DEBUGF("NOT forwarding signal [%d, %d, %d]\n", sigi.si_signo, sigi.si_code, sigi.si_errno);
continue;
}
}
DEBUGF("Forwarding "CDY"SIG_%d"CN" to pid "CY"%d"CN" [%d, %d, %d]\n", signum, pid, sigi.si_signo, sigi.si_code, sigi.si_errno);
data = (void *)((long)signum);
continue;
}
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &g_iov) == -1)
GOTOERR("ptrace(GETREGSET, %d): %s\n", pid, strerror(errno));
// if (SYSNO(*regsp) != SYS_execve)
// ERREXIT(255, "Not SYS_execve()\n"); // CAN NOT HAPPEN. We only trap execve().
// Linux prior 5.3 does not have GET_SYSCALL_INFO.
int ret = 1;
#ifdef PTRACE_GET_SYSCALL_INFO
struct ptrace_syscall_info si;
ret = ptrace(PTRACE_GET_SYSCALL_INFO, pid, sizeof si, &si);
if (ret != -1) {
DEBUGF("pid="CY"%d"CDY" OP #%d"CN" %d-%d\n", pid, si.op, (*status >> 8) & ~0x80, *status & 0xff);
if (si.op != PTRACE_EVENTMSG_SYSCALL_EXIT)
continue;
DEBUGF(" RET=%lld\n", si.exit.rval);
DEBUGF(" ISERR=%d\n", si.exit.is_error);
if (si.exit.is_error != 0)
continue;
ret = 0;
} else {
if (errno != EIO)
ERREXIT(255, "ptrace(%d)=%d %s\n", pid, ret, strerror(errno));
// HERE: No PTRACE_GET_SYSCALL_INFO.
// This can happen if using the Linux >= 5.3 static binary on Linux < 5.3
ret = 1;
}
#else
# warning "No PTRACE_GET_SYSCALL_INFO defined. Linux < 5.3? Using compat mode."
#endif
if (ret == 1) {
// PTRACE_GET_SYSCALL_INFO not available or call failed.
// FIXME: May need to use PTRACE_GETREGSET to better support Linux < 5.3
}
*pidp = pid;
return SYS_execve;
}
err:
return -1; // FATAL
}
static pid_t
start_trace_parent(const char *orig_prog, char *new_argv[]) {
pid_t pid;
pid_t pid_tracee;
int ret;
int status;
// Try ZAPPER to be the CHILD (tracer) and trace the PARENT (tracee)
// See ptrace_scope
// https://www.kernel.org/doc/Documentation/security/Yama.txt
int up[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, up) != 0)
goto err;
pid_tracee = getpid();
g_pid_master = getpid();
pid = fork();
if (pid != 0) {
// PARENT (tracee)
// Wait for first child to exit. (See Note-#1)
waitpid(pid, &ret, WUNTRACED);
#ifdef PR_SET_PTRACER
prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY);
#else
# warning "PR_SET_PTRACE_ANY not defined. SUper old linux?"
#endif
// Cant use kill(getpid(), SIGSTOP); because of prctl().
close(up[0]);
ret = write(up[1], &ret, sizeof ret); // Signal to TRACER that we are ready for ATTACH.
ret = read(up[1], &ret, sizeof ret); // Wait for TRACER to be ready.
close(up[1]);
// Check if CHILD (tracer) successfully attached to us; PARENT (tracee)
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
execvp(orig_prog, new_argv);
exit_emu_shell(127, orig_prog);
}
// TRACER failed to trace us.
kill(pid, SIGKILL); // Kill the TRACER (if still alive)
return -1;
}
// CHILD (tracer) (pid == 0)
// Note-#1: Fork again & let first child exit.
// This stops SIGCHLD to be send to the real parent when the TRACER exists
// without us messing with the sigmask.
pid = fork();
if (pid != 0)
exit(0);
g_pid = getpid();
// Wait for TRACEE to tell us when we can attach to TRACEE.
close(up[1]);
if (read(up[0], &ret, sizeof ret) < 0)
goto err;
if (ptrace(PTRACE_ATTACH, pid_tracee, NULL, NULL) == -1)
goto err;
// 5 == SIGTRAP
// 19 == SIGSTOP.
// 1029 (SIGTRAP | PTRACE_EVENT_EXEC << 8)
if (waitpid(pid_tracee, &ret, WUNTRACED) == -1)
GOTOERR("waitpid(%d): %s\n", pid_tracee, strerror(errno));
XFAIL(ptsetoptions(pid_tracee) == -1, "ptrace(%d): %s\n", pid_tracee, strerror(errno));
// Tell TRACEE that we are attached and TRACEE can call execve().
if (write(up[0], &ret, sizeof ret) != sizeof ret)
goto err;
close(up[0]);
DEBUGF("Tracing %d\n", pid_tracee);
ret = ptrace_until_execve(&pid_tracee, &status);
if (ret > 0)
return pid_tracee;
err:
close(up[0]);
close(up[1]);
return -1;
}
static void
fix_stack(pid_t pid)
{
size_t stack_sz;
char *stack;
unsigned long *stackp = NULL;
unsigned long *valp;
unsigned long spare_ofs = 0;
unsigned long argv0_ofs = 0;
unsigned long envv0_ofs = 0;
unsigned long stack_end;
int elft_idx;
int idx;
if (g_flags & FL_DRYRUN)
return;
// Find the end of the stack. We can not use /proc/<PID>/maps as this becomes
// inaccessible for the tracer if the tracee changes the EUID (is this a
// kernel bug? We are already tracing the tracee and have full control
// of the tracee anyway, so why deny access???)
for (errno = 0, idx = 0, stackp = NULL; errno == 0; idx++) {
stackp = realloc(stackp, (idx + 1) * sizeof (void *));
stackp[idx] = ptrace(PTRACE_PEEKDATA, pid, (unsigned long)SP(g_regs) + idx * sizeof (void *), NULL);
}
if (idx <= 1) {
// Can happen on EUID/EGID bins (even when root, e.g. inside docker container)
// CAP_SYS_PTRACE not set
DEBUGF("ERROR zapper: ptrace(%d): %s\n", pid, strerror(errno));
// fprintf(stderr, "ERROR zapper: ptrace(%d): %s\n", pid, strerror(errno));
return;
}
stack_end = SP(g_regs) + (idx - 1) * sizeof (void *);
stack = (char *)stackp;
stack_sz = stack_end - SP(g_regs);
DEBUGF("=> SP 0x%lx-0x%lx (stack_sz=%zu)\n", (unsigned long)SP(g_regs), stack_end, stack_sz);
dumpfile("stack.dat", stack, stack_sz);
DEBUGF("argc = %lx\n", stackp[0]);
DEBUGF("&argv[0] = %lx\n", stackp[1]);
char *str = &stack[stackp[1] - SP(g_regs)];
DEBUGF("argv[0] = '"CDR"%s""'\n"CN, str);
// Make argv0 smaller (See Note #2)
char *end = str + strlen(str);
while ((end-- > str) && (*end == ' '))
*end = '\0';
DEBUGF("argv[0] = '"CDR"%s""'\n"CN, str);
#ifdef DEBUG
idx = 0;
fprintf(out, "ARGS=");
while ((void *)stackp[idx + 1] != NULL) {
// DEBUGF("%lx\n", stackp[idx +1 ]);
fprintf(out, "'%s' ", &stack[stackp[idx + 1] - SP(g_regs)]);
idx++;
}
fprintf(out, "\n");
#endif
// Find lowest address (which normally is ARGV[0] but dont have to be).
size_t len = 0;
valp = &stackp[1];
argv0_ofs = stackp[1] - SP(g_regs);
unsigned long arg_min = stack_end;
unsigned long arg_max = SP(g_regs);
for (; *valp != 0x00 /* NULL */; valp++) {
arg_min = MIN(arg_min, *valp);
arg_max = MAX(arg_max, *valp);
}
arg_max += strlen(&stack[arg_max - SP(g_regs)]) + 1;
valp++; // Skip NULL
// Skip through envp and find start of elf-table
// stack_envp = valp;
envv0_ofs = (valp - &stackp[0]) * sizeof (void *);
unsigned long env_min = stack_end;
unsigned long env_max = SP(g_regs);
for (; *valp != 0x00 /* NULL */; valp++) {
env_min = MIN(env_min, *valp);
env_max = MAX(env_max, *valp);
}
env_max += strlen(&stack[env_max - SP(g_regs)]) + 1;
valp++; // Skip NULL
unsigned long smin, smax;
smin = MIN(arg_min, env_min);
smax = MAX(arg_max, env_max);
DEBUGF("ARG from +%lu to +%llu (%lu bytes)\n", arg_min - (unsigned long)SP(g_regs), arg_max - SP(g_regs), arg_max - arg_min);
DEBUGF("ENV from +%lu to +%llu (%lu bytes)\n", env_min - (unsigned long)SP(g_regs), env_max - SP(g_regs), env_max - env_min);
// valp now points to start of ELF Table.
DEBUGF("stackp 0x%lx valp 0x%lx\n", (unsigned long)stackp, (unsigned long)valp);
elft_idx = valp - stackp; // this is INDEX, not offset.
DEBUGF("Elf Table start at idx=%d (ofs=%lu)\n", elft_idx, elft_idx * sizeof (void *));
unsigned long ofs;
// Find where Randomized Stack area starts and how long it is:
for (idx = elft_idx; stackp[idx] != 0; idx += 2) {
// After ELF-Table there is 0x00 + 16 bytes random + "x86_64\0"
// The specs dont define which comes first so we need to check and
// find the largest to determine where the randomized stack starts.
if (stackp[idx] == 0x0f) {
// pointer to AT_PLATFORM string.
ofs = stackp[idx + 1] - SP(g_regs);
ofs += strlen(&stack[ofs]) + 1;
if (ofs > spare_ofs)
spare_ofs = ofs;
}
if (stackp[idx] == 0x19) {
// pointer to AT_RANDOM (16 bytes).
ofs = stackp[idx + 1] - SP(g_regs) + 16;
if (ofs > spare_ofs)
spare_ofs = ofs;
}
}
// size_t elft_sz = (idx - elft_idx + 2) * sizeof (void *);
// DEBUGF("ELF Table size=%zd\n", elft_sz);
// Calculate the gap that should be added
// so that entire stack is still 16 bytes aligned.
len = (smax - smin);
unsigned long sz = stack_sz + len;
if (sz != (sz & ~15))
sz = (sz + 16) & ~15;
len = sz - stack_sz;
DEBUGF("Creating a gap of %zu bytes\n", len);
// FIXME: we could use spare space from randomized stack, if available.
stack = realloc(stack, stack_sz + len);
// Copy everything down to where kernel's pointers point to.
memcpy(stack + stack_sz, stack + stack_sz - len, len);
stackp = (unsigned long *)stack;
stack_sz += len;
// Adjust the elf table that we moved to a lower address by len.
// Find the start of the randomized (spare_ofs).
for (idx = elft_idx; stackp[idx] != 0; idx += 2) {
// See https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/linux/auxvec.h
switch (stackp[idx]) {
case 0x19: // AT_RANDOM
case 0x1f: // AT_EXECFN, static=outside, dynamic=inside
case 0x21: // ptr to "\177ELF\002\001\001\000", normally outside stack region.
case 0x0f: // AT_PLATFORM
case 0x18: // AT_BASE_PLATFORM
if (stackp[idx + 1] == 0)
break; // Value is NULL (not set)
if (stackp[idx + 1] - SP(g_regs) > stack_sz) {
DEBUGF("[0x%02x] Value outside of fake stack: 0x%lx\n", (unsigned int)stackp[idx], stackp[idx + 1]);
break; // Address is at higher address that wasnt moved.
}
// DEBUGF("Adjusting 0x%02x [%s]\n", (unsigned int)stackp[idx], &stack[stackp[idx + 1] - SP(*regsp)]);
stackp[idx + 1] -= len;
break;
}
}
// Adjust address off all argv-pointers
valp = (unsigned long *)&stackp[1]; // [0] is argc and argv starts at [1]
while (*valp != 0)
*valp++ -= len;
// ZAP argv
memset(&stack[(unsigned long)arg_min - SP(g_regs) + len], 0, arg_max - arg_min);
// Adjust address off all env-pointers
valp = (unsigned long *)&stack[envv0_ofs];
while (*valp != 0)
*valp++ -= len;
// ZAP env
if (g_flags & FL_ZAP_ENV)
memset(&stack[(unsigned long)env_min - SP(g_regs) + len], 0, env_max - env_min);
// Copy g_cur_prg_name (-a name) to argv[0] (whom's location the kernel
// references). This may overlap into ENV if g_cur_prg_name is longer
// than the original argv[*] but only when it's the tracee's tracee
// (a grand-tracee of the tracer). See also Note #2.
size_t max_sz = smax - smin;
size_t prglen = MIN(max_sz - 1, strlen(g_cur_prg_name));
memcpy(&stack[argv0_ofs + len], g_cur_prg_name, prglen);
stack[argv0_ofs + len + prglen] = '\0';
// dumpfile("stack2.dat", stack, stack_sz);
// Increase the stack size (by decreasing the stack pointer).
SP(g_regs) -= len;
DEBUGF("New stack 0x%llx-0x%lx (size=%llu == %zu)\n", SP(g_regs), stack_end, stack_end - SP(g_regs), stack_sz);
ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &g_iov);
ptpokecpy(pid, (void *)SP(g_regs), stack, stack_sz);
free(stack);
}
static void
follow_forever(pid_t pid)
{
int status;
while (ptrace_until_execve(&pid, &status) > 0) {
fix_stack(pid);
}
exit(255);
}
// Trap at ELF's AT_ENTRY
static pid_t
start_trace(char *orig_prog, char *new_argv[]) {
pid_t pid;
char *orig_argv0 = new_argv[0];
char *enlarged_argv0 = NULL;
char *ptr;
// Determine name of programm to show in 'ps'.
if (! (g_flags & FL_PRGNAME) ) {
if ( (ptr = strrchr(orig_prog, '/')))
g_cur_prg_name = ++ptr;
else
g_cur_prg_name = orig_prog;
}
// Note #2: ADM reported a bug when under special conditions the -a name
// may leak into the environment:
// $ ./zapper -a 12345678990abcdef cat
// $ xxd /proc/$(pidof 12345678990abcdef)/environ | head
// The solution is to make enough space for the tracer to put the fake
// -a name and remove the extra string later when tracing the execve().
size_t glen = strlen(g_cur_prg_name);
size_t alen = strlen(new_argv[0]);
if (glen > alen) {
enlarged_argv0 = malloc(glen + 1);
enlarged_argv0[glen] = '\0';
memcpy(enlarged_argv0, new_argv[0], alen);
memset(enlarged_argv0 + alen, ' ', glen - alen);
DEBUGF("Enlarging argv[0] by %zu spaces to make space for longer -a name\n", glen - alen);
new_argv[0] = enlarged_argv0;
}
if (!(g_flags & FL_FOLLOW)) {
// Detach after zapping if we are a background process.
if (fcntl(0, F_GETFD, 0) != 0) {
// STDIN is closed. Assume I'm a background process.
g_flags &= ~FL_STAY_ATTACHED;
} else {
// STDIN is open
if (getpid() != tcgetpgrp(0))
g_flags &= ~FL_STAY_ATTACHED; // Got started as background process by shell
}
}
// Try for the CHILD to be the TRACER and trace this process.
if (!(g_flags & FL_FORCE_TRACER_IS_PARENT)) {
pid = start_trace_parent(orig_prog, new_argv);
if (pid > 0) {
DEBUGF("Trapped PARENT pid %d\n", pid);
if (!(g_flags & FL_FOLLOW))
g_flags &= ~FL_STAY_ATTACHED;
goto done; // We are now the CHILD.
}
DEBUGF("ERROR: TRACER failed to be the PARENT\n");
fprintf(stderr, "ERROR: Try with -c option\n");
exit(255);
}
// ### This PARENT is the TRACER and tracing the CHILD (TRACEE)
// Must always stay attached even if background process in case caller checks $!
g_flags |= FL_STAY_ATTACHED;
pid = start_trace_child(orig_prog, new_argv);
DEBUGF("[%d] Tracing child %d\n", getpid(), pid);
XFAIL(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &g_iov) == -1, "ptrace(%d): %s\n", pid, strerror(errno));
done:
if (enlarged_argv0) {
free(enlarged_argv0);
new_argv[0] = orig_argv0;
DEBUGF("restored to %p\n", new_argv[0]);
}
return pid;
}
int
main(int argc, char *argv[], char *envp[]) {
pid_t pid;
int i;
init_vars();
do_getopts(argc, argv);
pid = start_trace(argv[optind], &argv[optind]);
fix_stack(pid);
// TRACEE is a background process _OR_ TRACER is the child process
if (!(g_flags & FL_STAY_ATTACHED)) {
ptrace(PTRACE_DETACH, pid, NULL, NULL);
DEBUGF("All done. Tracer exiting....\n");
exit(0);
}
// Destroy my own argv
for (i = 1; i < argc; i++) {
DEBUGF("SelfZAP '%s' %zu\n", argv[i], strlen(argv[i]));
memset(argv[i], 0, strlen(argv[i]));
}
// Destroy my own envp
if (g_flags & FL_ZAP_ENV) {
for (i = 0; envp[i] != NULL; i++)
memset(envp[i], 0, strlen(envp[i]));
}
if (g_flags & FL_FOLLOW) {
DEBUGF("FOLLOWING...\n");
follow_forever(pid);
exit(255); // NOT REACHED.
}
ptrace(PTRACE_DETACH, pid, NULL, NULL);
// Wait for child to terminate
waitpid(pid, &i, 0);
if (WIFEXITED(i))
exit(WEXITSTATUS(i));
exit(255); // NOT REACHED
}