mips42

"Stuff your friends simply don't understand!"

Intercepting with ptrace()

Introduction

This essay aims to be a follow up to Intercepting with LD_PRELOAD. It discusses a more all-around and efficient technique to intercept syscalls. This time the technique works even if the target executable is statically linked.

Tools

Again, we will only need gcc.

Preliminary reading

man ptrace

Theory

Let us talk about ptrace(). We should first read the description from the manual: $ man ptrace
[...]
DESCRIPTION
  The ptrace() system call provides a means by which a parent process may
  observe and control the execution of another process, and examine and
  change its core image and registers. It is primarily used to implement
  breakpoint debugging and system call tracing.
[...]
In short, it is possible to code a complete debugger using this syscall alone, and it has been done. We are going to use it to intercept a syscall, actually the same getuid() we intercepted in the last essay.

The standard way to go consists in calling the fork() syscall from our process to split it in two. Then, we will make a debugger out of one of them, while the other one will just run our target, which in turn will be our debuggee. The debugger process (the parent) will be notified every time something happens in the target (the child) for which it required to be notified. In this case we will be requesting to be notified at every syscall. If this passage does not make sense to you, it will when you will have read the plentifully commented source code you will find at the end of this essay.

long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);
The ptrace() syscall can do many things, that is why an argument is needed, actually the first one, which indicates which kind of service we need, and which, not surprisingly, is called request. Besides, we will need to provide the process id of the process we want to work on, and we will do this with the second argument, pid. There are two more parameters whose meaning depends on the kind of request we are making. Let use explain the meaning of the requests we will look at in this essay:

  • PTRACE_TRACEME: just like the name suggests, if a process requests this (our child will) it means to ask for the parent to be able to trace it, or debug it. Furthermore, after calling ptrace() like this, if the child calls one of the functions in the exec* class to run a program, the parent will be automatically notified. The other arguments are ignored.

  • PTRACE_SYSCALL: our parent will request this. Once it does, it will be notified everytime the child enters or exits a syscall. The pid argument tells ptrace() what child we mean to monitor. The other parameters are ignored.

  • PTRACE_GETREGS: the parent will request this to read the our child's registers. Their values will be read into a user_regs_struct structure, which is similar to the CONTEXT structure in Windows, and which contains familiar members like eax, eip, etc. We will pass this structure by reference through the data parameter. The addr parameter is ignored.

  • PTRACE_SETREGS: basically identical to the former, except it writes the values it finds in the structure to the registers.

There is only one thing left to say: how do we wait for a notification to get to our parent? Just by calling wait(). pid_t wait(int *status); It has only one parameter, status, pointing to an integer value in which wait() will write a code indicating what kind of event arrived. This function returns the process id of the child which caused the event, but since we are dealing with just one child we can ignore it.

Now we can advance to...

Practice

We will now find out how to put all of what we talked about together.

Remember the target program from the last essay? Here it is: // target.c
#include <stdio.h>
#include <unistd.h>

int main() {
  printf( "user id: %d\n", getuid() );
  return 0;
}
Let us link it statically this time: $ gcc -static target.c -o target The technique from last time works no more.

Thanks to ptrace() we will now intercept all calls to getuid(). The point when our child exits the syscall is especially interesting, since it will be then that we will change the value of the eax register, which holds the return value of a procedure as you probably know, to what we like. In order to understand if we have stopped on getuid() or on another syscall, we can inspect the syscall number from the orig_eax member of the user_regs_struct structure, and compare it to the constant SYS_getuid32. This and other constants which name the syscall numbers are defined in the bits/syscall.h header, usually found in /usr/include.

The source code to our tracer, or debugger, follows. Instead of explaining tiny pieces of code, I chose to add comments, almost an infinite number of them, thus I think they are enough to explain everything :) // tracer.c
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <linux/user.h>
#include <sys/syscall.h>
#include <sys/reg.h>

// these #defines make this program easier to modify and adapt
// TARGET must be changed, it is the absolute path to the target
#define TARGET "/absolute/path/to/the/target"
#define NEW_UID 0

