Linux x64: Anti-Debug Shellcode + execve

Nutro da tempo un debole per tutto il mondo che ruota attorno alle tecniche di anti-reversing, specie se applicate agli shellcode.

In questo campo c’è poco e le motivazioni sono diverse, in primis il fatto che i check anti-debug occupano memoria nello shellcode e quando si fa shellcoding la dimensione in byte è un fattore cruciale. Shellcode di grandi dimensioni possono richiedere multi-stage, egghunting e altre tecniche, impattando la scelta del giusto payload da adottare in fase di exploit e complicando, talvolta, l’exploitation.

Un po’ di tempo fa mi sono imbattuto nello shellcode, scritto nel lontanissimo 2006 da izik e che lo stesso autore spiegava come segue:

(linux/x86) anti-debug trick (INT 3h trap) + execve("/bin/sh", ["/bin/sh", NULL], NULL) - 39 bytes 

The idea behind a shellcode w/ an anti-debugging trick embedded in it, is if for any reason the IDS would try to x86-emulate the shellcode it would *glitch* and fail. This also protectes the shellcode from running within a debugger environment such as gdb and strace. 

How this works? the shellcode registers for the SIGTRAP signal (aka. Breakpoint Interrupt) and use it to call the actual payload (e.g. _evil_code) while a greedy debugger or a confused x86-emu won't pass the signal handler to the shellcode, it would end up doing _exit() instead execuve() 

 - izik 

Cercando in giro non c’era una riscrittura della tecnica su Linux x64 così mi sono detto, perché non scriverne uno io prendendo spunto dal codice di izik?

Sfida bella e interessante la tecnica usata, che fa uso dell’exception handling per far gestire al debugger un finto breakpoint (via SIGTRAP) in modo che vada a fare lo skip dello shellcode, che rimane in mano all’handler (sorpassato dal debugger perché specializzato nel gestire le eccezioni INT3) e non viene eseguito. Nel caso dell’esecuzione fuori dal debugger invece la SIGTRAP viene catturata dall’handler impostato dallo shellcode con conseguente innesco della execve.

Bene, ottimi i presupposti, ed imbattersi in qualcosa che ancora non esiste è sempre intrigante.

Quindi ho proceduto nel seguente modo:

  1. Ho analizzato nel dettaglio lo shellcode x86 di izik;
  2. Ho cercato la documentazione relativa all’implementazione dei Signal per capire il funzionamento;
    • Nella versione x86 l’exception handling è delegato a signal, nella versione x64 le eccezioni vengono invece gestite da sigaction.
Linux x86: sighandler_t signal(int signum, sighandler_t handler);
Linux x64: int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  1. Ho capito, a quel punto, quale fosse il problema e cosa avrei dovuto fare per poter scrivere lo shellcode:
    • Cambiare i registri utilizzati (Linux a 64 bit usa ovviamente un assembly differente confronto alla versione a 32 bit);
    • Cambiare le syscall e i relativi parametri;
    • Inizializzare correttamente le due strutture *act e *oldact per poter effettuare correttamente la chiamata, task non banale tenendo a mente di scrivere uno shellcode e quindi considerando tutta una serie di altri fattori come dimensione, caratteri NUL, ecc;
    • Modificare in parte la struttura base del codice di izik per adattarlo alle nuove caratteristiche richieste.

La struttura struct sigaction, che ricopre un ruolo fondamentale nel nuovo shellcode è definita come segue:

struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

In casi come questo, il mio approccio preferito è quello di prendere un codice di esempio della funzionalità, compilarlo e fare Reverse Engineering sul codice assembly dell’eseguibile ottenuto, per capire come il compilatore ha trasformato il mio codice ad alto livello. Solo a quel punto, nel caso in cui io voglia farne uno shellcode, inizio a riscrivere il codice per cercare di limitare uso di memoria e per renderlo Position Independent e NUL free.

Dopo alcuni tentativi sono riuscito a scrivere un POC funzionante per Linux x64.

Potete trovare lo shellcode su Packet Storm Security oppure a pagina 2 di questo articolo.

Il codice è sufficientemente commentato per permettere facile comprensione delle operazioni effettuate.

Per oggi è tutto, ma se avete domande o considerazioni potete commentare o scrivermi all’indirizzo email che trovate nella sezione About. 🙂

-bdev-