Zoom Icon

Intercettare con ptrace()

From UIC


This page has been translated to English Image:Flag uk.gif.

Intercettare con 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: Italian Image:Flag_Italian.gif
Comments: E adesso?



Introduction

Questo essay vuole essere la continuazione del precedente Intercettare con LD_PRELOAD. Presenta un metodo più completo e efficiente per l'intercettazione delle syscall, e cerca di dare in modo sintetico le informazioni di base che permettono di fare cose come quelle che ho fatto con il (povero :) CloneDVD in ambiente linux. Questa volta il metodo funziona anche se l'eseguibile è linkato staticamente.


Tools

Anche oggi useremo solo gcc.


Link e Riferimenti

man ptrace


Essay

Teoria

Parliamo di ptrace(). Leggiamo la descrizione del manuale:

$ 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.
[...]

Insomma, si può scrivere un debugger completo usando solo questa syscall, ed è stato fatto. Noi qui la useremo per intercettare una syscall, per la precisione la stessa getuid() che abbiamo intercettato nel precedente essay.

Il metodo standard con cui si procede consiste nell'usare la syscall fork() per sdoppiare il nostro processo. A questo punto, faremo fare da debugger a uno dei due, mentre l'altro andrà ad eseguire il nostro target, che farà da debuggee (cioè processo debuggato). Il processo debugger (il parent) verrà notificato ogni volta che nel target (il child) succede qualcosa per cui ha richiesto di essere notificato. In questo caso noi richiederemo la notifica a ogni syscall. Se questo passaggio non vi è chiaro, lo sarà quando leggerete il sorgente minuziosamente commentato che troverete alla fine dell'essay.


long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);

La syscall ptrace() può fare molte cose, quindi c'è bisogno di un parametro, il primo, che indica il tipo di richiesta che ci interessa, e che non sorprendentemente si chiama request. Inoltre dovremmo fornirgli il process id del processo su cui vogliamo lavorare, attraverso il secondo parametro, pid. Ci sono altri due parametri il cui significato varia a seconda della request che facciamo. Vediamo ora il significato delle request che ci interessano ai fini di questo essay:

  • PTRACE_TRACEME: come il nome suggerisce, se un processo fa questa request (che noi faremo fare al child) intende richiedere che il parent possa fare trace su di lui, ovvero fare da debugger. Inoltre, dopo questa chiamata, se il child chiama una delle funzioni della classe exec* per eseguire un certo programma, il parent verrà notificato. Gli altri parametri di ptrace() sono ignorati.
  • PTRACE_SYSCALL: faremo eseguire questa request al parent. Una volta eseguita, il parent verrà notificato ogni volta che il child entra o esce da una syscall. Il parametro pid indica a quale child ci riferiamo. Gli altri parametri sono ignorati.
  • PTRACE_GETREGS: faremo fare al parent questa request per leggere i registri del child. I loro valori verranno letti in una struttura user_regs_struct, simile alla struttura CONTEXT su Windows, tra i membri della quale troviamo cose molto familiari come eax, eip, etc., che passeremo per indirizzo nel parametro data. Il parametro addr viene ignorato.
  • PTRACE_SETREGS: praticamente uguale alla precedente, l'unica differenza è che scrive i valori dei registri che trova nella struttura invece di leggerceli dentro.

Rimane solo una cosa da dire: come facciamo ad aspettare che arrivi una notifica al parent? È sufficiente chiamare wait().

pid_t wait(int *status);

Ha solo un parametro, status, che punta a un intero in cui wait() scriverà un codice che indica il tipo di evento che è arrivato. La funzione ritorna il process id del child in cui si è verificato l'evento, ma dato che nel nostro caso abbiamo un solo child possiamo ignorarlo.

Detto ciò, passiamo alla...

Pratica

Ora vediamo come mettere assieme tutto quello di cui abbiamo parlato finora.

Ricordate il programma target dell'essay precedente? Eccolo qui:

// target.c
&#35;include <stdio.h>
&#35;include <unistd.h>

int main() {
printf( "user id: %d\n", getuid() );
return 0;
}

Questa volta lo compiliamo staticamente:

$ gcc -static target.c -o target

Il metodo che abbiamo studiato l'altra volta non funziona più.

Grazie a ptrace() ora andiamo a intercettare le chiamate a getuid(). Ci interessa in particolar modo il punto in cui il child esce da questa syscall, perchè è lì che andremo a modificare il valore del registro eax, che come probabilmente saprete contiene il valore di ritorno di una funzione, a nostro piacimento. Per capire se ci siamo fermati su getuid() o su un'altra syscall, leggiamo il numero della syscall, contenuto nel membro orig_eax della struttura user_regs_struct, e lo confrontiamo con la costante SYS_getuid32. Questa ed altre costanti che danno un nome ai vari numeri delle syscall sono definiti nel file bits/syscall.h che solitamente potete trovare in /usr/include/.