int main() {
  // the status of the child when it is paused, I just need this
  // to know if it is terminated
  int status = 0;
  // will hold the number of the intercepted syscall
  int syscall_n = 0;
  // the child will be interrupted both on entering and exiting
  // the syscall, I need this variable to distinguish the two
  // (see later)
  int entering = 1;
  // this structure will hold the child's registers
  struct user_regs_struct regs;
  // fork() creates a new process by copying the current:
  //  - in one of the two (the parent) it returns
  //    the child's process id (pid)
  //  - in the other one (the child) it returns 0
  // with one of them I run the target, with the other one
  // I trace it
  int pid = fork();

  if ( !pid ) { // we are the child (pid == 0)
    // let the parent trace us
    ptrace( PTRACE_TRACEME, 0, 0, 0 );
    // run the target, which can now be traced,
    // and notifies the parent
    execlp( TARGET, TARGET, 0 );
  }
  else { // we are the parent
    // I wait for the first event, which arrives when the child
    // executes the target (and ignore it). now the child is paused
    wait( &status );

    while ( 1 ) {
      // I specify I want to be notified at every syscall,
      // this also unpauses the child
      ptrace( PTRACE_SYSCALL, pid, 0, 0 );

      // I wait for an event:
      //  - the first will be caused by the child
      //    by running the target (see before)
      //  - the following will be syscalls
      //  - the last will be the child's termination
      // (now the child is paused)
      wait( &status );

      // with this macro I can find out whether the child has been
      // terminated; if so, I break my loop, too.
      if ( WIFEXITED( status ) ) break;

      // read the child's registers
      ptrace( PTRACE_GETREGS, pid, 0, &regs );
      // member orig_eax contains the syscall number
      syscall_n = regs.orig_eax;
      // if it is the one I am looking for, let us get to business
      if ( syscall_n == SYS_getuid32 ) {
        // if the child is entering the syscall...
        if ( entering ) {
          // I note down that the next time he will be exiting
          entering = 0;
        }
        else {
          // if, instead, it is exiting i read the registers
          ptrace( PTRACE_GETREGS, pid, 0, &regs );
          // I change the return value (eax) with the new id
          regs.eax = NEW_UID;
          // and write the registers I modified
          ptrace( PTRACE_SETREGS, pid, 0, &regs );
          // plus, I remember that the next time the child
          // will be entering the syscall
          entering = 1;
        }
      }
    }
  }

  return 0;
}
Let us build our tracer: $ gcc tracer.c -o tracer Now let us try to run our target, first normally, then from our tracer: $ ./target
user id: 1000
$ ./tracer
user id: 0
As you can see, we made it again. You have surely understood that there are lots of interesting things that can be done thanks to ptrace(), in fact there exist many more requests than we discussed here. The manual will make everything clear.

Here is the source for our tracer with no comments, for reference: #include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <linux/user.h>
#include <sys/syscall.h>
#include <sys/reg.h>

#define TARGET "/absolute/path/to/the/target"
#define NEW_UID 0

int main() {
  int status = 0;
  int syscall_n = 0;
  int entering = 1;
  struct user_regs_struct regs;
  int pid = fork();

  if ( !pid ) {
    ptrace( PTRACE_TRACEME, 0, 0, 0 );
    execlp( TARGET, TARGET, 0 );
  }
  else {
    wait( &status );

    while ( 1 ) {
      ptrace( PTRACE_SYSCALL, pid, 0, 0 );

      wait( &status );

      if ( WIFEXITED( status ) ) break;

      ptrace( PTRACE_GETREGS, pid, 0, &regs );
      syscall_n = regs.orig_eax;
      if ( syscall_n == SYS_getuid32 ) {
        if ( entering ) {
          entering = 0;
        }
        else {
          ptrace( PTRACE_GETREGS, pid, 0, &regs );
          regs.eax = NEW_UID;
          ptrace( PTRACE_SETREGS, pid, 0, &regs );
          entering = 1;
        }
      }
    }
  }

  return 0;
}

Final words

If you want to talk, head on to the contact page.
See you next essay... bye.
Top
This web site is written in XHTML 1.0 Strict and CSS 2.
It is licensed under a Creative Commons Attribution-Noncommercial 3.0 License.