Questo articolo spiega gli strumenti e i comandi che possono essere utilizzati per decodificare un eseguibile in un ambiente Linux.
Il reverse engineering è l'atto di capire cosa fa un software, per il quale non è disponibile codice sorgente. Il reverse engineering potrebbe non fornire i dettagli esatti del software. Ma puoi capire abbastanza bene come è stato implementato un software.
Il reverse engineering prevede i seguenti tre passaggi fondamentali:
- Raccolta delle informazioni
- Determinazione del comportamento del programma
- Intercettare le chiamate in biblioteca
Io. Raccolta delle informazioni
Il primo passo è raccogliere le informazioni sul programma target e su cosa fa. Per il nostro esempio, prenderemo il comando "chi". Il comando 'chi' stampa l'elenco degli utenti attualmente connessi.
1. Comando stringhe
Strings è un comando che stampa le stringhe di caratteri stampabili nei file. Quindi ora usiamo questo contro il nostro comando target (who).
# strings /usr/bin/who
Alcune delle stringhe importanti sono,
users=%lu EXIT COMMENT IDLE TIME LINE NAME /dev/ /var/log/wtmp /var/run/utmp /usr/share/locale Michael Stone David MacKenzie Joseph Arceneaux
Dall'output about, possiamo sapere che "chi" sta usando circa 3 file (/var/log/wtmp, /var/log/utmp, /usr/share/locale).
Ulteriori informazioni:Esempi di comandi di stringhe Linux (cerca testo in file binari UNIX)
2. nm Comando
nm comando, viene utilizzato per elencare i simboli dal programma di destinazione. Utilizzando nm, possiamo conoscere le funzioni locali e di libreria e anche le variabili globali utilizzate. nm non può funzionare su un programma di cui è stato eseguito lo striping usando il comando 'strip'.
Nota:per impostazione predefinita, il comando "chi" è rimosso. Per questo esempio, ho compilato ancora una volta il comando 'who'.
# nm /usr/bin/who
Questo elencherà quanto segue:
08049110 t print_line 08049320 t time_string 08049390 t print_user 08049820 t make_id_equals_comment 080498b0 t who 0804a170 T usage 0804a4e0 T main 0804a900 T set_program_name 08051ddc b need_runlevel 08051ddd b need_users 08051dde b my_line_only 08051de0 b time_format 08051de4 b time_format_width 08051de8 B program_name 08051d24 D Version 08051d28 D exit_failure
Nell'output sopra:
- t|T – Il simbolo è presente nella sezione del codice .text
- b|B – Il simbolo è nella sezione .data inizializzata dall'ONU
- D|d – Il simbolo è nella sezione .data inizializzata.
La lettera maiuscola o minuscola determina se il simbolo è locale o globale.
Dall'output about, possiamo sapere quanto segue,
- Ha la funzione globale (main,set_program_name,usage,ecc..)
- Ha alcune funzioni locali (print_user, time_string ecc..)
- Ha variabili inizializzate globali (Version,exit_failure)
- Ha le variabili inizializzate dall'ONU (time_format, time_format_width, ecc.)
A volte, usando i nomi delle funzioni possiamo indovinare cosa faranno le funzioni.
Leggi di più:10 pratici esempi di comandi Linux nm
Gli altri comandi che possono essere utilizzati per ottenere informazioni sono
- comando ldd
- comando fusore
- comando lsof
- /file system proc
II. Determinazione del comportamento del programma
3. comando ltraccia
Traccia le chiamate alla funzione di libreria. Esegue il programma in quel processo.
# ltrace /usr/bin/who
L'output è mostrato di seguito.
utmpxname(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0 setutxent(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 1 getutxent(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(NULL, 384) = 0x09ed59e8 getutxent(0, 384, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 768) = 0x09ed59e8 getutxent(0x9ed59e8, 768, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 1152) = 0x09ed59e8 getutxent(0x9ed59e8, 1152, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 1920) = 0x09ed59e8 getutxent(0x9ed59e8, 1920, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 1920, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 3072) = 0x09ed59e8 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78)
Puoi osservare che esiste un insieme di chiamate a getutxent e alla sua famiglia di funzioni di libreria. Puoi anche notare che ltrace fornisce i risultati nell'ordine in cui le funzioni sono chiamate nel programma.
Ora sappiamo che il comando "chi" funziona chiamando getutxent e la sua famiglia di funzioni per ottenere gli utenti che hanno effettuato l'accesso.
4. comando strace
Il comando strace viene utilizzato per tracciare le chiamate di sistema effettuate dal programma. Se un programma non utilizza alcuna funzione di libreria e utilizza solo chiamate di sistema, utilizzando ltrace semplice non possiamo tracciare l'esecuzione del programma.
# strace /usr/bin/who
[b76e7424] brk(0x887d000) = 0x887d000 [b76e7424] access("/var/run/utmpx", F_OK) = -1 ENOENT (No such file or directory) [b76e7424] open("/var/run/utmp", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3 . . . [b76e7424] fcntl64(3, F_SETLKW, {type=F_RDLCK, whence=SEEK_SET, start=0, len=0}) = 0 [b76e7424] read(3, "\10\325"..., 384) = 384 [b76e7424] fcntl64(3, F_SETLKW, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0
Puoi osservare che ogni volta che viene chiamata la funzione malloc, chiama la chiamata di sistema brk(). La funzione della libreria getutxent chiama effettivamente la chiamata di sistema "open" per aprire "/var/run/utmp" e inserisce un blocco di lettura e legge il contenuto, quindi rilascia i blocchi.
Ora abbiamo confermato che il comando who ha letto il file utmp per visualizzare l'output.
Sia "strace" che "ltrace" hanno una serie di buone opzioni che possono essere utilizzate.
- -p pid – Si collega al pid specificato. Utile se il programma è già in esecuzione e vuoi conoscerne il comportamento.
- -n 2 – Rientra ogni chiamata nidificata di 2 spazi.
- -f – Segui il fork
Per saperne di più:7 esempi di Strace per eseguire il debug dell'esecuzione di un programma in Linux
III. Intercettare le chiamate in biblioteca
5. LD_PRELOAD &LD_LIBRARY_PATH
LD_PRELOAD ci permette di aggiungere una libreria ad una particolare esecuzione del programma. La funzione in questa libreria sovrascriverà la funzione della libreria effettiva.
Nota:non possiamo usarlo con i programmi impostati con il bit "suid".
Prendiamo il seguente programma.
#include <stdio.h> int main() { char str1[]="TGS"; char str2[]="tgs"; if(strcmp(str1,str2)) { printf("String are not matched\n"); } else { printf("Strings are matched\n"); } }
Compila ed esegui il programma.
# cc -o my_prg my_prg.c # ./my_prg
Stamperà "Le stringhe non sono abbinate".
Ora scriveremo la nostra libreria e vedremo come possiamo intercettare la funzione libreria.
#include <stdio.h> int strcmp(const char *s1, const char *s2) { // Always return 0. return 0; }
Compila e imposta la variabile LD_LIBRARY_PATH sulla directory corrente.
# cc -o mylibrary.so -shared library.c -ldl # LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
Ora verrà creato un file chiamato "library.so".
Imposta la variabile LD_PRELOAD su questo file ed esegui il programma di confronto delle stringhe.
# LD_PRELOAD=mylibrary.so ./my_prg
Ora stamperà "Le stringhe sono abbinate" perché utilizza la nostra versione della funzione strcmp.
Nota:se desideri intercettare qualsiasi funzione di libreria, la tua funzione di libreria dovrebbe avere lo stesso prototipo della funzione di libreria originale.
Abbiamo appena trattato le cose di base necessarie per decodificare un programma.
Per coloro che desiderano fare il passo successivo nel reverse engineering, la comprensione del formato di file ELF e del programma Assembly Language aiuterà in misura maggiore.