Courses/Computer Science/CPSC 601.29.ISSA/20110211CodeSession
References / Documentation
A few pieces of preamble:
We looked at the following pieces of reference material:
- man 3 strsignal
- man 7 signal
- man man
- man 2 wait
- man 2 ptrace (on both Linux and Mac, noting differences)
Sample Output
Our program snyfer had the following sample output:
[michael@proton ptrace]$ ./snyfer -p 6269 [snyfer] tracing process 6269 Successful attach. Child stopped by signal 19 : Stopped (signal) snyfer> run not implemented snyfer> cont stopped by signal 5 : Trace/breakpoint trap process issued syscall: write(1, -1216974841, 23) snyfer> cont stopped by signal 5 : Trace/breakpoint trap system call returned 23 snyfer> cont stopped by signal 5 : Trace/breakpoint trap process issued syscall: write(1, -1216974848, 30) snyfer> cont stopped by signal 5 : Trace/breakpoint trap system call returned 30 snyfer> kill killing child and exiting... [michael@proton ptrace]$
The snyfer code
This code is presented in source code order, not in expected control flow order (i.e., from higher levels of abstraction down to details).
We need to include a number of headers to get access to ptrace and supplementary facilities like I/O, system call symbols, and data structures for communicating with the kernel about saved registers. We also need to define the GNU_SOURCE symbol because we use the GNU/Linux specific function `strsignal(3)'
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> //memset, strncmp, strsignal #include <unistd.h> #include <sys/wait.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/syscall.h> #include <sys/user.h> #include <asm/ptrace-abi.h> #include <asm/ptrace.h>
After some more global state and symbols (yes, admittedly I took the easy way out for dealing with cmdline input):
#define LINELEN 81 extern char* __progname; //a GNU symbol /** The process ID to trace. */ long int tr_pid = 0; /** pointer to user-entered string */ char* usercmd = NULL;
We now define a number of subroutine function signatures (I did this retroactively after I had written a piece of monolithic spaghetti code). I like specifying the `static' keyword because there is (as yet) no reason for these functions to be invoked from outside this `snyfer.c' module. As a brief design note, the system starts up, calls `init_attach()', calls `create_interpreter()' and then invokes `do_sniff()'. This function loops around a call to ptrace() within `handle_continue()' and occasionally calls `handle_syscall()'.
static void do_sniff(); static void do_usage(void); static void handle_syscall(int*); static void init_attach(char*); static void create_interpreter(void); static void quit(void); static void kill_trace_session(void); static void handle_continue(int*, int*);
The usage implementation isn't terribly exciting, except to show how you can use the `__progname' symbol instead of manipulating argv[0] (which is poor form).
static void do_usage() { fprintf(stderr, "Usage: %s -p [pid]\n", __progname); return; }
The create_interpreter function simply allocates memory to hold the input we will receive from the user on stdin.
static void create_interpreter() { usercmd = calloc(LINELEN, sizeof(char)); if(NULL==usercmd) { fprintf(stderr, "failed to allocate memory for user command line\n"); exit(-3); } return; }
The `init_attach()' function parses its parameter as a number (i.e., a process ID). It then asks the kernel (via ptrace) to register it as the target processes parent, which effectively means that this process (snyfer) wants to receive signals about what happens in the child/traced process. Note the use of the waitpid() system call so that the new parent (i.e., us) can synchronize execution with the child's state.
static void init_attach(char* tpid) { int s = 0; long p_ret = 0; pid_t p = 0; int attach_status = 0; tr_pid = strtol(tpid, NULL, 10); fprintf(stdout, "[snyfer] tracing process %ld\n", tr_pid); p_ret = ptrace(PTRACE_ATTACH, tr_pid, NULL, //ignored NULL); //ignored if(-1==p_ret) { fprintf(stderr, "[snyfer] failed to attach to child, exiting...\n"); exit(-1); } p = waitpid(tr_pid, &attach_status, WUNTRACED | WCONTINUED);
if(WIFSTOPPED(attach_status)) { s = WSTOPSIG(attach_status); fprintf(stdout, "Successful attach. Child stopped by signal %d : %s\n", s, strsignal(s)); }else{ fprintf(stdout, "failed to stop / attach target process\n"); exit(-2); } return; }
The `handle_syscall' function demonstrates the use of different ptrace requests to extract data from the user meta-data memory area in ptrace and the request to read the register set. For the brave, there are also ptrace requests to write to both the memory and the register set. Naturally, this means you can change both data (for example, data used in making control flow decisions) and registers (for example, used in storing stack state or system call arguments, as well as pretty much everything else). Finally, recall our discussion about indexes vs. scaling the index to the appropriate word or byte. This is basically because we are dealing with pointer arithmetic here, not straight addition. Also notice the use of the `SYS_write' symbol referring to the write(2) call.
/** This function assumes everything else is set up properly. */ static void handle_syscall(int* syscall_started) { long int oeax = 0; long int eax = 0; struct user_regs_struct regfile; oeax = ptrace(PTRACE_PEEKUSER, tr_pid, 4 * ORIG_EAX, NULL); if(SYS_write==oeax) { if(0==*syscall_started) { *syscall_started = 1; //print registers ptrace(PTRACE_GETREGS, tr_pid, NULL, ®file); fprintf(stdout, "process issued syscall: write(%ld, %ld, %ld)\n", regfile.ebx, regfile.ecx, regfile.edx); }else{ eax = ptrace(PTRACE_PEEKUSER, tr_pid, 4 * EAX, NULL); fprintf(stdout, "system call returned %ld\n", eax); *syscall_started = 0; } }else{ fprintf(stdout, "issued unhandled syscall %ld\n", oeax); } return; }
We can define a subroutine for issuing a SIGKILL via ptrace. Note that doing so doesn't kill us -- merely the child. Of course, we have nothing else to do but twiddle our thumbs now, so we call exit(0) and quickly terminate as well.
static void kill_trace_session() { //send child PTRACE_KILL fprintf(stdout, "killing child and exiting...\n"); ptrace(PTRACE_KILL, tr_pid, NULL, NULL); free(usercmd); usercmd = NULL; exit(0); }
Calling `quit()' allows us to terminate while letting the child continue because we requested PTRACE_DETACH via ptrace(2). Notice both here and above that I am returning the usercmd memory chunk to glibc before exiting. Nice of me, but not strictly necessary because the entire process will be cleaned up very soon -- but still a good habit to form. Keeping track of allocation and de-allocation sites is tricky, particularly across multiple components, scopes, and layers of abstraction. A very nice source of bugs.
static void quit() { //cleanup and quit fprintf(stdout, "Tracer will quit. Traced process %ld will continue running.\n", tr_pid); ptrace(PTRACE_DETACH, tr_pid, NULL, NULL); free(usercmd); usercmd = NULL; exit(0); }
Starting off with some pointer notation discussion from class today, we encounter the `handle_continue()' function.
/** int* i = 0; int a = 100; int* x = &a; int y = (*x) + 1; a = 200; */ static void handle_continue(int* cont, int* syscall_started) { long p_ret = 0; pid_t p = 0; int attach_status = 0; int s = 0; //continue to the next system call (or end of this one) p_ret = ptrace(PTRACE_SYSCALL, tr_pid, NULL, NULL); p = waitpid(tr_pid, &attach_status, WUNTRACED | WCONTINUED); if(-1==p) { perror("waitpid"); exit(-5); } if(WIFEXITED(attach_status)) { fprintf(stdout, "exited, status=%d\n", WEXITSTATUS(attach_status)); }else if (WIFSIGNALED(attach_status)){ s = WTERMSIG(attach_status); fprintf(stdout,"killed by signal %d : %s\n", s, strsignal(s)); }else if (WIFSTOPPED(attach_status)){ s = WSTOPSIG(attach_status); fprintf(stdout,"stopped by signal %d : %s\n", s, strsignal(s)); if(SIGTRAP==WSTOPSIG(attach_status) || SIGSTOP==WSTOPSIG(attach_status)) { handle_syscall(syscall_started); }else{ fprintf(stderr, "unrecognized signal state for stopped @ syscall\n"); } } else if (WIFCONTINUED(attach_status)) { fprintf(stdout,"continued\n"); }else{ fprintf(stdout, "unrecognized stop condition\n"); } if(!WIFEXITED(attach_status) && !WIFSIGNALED(attach_status)) { *cont = 1; }else{ fprintf(stdout, "setting stop flag\n"); *cont = 0; } return; }
And the `do_sniff()' function, which kicks off handle_continue and the system call handler in turn.
/** long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data) */ static void do_sniff() { int should_continue = 1; //TRUE int syscall_started = 0; // FALSE do { fprintf(stdout, "snyfer> "); usercmd = fgets(usercmd, LINELEN, stdin); if(NULL == usercmd) { fprintf(stderr, "problem reading input\n"); exit(-4); } if(0==strncmp(usercmd, "quit", 4) || 0==strncmp(usercmd, "exit", 4)){ quit(); }else if(0==strncmp(usercmd, "run", 3)){ fprintf(stdout, "not implemented\n"); }else if(0==strncmp(usercmd, "kill", 4)){ kill_trace_session(); }else if(0==strncmp(usercmd, "cont", 4)){ handle_continue(&should_continue, &syscall_started); }else{ fprintf(stdout, "snyfer did not recognize your command\n"); } memset(usercmd, '\0', LINELEN); } while(1==should_continue); return; }
And finally a simple main, which sets things up and then hands off control to do_sniff. Sadly, we aren't yet at the point where we re-write system call params...well, yes we are...we just need to do it in handle_syscall above.
/** * ./snyfer -p [PID] * * opens an interactive session where you can re-write arguments to * system calls of the traced process. Its default mode is simply to * print the syscall number and arg values. * * */ int main(int argc, char* argv[]) { if(3==argc) { init_attach(argv[2]); create_interpreter(); do_sniff(); }else{ do_usage(); return -1; } return 0; }