ptrace (from process trace) is a system call in some unix-like systems (including Linux, FreeBSD, Max OS X), which allows you to trace or debug a selected process. We can say that ptrace gives you complete control over the process: you can change the course of the program, watch and change values in memory or state of registers. It is worth mentioning that we do not get any additional rights in this case - the possible actions are limited to the rights of the running process. In addition, when tracing a program with a setuid bit, this bit itself does not work - privileges are not raised.
The article will show how to intercept system calls on the example of Linux.
1. A little about ptrace
Here is the prototype of the ptrace function:
#include <sys/ptrace.h>
long ptrace( enum __ptrace_request request, pid_t pid, void *addr, void *data);
- request is an action that needs to be performed, for example PTRACE_CONT, PTRACE_PEEKTEXT
- pid - tracer process identifier
- addr and data depend on request 'a
You can start a trace in two ways: attach to an already running process (PTRACE_ATTACH), or start it yourself using PTRACE_TRACEME. We will consider the second case, it is a little bit simpler, but the essence is the same. You can use the following arguments to control the trace:
- PTRACE_SINGLESTEP - step-by-step program execution, control will be transferred after the execution of each instruction; such tracing is slow enough
- PTRACE_SYSCALL - continue program execution until entering or exiting a system call
- PTRACE_CONT - just continue the program
For more information -
man ptrace .
2. View system calls
Let's write a program to display a list of system calls used by the program (a simple analogue of the strace utility).
')
So, first you need to make a
fork - the parent process will debug the child:
int main( int argc, char *argv[]) {
pid_t pid = fork();
if (pid)
parent(pid);
else
child();
return 0;
}
In the child process, everything is simple - we start the trace with PTRACE_TRACEME and run the necessary program:
void child() {
ptrace(PTRACE_TRACEME, 0, 0, 0);
execl( "/bin/echo" , "/bin/echo" , "Hello, world!" , NULL);
perror( "execl" );
}
When
execl is executed
, the traced process will stop, passing its new state to its parent. Therefore, the parent process must first wait for the program to start using
waitpid (you can just
wait , since the child process is only one):
int status;
waitpid(pid, &status, 0);
In order to somehow distinguish between system calls and other stops (for example, SIGTRAP), a special parameter PTRACE_O_TRACESYSGOOD is provided - when stopped on a system call, the parent process will receive the status
SIGTRAP | 0x80 :
ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESYSGOOD);
Now you can loop through PTRACE_SYSCALL before exiting the program, and watch the value of the
eax register to determine the system call number. For this we use PTRACE_GETREGS. It should be noted that the
eax register is replaced at the moment of stopping, and therefore it is necessary to use the saved
state.orig_eax :
while (!WIFEXITED(status)) {
struct user_regs_struct state;
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
// at syscall
if (WIFSTOPPED(status) && WSTOPSIG(status) & 0x80) {
ptrace(PTRACE_GETREGS, pid, 0, &state);
printf( "SYSCALL %d at %08lx\n" , state.orig_eax, state.eip);
// skip after syscall
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
}
}
Running the program, we will see something like this:
...
SYSCALL 6 at b783a430
SYSCALL 197 at b783a430
SYSCALL 192 at b783a430
SYSCALL 4 at b783a430
Hello, world!
SYSCALL 6 at b783a430
SYSCALL 91 at b783a430
SYSCALL 6 at b783a430
SYSCALL 252 at b783a430
As you can see, after the system call number 4 (and this is
sys_write ), our text is displayed.
3. Intercept system call
Now let's try to intercept the call and do something good. The
write system call looks like this:
write(fd, buf, n);
- ebx: fd - file descriptor (number)
- ecx: buf - a pointer to text to display
- edx: n - number of bytes
To replace the text, use PTRACE_POKETEXT:
// sys_write
if (state.orig_eax == 4) {
char * text = ( char *)state.ecx;
ptrace(PTRACE_POKETEXT, pid, ( void *)(text+7), 0x72626168); //habr
ptrace(PTRACE_POKETEXT, pid, ( void *)(text+11), 0x00000a21); //!\n
}
Run, and ...
...
SYSCALL 6 at 00556416
SYSCALL 197 at 00556416
SYSCALL 192 at 00556416
SYSCALL 4 at 00556416
Hello, habr!
SYSCALL 6 at 00556416
SYSCALL 91 at 00556416
SYSCALL 6 at 00556416
SYSCALL 252 at 00556416
Thus, we intercepted the
sys_write system call in the
/ bin / echo program to display our text. This is just a simple example of using ptrace. With it, you can also easily make memory dumps (this, by the way, helps a lot with solving Linux cracks), set breakpoints (using PTRACE_SINGLESTEP or replacing instructions with 0xCC), analyze registers / variables and much more.
ptrace is very useful, for example, when you can’t quickly get to the problem area of the code - if you have to jump, replace data in the debugger, and then the program dies and you have to do everything anew; if you write a program for debugging with ptrace, all these actions need to be described only once, and they will be performed automatically. Of course, in some debuggers you can write scripts - but they are probably inferior in capabilities.
UPD: forgot to post the full
source4. What to read
man ptraceman waitPlaying with ptrace, part IPlaying with ptrace, part IIsyscalls table