Zoom Icon

Intercepting with ptrace()

From UIC


Questa pagina esiste anche in Italiano Image:Flag it.gif.

Intercepting with ptrace()

Contents


Infos
Author: Bender0
Email: Image:Mips-email.png
Website: http://mips42.altervista.org/
Date: 12/08/2007 (dd/mm/yyyy)
Level: Some skills are required
Language: English Image:Flag_English.gif
Comments: What now?



Introduction

This essay aims to be a follow up to Intercepting with LD_PRELOAD. It dicusses 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'll only need gcc.


Links and References

man ptrace


Essay

Theory

Let's 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's possible to code a complete debugger using this syscall alone, and it has been done. We're 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'll 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 everytime 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 doesn't make sense to you, it will when you'll have read the plentifully commented source code you'll 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's 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'll need to provide the process id of the process we want to work on, and we'll do this with the second argument, pid. There are two more parameters whose meaning depends on the kind of request we're making. Let use explain the meaning of the requests we'll 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's 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'll 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'll now intercept all calls to getuid(). The point when our child exits the syscall is especially interesting, since it will be then that we'll 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've 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're 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's 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's 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're 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're 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( &amp;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( &amp;status );

      // with this macro I can find out wheter 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, &amp;regs );
      // member orig_eax contains the syscall number
      syscall_n = regs.orig_eax;
      // if it is the one I'm looking for, let's 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, &amp;regs );
          // I change the return value (eax) with the new id
          regs.eax = NEW_UID;
          // and write the registers I modifed
          ptrace( PTRACE_SETREGS, pid, 0, &amp;regs );
          // plus, I remember that the next time the child
          // will be entering the syscall
          entering = 1;
        }
      }
    }
  }

  return 0;
}

Let's 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've 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's 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( &amp;status );

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

      wait( &amp;status );

      if ( WIFEXITED( status ) ) break;

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

  return 0;
}


Final Notes

Second essay in the linux section. Hope you liked it.
As always, thanks to all the people from UIC :)
See you next essay... bye.


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevoli e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.