Quello che segue è il sorgente del nostro tracer, o debugger. Invece di dilungarmi a spiegare pezzettini di codice, ho preferito aggiungere dei commenti, il cui numero rasenta l'infinito, quindi penso che siano sufficienti per spiegare tutto :)

// tracer.c
&#35;include <sys/ptrace.h>
&#35;include <sys/types.h>
&#35;include <sys/wait.h>
&#35;include <unistd.h>
&#35;include <string.h>
&#35;include <errno.h>
&#35;include <linux/user.h>
&#35;include <sys/syscall.h>
&#35;include <sys/reg.h>

// questi #define rendono piu' generico e adattabile il programma
// TARGET va cambiato, e' il percorso assoluto del target
&#35;define TARGET "/percorso/completo/del/target"
&#35;define NEW_UID 0

int main() {
// lo stato del child quando si interrompe, mi serve solo
// a capire se e' terminato
int status = 0;
// conterra' il numero della syscall intercettata
int syscall_n = 0;
// il child si interrompe sia all'ingresso che all'uscita
// della syscall, con questa variabile riesco a
// distinguere (vedere il seguito)
int entering = 1;
// struttura che serve a contenere i registri del child
struct user_regs_struct regs;
// fork() crea un nuovo processo come copia del corrente:
// - in uno dei due (il parent) ritorna il process id (pid) del figlio
// - nell'altro (il child) ritorna 0
// con uno dei due processi eseguo il target, con l'altro
// faccio trace su di lui
int pid = fork();

if ( !pid ) { // siamo il processo child (pid == 0)
// fa in modo che il parent possa fare trace sul child
ptrace( PTRACE_TRACEME, 0, 0, 0 );
// esegue il target, sui cui ora si puo' fare trace,
// e interrompe il parent
execlp( TARGET, TARGET, 0 );
}
else { // siamo il processo parent
// aspetto il primo evento, che arriva quando il child
// esegue il target (e lo ignoro). ora il child e' bloccato
wait( &status );

while ( 1 ) {
// segnalo che voglio interrompere ad ogni syscall,
// questa chiamata inoltre fa ripartire il child
ptrace( PTRACE_SYSCALL, pid, 0, 0 );

// aspetto un evento:
// - il primo sara' causato dal child, quando esegue il target (vedi prima)
// - i successivi saranno le syscall
// - l'ultimo sara' la terminazione del child
// (ora il child e' bloccato)
wait( &status );

// con questa macro capisco se il child e' terminato,
// in tal caso interrompo anche il mio ciclo.
if ( WIFEXITED( status ) ) break;

// leggo i registri del child
ptrace( PTRACE_GETREGS, pid, 0, &regs );
// il membro orig_eax contiene il numero della syscall
syscall_n = regs.orig_eax;
// se e' quella che cercavo agisco
if ( syscall_n == SYS_getuid32 ) {
// se il child sta entrando nella syscall...
if ( entering ) {
// mi segno che la prossima volta stara' uscendo
entering = 0;
}
else {
// se invece sta uscendo leggo i registri
ptrace( PTRACE_GETREGS, pid, 0, &regs );
// modifico il valore di ritorno (eax) con il nuovo id
regs.eax = NEW_UID;
// e scrivo i registri cosi' modificati
ptrace( PTRACE_SETREGS, pid, 0, &regs );
// inoltre mi segno che la prossima volta stara'
// entrando nella syscall
entering = 1;
}
}
}
}

return 0;
}

Compiliamo il nostro tracer:

$ gcc tracer.c -o tracer

E proviamo ad eseguire il nostro target prima normalmente, e poi con il tracer:

$ ./target
user id: 1000
$ ./tracer
user id: 0

Come vedete, anche questa volta ce l'abbiamo fatta. Avrete certamente capito che si possono fare molte altre cose interessanti grazie a ptrace(), infatti ci sono molte più request di quelle qui trattate. Il manuale chiarirà ogni vostra curiosità.

Per comodità, ecco il sorgente del tracer senza commenti:

&#35;include <sys/ptrace.h>
&#35;include <sys/types.h>
&#35;include <sys/wait.h>
&#35;include <unistd.h>
&#35;include <string.h>
&#35;include <errno.h>
&#35;include <linux/user.h>
&#35;include <sys/syscall.h>
&#35;include <sys/reg.h>

&#35;define TARGET "/percorso/completo/del/target"
&#35;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;
}


Note Finali

Secondo essay nella sezione linux. Spero via sia piaciuto.
Ringrazio tutta la gentaglia della UIC, come al solito :)
Ci vediamo al prossimo 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.