Back to Home

Intercepting system calls using ptrace

linux · ptrace · c · syscalls

Intercepting system calls using ptrace

ptrace (from process trace) - a system call on some unix-like systems (including Linux, FreeBSD, Max OS X), which allows you to trace or debug the 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 the values ​​in memory or the status of the registers. It is worth mentioning that we do not get any additional rights - the possible actions are limited by the rights of the running process. In addition, when tracing a program with a setuid bit, this same bit does not work - privileges do not increase.

This article will demonstrate how to intercept system calls using the Linux OS as an example.

1. A little about ptrace


Here's what the prototype of the ptrace function looks like:
#include
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
  • request is an action to be taken, for example PTRACE_CONT, PTRACE_PEEKTEXT
  • pid - identifier of the traced process
  • addr and data depend on request 'and
You can start tracing 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 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 each instruction is executed; such tracing is slow enough
  • PTRACE_SYSCALL - continue the program until the system call is entered or exited
  • PTRACE_CONT - just continue program execution
For more information, see man ptrace .

2. View system calls


We will write a program to list the 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 - start the trace with PTRACE_TRACEME and run the desired program:

void child() {
  ptrace(PTRACE_TRACEME, 0, 0, 0);
  execl("/bin/echo", "/bin/echo", "Hello, world!", NULL);
  perror("execl");
}

When execl executes, the traced process will stop passing its new state to the parent. Therefore, the parent process must first wait for the program to start using waitpid (you can just wait , since there is only one child process):

  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 a system call is stopped, the parent process will receive SIGTRAP | 0x80 :

  ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_TRACESYSGOOD);

Now you can execute PTRACE_SYSCALL in a loop before exiting the program, and look at the value of the eax register to determine the system call number. To do this, use PTRACE_GETREGS. It should be noted that the eax register at the time of stopping is replaced, 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 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 system call No. 4 (and this is sys_write ), our text is displayed.

3. Intercepting a system call


Now let's try to intercept the challenge 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 the 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
      }

We launch, 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 output our text. This is just a simple example of using ptrace. It can also be used to easily make memory dumps (this, by the way, helps a lot when solving Linux crackmixes), set breakpoints (using PTRACE_SINGLESTEP or replacing the instruction with 0xCC), analyze registers / variables and much more. ptracevery useful, for example, when you can’t quickly get to the problematic code section - if you have to jump many times in the debugger, replace the data, and then the program dies and you have to do everything anew; if you write a program for debugging with ptrace, then 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

Read Next