📜 ⬆️ ⬇️

Intercepting system calls with ptrace

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);
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:
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);
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 source

4. What to read


man ptrace
man wait
Playing with ptrace, part I
Playing with ptrace, part II
syscalls table

Source: https://habr.com/ru/post/111266/


All Articles