Zoom Icon

Anti Disassembling Techniques

From UIC

Anti Disassembling Techniques

Contents


Infos
Author: quequero
Email: Image:Que addr.gif
Website: http://quequero.org
Date: 25/04/2009 (dd/mm/yyyy)
Level: Luck and skills are required
Language: Italian Image:Flag_Italian.gif
Comments: Un saluto a tutte le persone che il sisma del 6 Aprile ci ha portato via.



Introduzione

Stavo curiosando tra i tutorial della UIC in cerca di spunti per sviluppare delle tecniche anti-disasm per ARM e mi sono accorto che... Non c'e' neanche un tutorial sulle tecniche anti-disasm per x86. Ok, dal momento che sto a letto malaticcio (e ho appena preso IDA 5.4 ;p) non mi posso esimere dal colmare questo vuoto, quindi oggi parleremo di tecniche che possono essere utilizzare per cercare di ingannare sia i disassembler che i debugger (o meglio... I disassembler dei debugger).


Tools

  • Mr. Brain
  • IDA
  • OllyDbg
  • Qualunque altro disassembler conosciate


Essay

Il compito del disassembler e' quello di trasformare il codice macchina in codice mnemonico, che risulta piu' semplice da interpretare per gli umani. In questo ambito possiamo identificare modalita' di funzionamento differenti che ci tornano utili per raggruppare i disassembler in due grandi famiglie:

  • Linear Sweep Disassemblers: disassemblano un'istruzione per volta
  • Recursive Traversal Disassemblers: iniziano a disassemblare dall'entry-point e proseguono fino alla prima istruzione di branch (jmp, jnz, jb etc...) per poi riprendere a disassemblare direttamente da quel punto.

Tra i Linear Sweep Disassemblers troviamo il buon vecchio SoftICE, objdump e WinDbg. Tra i Recursive Traversals Disassemblers troviamo invece la vecchia cara IDA, OllyDbg e l'ottimo PEBrowse. Per riuscire a confondere un disassembler dobbiamo capire come funziona, e gia' questa suddivisione ci da' un'idea generale del loro modus operandi. Ma dalla nostra abbiamo un'arma molto potente: su x86 le istruzioni non sono a lunghezza fissa (come accade su ARM e su tutti i RISC ad esempio) ma possono essere lunghe da un byte in su, una situazione che senz'altro ci da' spazio di manovra per confondere il motore di disassembly, e come se non bastasse nulla ci vieta di mescolare dati e codice. A conti fatti sembra che sia piu' semplice fare del buon codice offuscato che un buon disassembler, ed in effetti e' cosi' (che sorpresa eh ;p), per una serie di ragioni che vedremo a breve.

Junk Bytes

Confondere un disassembler significa, essenzialmente, fare in modo che interpreti uno o piu' byte di dati come l'inizio di un'istruzione. Ancora una volta siamo fortunati perche' su x86 gli opcode che rappresentano l'inizio di un'istruzione sono ben 248 su 256, percio' non avremo sicuramente problemi a trovare quello che fara' per noi. Come primo test proviamo ad inserire nel codice dei byte di spazzatura ed a vedere come reagiscono i disassembler, quindi prendiamo questo piccolo programma e compiliamolo:

int main(int argc, WCHAR* argv[]) {
    int a = 0;

    goto label;
    __asm _emit(0x83); // Junk byte
label:
    a++;
    return 0;
}

Per chi non la conoscesse, l'istruzione _emit serve ad inserire nel codice un byte che desideriamo e che in teoria dovrebbe rappresentare un opcode. Cominciamo ora il nostro studio: compiliamo il programma disabilitando le ottimizzazioni e la creazione delle info di debug (cosi' da non produrre il .pdb che tanto aiuterebbe la brava IDA ;p), disassembliamo poi il codice ottenuto con il Linear Sweep Disassembler di Visual Studio:

; Disassembly from Visual Studio
00401000 55               push   ebp  
00401001 8B EC            mov    ebp,esp
00401003 51               push   ecx  
00401004 C7 45 FC 00 00.. mov    dword ptr [a],0
0040100B EB 03            jmp    label (401010h)
0040100D EB 01            jmp    label (401010h) ; Le ottimizzazioni sono disabilitate
0040100F 83 8B 45 FC 83.. or     dword ptr [ebx-3F7C03BBh],1
00401016 89 45 FC         mov    dword ptr [a],eax
00401019 33 C0            xor    eax,eax
0040101B 8B E5            mov    esp,ebp
0040101D 5D               pop    ebp  
0040101E C3               ret

Come vedete la strategia di disassemblare un'istruzione per volta non consente di riconoscere il nostro junk byte come tale che quindi diviene parte di un'istruzione di or, il risultato del disassembler e' quindi sbagliato. Vediamo ora come si comporta un normale Recursive Traversal Path disassembler come IDA v5.4:

; Disassembly from IDA v5.4
.text:00401000            push    ebp
.text:00401001            mov     ebp, esp
.text:00401003            push    ecx
.text:00401004            mov     [ebp+a], 0
.text:0040100B            jmp     short label
.text:0040100D            jmp     short label
.text:0040100F            db 83h  ; Junk Byte
.text:00401010 label:     mov     eax, [ebp+a]
.text:00401013            add     eax, 1
.text:00401016            mov     [ebp+a], eax
.text:00401019            xor     eax, eax
.text:0040101B            mov     esp, ebp
.text:0040101D            pop     ebp
.text:0040101E            retn

La strategia adottata da disassembler piu' evoluti e' sicuramente piu' funzionale, il junk byte viene immediatamente identificato come tale, ed il risultato del disassembly e' corretto, ma in cosa differisce questo approccio dal primo? Nel caso di Visual Studio il disassembling e' un procedimento cieco che prende il primo byte e lo disassembla alla prima istruzione possibile, IDA invece e' in grado di identificare quelle porzioni di codice che non vengono mai toccate dal programma, e che quindi possono essere considerate come componenti estranei. Se vi state chiedendo come fa e' presto detto: viene calcolata la destinazione di tutte le istruzioni di branch, se un dato ramo di codice non viene raggiunto da nessun branch, allora viene marcato come estraneo. In effetto il nostro junk byte non viene raggiunto o richiamato da nessuno, quindi e' corretto isolarlo. Ma a questo punto dovrebbe esservi venuta un'ideuzza per confondere anche la bella IDA vero?

Opaque Predicates

Con questo termine Linn e Debray definirono un predicato che, in maniera non ovvia, torna sempre TRUE o FALSE. Come vedremo si tratta di un trucchetto efficace per confondere i Recursive Traversal Disassemblers, anche se torneremo a fine paragrafo sul discorso per una breve precisazione. Creare un predicato opaco e' un'operazione abbastanza semplice, guardate questo pezzetto di codice:

int main(int argc, WCHAR* argv[]) {
    int a = 0;

    if(!a)
        goto label; // Arriveremo sempre qui

    __asm _emit(0x83);
label:
    a++;
    return 0;
}

La variabile a viene inizializzata a 0 e mai piu' toccata, quindi il primo if() sara' vero e finiremo sempre sulla goto senza mai attraversare l'istruzione _emit. Questo e' un predicato opaco poiche' quello che viene tradotto in codice come un salto condizionale, e' a tutti effetti un jmp in piena regola. Disassembliamo nuovamente questo programma con IDA (il risultato che verrebbe con Visual Studio gia' lo conosciamo) ed esaminiamone il risultato:

; Disassembly from IDA v5.4
.text:00401000            push    ebp
.text:00401001            mov     ebp, esp
.text:00401003            push    ecx
.text:00401004            mov     dword ptr [ebp-4], 0
.text:0040100B            cmp     dword ptr [ebp-4], 0
.text:0040100F            jnz     short loc_401015
.text:00401011            jmp     short near ptr loc_401015+1
.text:00401013            db 0EBh
.text:00401014            db 1
.text:00401015 label:    
.text:00401015 83 8B 45.. or      dword ptr [ebx-3F7C03BBh], 1 ; Junk Byte
.text:0040101C            mov     [ebp-4], eax
.text:0040101F            xor     eax, eax
.text:00401021            mov     esp, ebp
.text:00401023            pop     ebp
.text:00401024            retn

Con questo semplicissimo trucco siamo riusciti a confondere anche IDA. Diamo invece uno sguardo al codice utilizzando l'ultima Beta disponibile di OllyDbg 2:

; Disassembly from OllyDbg 2.0 beta 2
00401000 55               push ebp
00401001 8B EC            mov ebp,esp
00401003 51               push ecx
00401004 C7 45 FC 00 00.. mov dword ptr ss:[ebp-4],0
0040100B 83 7D FC 00      cmp dword ptr ss:[ebp-4],0
0040100F 75 04            jne short 00401015
00401011 EB 03            jmp short label
00401013 EB 01            jmp short label
00401015 83               db 83                         ; Junk Byte
00401016 label:    
00401016 8B 45 FC         mov eax,dword ptr ss:[ebp-4]
00401019 83 C0 01         add eax,1
0040101C 89 45 FC         mov dword ptr ss:[ebp-4],eax
0040101F 33 C0            xor eax,eax
00401021 8B E5            mov esp,ebp
00401023 5D               pop ebp
00401024 C3               retn

A quanto pare, nonostante la differenza in termini di denaro OllyDbg sembra essere piu' intelligente di IDA :>. In effetti prima abbiamo detto che e' necessario creare un predicato opaco che non sia ovvio per il disassembler... Bene, cambiamo un pochino le carte in tavola e vediamo chi riesce a starci dietro, compiliamo il seguente codice:

int main(int argc, WCHAR* argv[]) {
    int a = 0;

    if(!a)
        goto label;
    else
        __asm _emit(0xB8); // Junk Instruction

    __asm _emit(0x18);     // Junk Instruction
    a >>= 0;               // Junk Instruction

label:
        return 0;
}

In questo caso abbiamo aggiunto un ramo else al predicato, in modo da costringere il motore di disassembly ad eseguire un'analisi piu' approfondita. Passiamo l'eseguibile ad IDA e vediamone il responso:

; Disassembly from IDA v5.4
.text:00401000            push    ebp
.text:00401001            mov     ebp, esp
.text:00401003            push    ecx
.text:00401004            mov     [ebp+a], 0
.text:0040100B            cmp     [ebp+a], 0
.text:0040100F            jnz     short loc_401017
.text:00401011            jmp     short label
.text:00401013            jmp     short label
.text:00401015            db 0EBh, 1
.text:00401017 loc_401017:
.text:00401017 B8 18 8B.. mov     eax, 0FC458B18h ; I nostri Junk Bytes
.text:0040101C            mov     [ebp+a], eax
.text:0040101F label:
.text:0040101F            xor     eax, eax
.text:00401021            mov     esp, ebp
.text:00401023            pop     ebp
.text:00401024            retn

IDA non riesce a disassemblare correttamente il codice, come vediamo viene messo in EAX un valore anomalo che poi viene spostato all'interno della variabile a, cosa che nel codice da noi scritto non succede. Tuttavia possiamo assistere alla risincronizzazione del motore, infatti vediamo che un'istruzione e' stata interpretata come dati, la successiva come codice (in maniera errata) e poi dalla terza in poi il disassembler e' stato capace di riprendere il normale flusso, infatti il codice successivo e' rappresentato correttamente. Vediamo ora OllyDbg cosa riesce a fare:

; Disassembly from OllyDbg 2.0 beta 2
00401000                  push ebp
00401001                  mov ebp,esp
00401003                  push ecx
00401004                  mov dword ptr ss:[a],0
0040100B                  cmp dword ptr ss:[a],0
0040100F                  jne short 00401017
00401011                  jmp short label
00401013                  jmp short label
00401015                  db EB
00401016                  db 01
00401017 B8 18 8B 45 FC   mov eax,FC458B18 ; Junk Bytes
0040101C                  mov dword ptr ss:[a],eax
0040101F label:           xor eax,eax
00401021                  mov esp,ebp
00401023                  pop ebp
00401024                  retn

A quanto pare anche OllyDbg e' caduto sotto i colpi della nostra ascia, questo ci fa ben sperare :) e se siete curiosi sappiate che anche il Linear Sweep Disassembler di Visual Studio presenta lo stesso identico risultato. In verita' dovremmo rimanere abbastanza sorpresi dal fatto che certi trick funzionino, abbiamo chiamato questi costrutti predicati opachi, ma e' evidente che non si tratta di predicati indecibili (se non a runtime), ma di predicati il cui risultato e' gia' deciso, ed in maniera piuttosto esplicita, gia' a compile time. Questo e' segno che il lavoro svolto dal disassembler e' estremamente gravoso e allo stato attuale nessun motore, tra quelli provati, e' in grado di seguire correttamente il flusso del programma. In sostanza possiamo dire che di opaco in questi predicati c'e' ben poco e l'occhio esperto del reverser sara' in grado di notare immediatamente il trucco. Nel caso appena visto potremmo fare anche di meglio, ad esempio inserendo dei junk byte che nel disassemblato non facciano apparire le altre istruzioni come data, questo e' solo un'esercizio ma l'idea di fondo resta la stessa: basta mettere il disassembler davanti ad un albero decisionale e potenzialmente saremo in grado di mandarlo in crisi, come se non bastasse i predicati appena visti possono essere complicati a piacimento :).

Fake Return Points

Un'altra strategia che confonde facilmente i disassembler e' quella che mi sono permesso di battezzare come Fake Return Points. I disassembler assumono infatti che l'istruzione eseguita al ritorno di una funzione sia quella immediatamente successiva alla CALL stessa, vediamo quindi cosa succede se quest'assunzione diviene errata.

int main(int argc, WCHAR* argv[]) {
    int a = 0;

    if(!a) {
        func();
        __asm _emit(0xB8);
        a += 3;
    }

    return 0;
}

Al momento cosa fa func() non e' importante, l'importante e' che ci sia una chiamata a funzione, vediamo il risultato di IDA (non riporto quello di Olly visto che e' identico):

; Disassembly from IDA v5.4
.text:00401040            push    ebp
.text:00401041            mov     ebp, esp
.text:00401043            push    ecx
.text:00401044            mov     [ebp+var_4], 0
.text:0040104B            cmp     [ebp+var_4], 0
.text:0040104F            jnz     short FakeBranch
.text:00401051            call    func
.text:00401056            mov     eax, 83FC458Bh
.text:0040105B            rol     byte ptr [ebx], 89h
.text:0040105E            inc     ebp
.text:0040105F            cld
.text:00401060 FakeBranch:
.text:00401060            xor     eax, eax
.text:00401062            mov     esp, ebp
.text:00401064            pop     ebp
.text:00401065            retn

Vediamo chiaramente che il disassembler si desincronizza all'indirizzo 00401056 e riprende a disassemblare correttamente soltanto all'indirizzo 00401060. La risincronizzazione e' dovuta solo al fatto che a quell'indirizzo c'e' la destinazione di un branch, altrimenti avrebbe continuato a disassemblare in maniera errata ancora per molti byte. Utilizzando questo approccio dobbiamo pero' stare molto attenti alla calling convention utilizzata, a questo scopo faccio una brevissima ma necessaria digressione.

Standard Calling Convention

Windows utilizza quella che e' stata ribattezzata Standard Calling Convention, sebbene non sia il vero standard (rappresentato dalla C Calling Convention, accettata da tutti fuorche' Microsoft) funziona in questo modo: i parametri della funzione vengono salvati sullo stack in right-to-left order e prima di tornare al chiamante i parametri utilizzati devono essere liberati. Questa convention conserva lo stato di tutti i registri ad eccezione di: EAX, ECX ed EDX (ed i relativi RAX, RCX ed RDX). Quindi dopo una chiamata _stdcall tutti i registri, ad eccezione di quelli appena nominati, saranno inalterati. Ovviamente dovendo liberare lo stack all'interno della chiamata, non e' possibile utilizzare questa convention per le funzioni che prendono un numero variabile di parametri (printf() su tutte). Guardiamo un esempio di chiamata a funzione con questa calling convention:

// stdcall
int __stdcall func(int a, int b) {
        // ...
}

int main(int argc, WCHAR* argv[]) {
        // ...
        int x = func(a, 7);
        // ...
}

Il disassemblato appare cosi':

; func()
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
...
.text:00401023                 mov     esp, ebp
.text:00401025                 pop     ebp
.text:00401026                 retn    8               ; Qui viene pulito lo stack

; main()
.text:00401046                 push    7               ; b
.text:00401048                 mov     ecx, [ebp+a]    ; a
.text:0040104B                 push    ecx
.text:0040104C                 call    func
.text:00401051                 mov     [ebp+x], eax

Lo stack viene ripulito all'interno di func() quindi possiamo inserire quello che vogliamo dopo la chiamata a func(), senza doverci preoccupare di compromettere il funzionamento del programma.

C Calling Convention

A differenza della precedente la _cdecl richiede che sia il chiamante, e non la funzione stessa, a liberare i parametri dallo stack dopo il ritorno dalla funzione. Come la _stdcall anche la _cdecl garantisce la conservazione dello stato di tutti i registri ad eccezione di: EAX, ECX ed EDX. Questa e' la convention utilizzata per default dalla suite di GCC/G++ ed in generale per tutte le chiamate (anche su Windows) che utilizzano un numero variabile di parametri. Ecco un esempio di codice:

// cdecl
int __cdecl func(int a, int b) {
        // ...
}

int main(int argc, WCHAR* argv[]) {
        // ...
        int x = func(a, 7);
        // ...
}

Ed ecco come appare il disassemblato:

; func()
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 push    ecx
...
.text:00401023                 mov     esp, ebp
.text:00401025                 pop     ebp
.text:00401026                 retn

; main()
.text:00401046                 push    7               ; b
.text:00401048                 mov     ecx, [ebp+a]    ; a
.text:0040104B                 push    ecx
.text:0040104C                 call    func
.text:00401051                 add     esp, 8          ; Qui viene pulito lo stack
.text:00401054                 mov     [ebp+x], eax

Nel main vediamo che dopo la chiamata a func() lo stack viene ripulito, in questo caso non possiamo utilizzare il trick del fake return point dal momento che non possiamo inserire codice immediatamente dopo la chiamata.

Fastcall Convention

Nelle fastcall i parametri delle funzioni vengono passati in ECX ed EDX, i restanti sullo stack nel solito right-to-left order. Come per la _stdcall anche nelle fastcall i parametri utilizzati sullo stack vengono liberati dal chiamante e non dalla funzione. Ricompiliamo un piccolo esempio:

int __fastcall func(int a, int b) {
        // ...
}

int main(int argc, WCHAR* argv[]) {
        // ...
        int x = func(a, 7);
        // ...
}

E guardiamone di nuovo il disassemblato:

; func()
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
...
.text:0040102B                 mov     esp, ebp
.text:0040102D                 pop     ebp
.text:0040102E                 retn                    ; Lo stack verrebbe pulito qui

; main()
.text:00401046                 mov     edx, 7          ; b
.text:0040104B                 mov     ecx, [ebp+a]    ; a
.text:0040104E                 call    func
.text:00401053                 mov     [ebp+x], eax

In questo caso i parametri sono soltanto due quindi vengono passati direttamente sui registri, se fossero stati tre uno sarebbe stato passato sullo stack che poi sarebbe stato ripulito direttamente all'interno di func().

C++ Calling Convention

Per il C++ abbiamo un'ulteriore convenzione dal momento che oltre ai soliti parametri, in maniera invisibile per lo sviluppatore viene sempre passato il puntatore al this. Non esistendo uno standard definito per questa convention, ogni compilatore utilizza la propria: Microsoft definisce la thiscall convention dove il this viene passato in ECX (per tutte le funzioni membro non statiche) ed il resto viene trattato ugualmente a una comune _stdcall. G++ invece passa il this sempre come primo parametro e tratta il resto come una comune _cdecl.

Torniamo ai Fake Return Points

E' quindi evidente che per utilizzare questo trick dobbiamo esplicitamente utilizzare una chiamata _stdcall, altrimenti il compilatore inserirebbe del codice dopo la funzione che non ci consentirebbe di utilizzare alcun trick anti-disassembly, poiche' non sarebbe possibile inserire dei junk bytes senza compromettere la funzionalita' del programma stesso. Nell'esempio che segue sposteremo il return point in avanti di un byte e modificheremo lo stato di una variabile globale. Anche qui fate attenzione perche' se compilate il programma con l'opzione per omettere il frame pointer non troverete il return point dentro EBP ma dentro ESP.

int g_Flag;

int __stdcall func() {
    int k;

    // Let's move the return point 1 byte ahead
    __asm add dword ptr[ebp + 4], 1;

    k = 1;
    g_Flag = k;
    return -k;
}

int main(int argc, WCHAR* argv[]) {
    int a = 0;

    if(!a) {
        func();
        __asm _emit(0xC7);
        a += g_Flag;
    } else {
        __asm _emit(0x0F);
    }

    a += g_Flag - 4;
    return 0;
}

Ecco IDA come disassembla il codice:

; Disassembly from IDA v5.4
.text:00401020            push    ebp
.text:00401021            mov     ebp, esp
.text:00401023            push    ecx
.text:00401024            mov     dword ptr [ebp-4], 0
.text:0040102B            cmp     dword ptr [ebp-4], 0
.text:0040102F            jnz     short FakeBranch
.text:00401031            call    func
.text:00401036            dw 8BC7h
.text:00401038            inc     ebp
.text:00401039            cld
.text:0040103A            add     eax, dword_403374
.text:00401040            mov     [ebp-4], eax
.text:00401043            jmp     short near ptr loc_401045+1
.text:00401045 FakeBranch:
.text:00401045            jnp     near ptr 40738458h
.text:0040104B            add     [ebx+448DFC55h], cl
.text:00401051            or      bh, ah
.text:00401053            mov     [ebp-4], eax
.text:00401056            xor     eax, eax
.text:00401058            mov     esp, ebp
.text:0040105A            pop     ebp
.text:0040105B            retn

L'istruzione subito dopo la chiamata viene disassemblata in maniera errata ed anche il resto fino al punto in cui il disassembler riesce a risincronizzarsi. Anche stavolta vediamo OllyDbg come si comporta:

; Disassembly from OllyDbg 2.0 beta 2
00401000                  push ebp
00401020                  push ebp
00401021                  mov ebp,esp
00401023                  push ecx
00401024                  mov dword ptr ss:[ebp-4],0
0040102B                  cmp dword ptr ss:[ebp-4],0
0040102F                  jne short FakeBranch
00401031                  call func
00401036                  db C7                 ; First Junk
00401037                  mov eax,dword ptr ss:[ebp-4]
0040103A                  add eax,dword ptr ds:[403374]
00401040                  mov dword ptr ss:[ebp-4],eax
00401043                  jmp short 00401046
00401045                  db 0F                 ; Second Junk
00401046 FakeBranch:      mov ecx,dword ptr ds:[403374]
0040104C                  mov edx,dword ptr ss:[ebp-4]
0040104F                  lea eax,[ecx+edx-4]
00401053                  mov dword ptr ss:[ebp-4],eax
00401056                  xor eax,eax
00401058                  mov esp,ebp
0040105A                  pop ebp
0040105B                  retn

Olly e' decisamente piu' attento in questo caso e non si lascia fregare facilmente, infatti e' in grado di identificare correttamente entrambi i junk byte. Suggerimento: provate a simulare l'epilogo di una funzione con la direttiva __asm all'interno di un predicato e vedete cosa accade dentro IDA.

Indirect Jumps

Se vogliamo vedere l'inferno in Terrra... Possiamo confondere ulteriormente le idee al disassembler utilizzando una vasta gamma di funzioni indirette insieme ai predicati opachi. Guardate questo esempio (ok potevo farlo in assembly, ma VS mi tornava piu' comodo ;p):

int main(int argc, WCHAR* argv[]) {
    int a = 0, mark = 0;

    if(!a) {
        __asm mov eax, label;
        __asm mov mark, eax;
    } else {
        __asm mov eax, main;
        __asm mov mark, eax;
    }

    if(a)
        __asm _emit (0xB8);

    __asm mov eax, mark;
    __asm jmp eax;           // Saltiamo a label

label:
    return 0;
}

Questa volta utilizziamo due variabili: a che viene utilizzata per il predicato opaco e mark che viene utilizzata per un jmp. Nel ramo del predicato che viene eseguito a runtime non facciamo altro che muovere dentro mark l'indirizzo di label. Nel ramo che invece non viene mai eseguito, copiamo dentro mark l'indirizzo del main. Poi aggiungiamo un secondo predicato opaco che semplicemente scrive il byte 0xB8, l'istruzione successiva carica in EAX il contenuto di mark e poi ci salta. Il disassembler non puo' sapere cosa conterra' EAX e quindi evitera' in toto di fare assunzioni a riguardo, infatti guardate cosa succede dentro IDA:

; Disassembly from IDA v5.4
.text:00401000            push    ebp
.text:00401001            mov     ebp, esp
.text:00401003            sub     esp, 8
.text:00401006            mov     dword ptr [ebp-4], 0
.text:0040100D            mov     dword ptr [ebp-8], 0
.text:00401014            cmp     dword ptr [ebp-4], 0
.text:00401018            jnz     short FakeBranch
.text:0040101A            mov     eax, (offset loc_401037+1)
.text:0040101F            mov     [ebp-8], eax
.text:00401022            jmp     short FakePredicate2
.text:00401024 FakeBranch:
.text:00401024            mov     eax, offset _main
.text:00401029            mov     [ebp-8], eax
.text:0040102C FakePredicate2:
.text:0040102C            cmp     dword ptr [ebp-4], 0
.text:00401030            jz      short near ptr loc_401032+1
.text:00401032 loc_401032:
.text:00401032            mov     eax, 0FFF8458Bh
.text:00401037 loc_401037:
.text:00401037            loopne  loc_40106C
.text:00401039            ror     byte ptr [ebx+3BC35DE5h], 0Dh
.text:00401040            add     [eax+0], dh
.text:00401042            inc     eax
.text:00401043            add     [ebp+2], dh
.text:00401046            rep retn

Ahhh dolore e sofferenza, vediamo subito che il disassembler inizia a vacillare all'indirizzo 0040101A, infatti dal suo punto di vista viene caricato in EAX un'indirizzo che appartiene al secondo byte di un'istruzione. Ma e' poco dopo il secondo predicato opaco che inizia il delirio: il disassembler perde completamente il filo, tant'e' che non riesce neanche piu' ad identificare il termine della funzione. Vediamo ora lo stesso codice come appare all'interno di OllyDbg:

; Disassembly from OllyDbg 2.0 beta 2
00401000                  push ebp
00401001                  mov ebp,esp
00401003                  sub esp,8
00401006                  mov dword ptr ss:[ebp-4],0
0040100D                  mov dword ptr ss:[ebp-8],0
00401014                  cmp dword ptr ss:[ebp-4],0
00401018                  jne short FakeBranch
0040101A                  mov eax,label
0040101F                  mov dword ptr ss:[ebp-8],eax
00401022                  jmp short FakePredicate2
00401024 FakeBranch:      mov eax,main
00401029                  mov dword ptr ss:[ebp-8],eax
0040102C FakePredicate2:  cmp dword ptr ss:[ebp-4],0
00401030                  je short 00401033            ; Jump to real code
00401032 B8 8B 45 F8 FF   mov eax,FFF8458B             ; Junk bytes + real code
00401037                  db E0                        ; Real byte interpreted as junk
00401038 label:           xor eax,eax
0040103A                  mov esp,ebp
0040103C                  pop ebp
0040103D                  retn

OllyDbg come sempre si comporta decisamente meglio, ma neanche lui e' riuscito ad identificare il jmp eax, pur riuscendo a recuperare la sincronizzazione molto prima di IDA che invece e' andata avanti per decine di byte (che non ho messo nel codice per brevita').

Jump Tables

Un'altra strategia utile sia contro il disassembling che contro il reversing in se e' quella di utilizzare le tecniche appena viste insieme a delle jump table. Una jump table (che poi e' ne' piu' ne' meno il risultato in asm di uno switch in C) consente di spezzare il flusso del programma e di confondere notevolmente il disassembler, vediamo subito un codice di esempio:

int main(int argc, WCHAR* argv[]) {
    int a = 0;
    int l1, l2, l3, lend, lloop;
    WCHAR t = 'A';

    __asm mov lloop, offset iloop;
    __asm mov lend, offset end;
       
    // Our tolower() version :)
    while(1) {
        switch(a) {
                case 0: // Is it a number?
                        __asm mov l1, offset label1;
                        __asm jmp l1;
                        __asm _emit(0xC7);

                case 1:
                        __asm _emit(0xE8);

                case 2: // Is it already lower case?
                        __asm mov l2, offset label2;
                        __asm jmp l2;
                        __asm _emit(0xC7);

                case 3:
                        __asm _emit(0xE8);

                case 4: // Let's make it lowercase!
                        __asm mov l3, offset label3;
                        __asm jmp l3;

                case 5:
                        __asm _emit(0xE8);

                default:
                        __asm _emit(0xB9);
                        break;
        }
iloop:
        a += 2;
    }

end:
    // tolower() end
    return 0;

label1:
    if(t >= '0' && t <= '9') {
        __asm jmp lend;
        __asm _emit(0xB8);
    } else
        __asm jmp lloop;

label2:
    if(t >= 'a' && t <= 'z')
        __asm jmp lend;
    else {
        __asm jmp lloop;
        __asm _emit(0xB8);
    }

label3:
    if(t < 'A' || t > 'Z') { // Invalid character
        __asm jmp lend;
        __asm _emit(0xB8);
    }

    t += 'a' - 'A';
    __asm jmp lend;
    __asm _emit(0x0F);

    return 0;
}

Ok prendetevi un momento di tempo per capire il codice, quella che vedete e' una funzione simile alla tolower(), in pratica data una lettera in ingresso ne torna il relativo carattere in lowercase, altrimenti il carattere stesso nel caso si tratti di un simbolo/numero o di una lettera gia' minuscola. La funzione attraversa tre step:

  1. Controlla che la lettera passata non sia un numero, in caso termina il ciclo
  2. Controlla che la lettera non sia gia' minuscola, in caso termina il ciclo
  3. Controlla che la lettera non sia un simbolo (in caso termina il ciclo) altrimenti la converte in lowercase

A livello logico possiamo identificare tre step separati, e quindi possiamo fare una cosa inutile e dannosa: separare questi tre step il piu' possibile, ma come? L'idea che mi e' venuta e' stata quella di inserire la routine all'interno di uno switch() che dispatcha l'esecuzione alla subroutine corretta leggendo il valore di un counter a, che viene incrementato ad ogni loop di 2. Quindi il programma entra nel while(), entra nello switch, lo switch porta l'esecuzione su una label che a sua volta torna nel ciclo. Tanto per capirci si tratta di un ciclo esteso verso l'esterno. Il tutto condito con dei predicati opachi rappresentati da dei case che di fatto non vengono mai eseguiti (quelli con numero dispari), e con dei junk byte posti al di sotto di ogni case che comunque non vengono mai eseguiti perche' l'istruzione precedente e' sempre un jmp (e questo non dovrebbe ingannare il disassembler). E' da notare che il target di ogni jmp all'interno dei case viene riempito a runtime e quindi il disassembler non e' (ma dovrebbe esserlo) in grado di stabilire gli indirizzi di destinazione. Vediamo IDA cosa ci tira fuori... Preparatevi spiritualmente:

; Disassembly from IDA v5.4
.text:00401000            push    ebp
.text:00401001            mov     ebp, esp
.text:00401003            sub     esp, 20h
.text:00401006            mov     [ebp+var_C], 0
.text:0040100D            mov     eax, 41h
.text:00401012            mov     [ebp+var_18], ax
.text:00401016            mov     [ebp+var_14], (offset loc_401066+1)
.text:0040101D            mov     [ebp+var_8], offset loc_401072
.text:00401024 loc_401024:
.text:00401024            mov     ecx, 1
.text:00401029            test    ecx, ecx
.text:0040102B            jz      short loc_401072
.text:0040102D            mov     edx, [ebp+var_C]
.text:00401030            mov     [ebp+var_20], edx
.text:00401033            cmp     [ebp+var_20], 5
.text:00401037            ja      short loc_401066
.text:00401039            mov     eax, [ebp+var_20]
.text:0040103C            jmp     ds:off_4010D8[eax*4]
.text:00401043
.text:00401043 loc_401043:
.text:00401043            mov     [ebp+var_4], offset loc_401076
.text:0040104A            jmp     [ebp+var_4]
.text:0040104D            db 0C7h, 0E8h, 0C7h
.text:00401050            inc     ebp
.text:00401051            in      al, 91h
.text:00401053            adc     [eax+0], al
.text:00401056            jmp     [ebp+var_1C]
.text:00401059            db 0C7h, 0E8h, 0C7h
.text:0040105C            inc     ebp
.text:0040105D            lock lodsb
.text:0040105F            adc     [eax+0], al
.text:00401062            jmp     [ebp+var_10]
.text:00401065            db 0E8h
.text:00401066 loc_401066:
.text:00401066            mov     ecx, 83F44D8Bh
.text:0040106B            rol     dword ptr [edx], 89h
.text:0040106E            dec     ebp
.text:0040106F            hlt
.text:00401070            jmp     short loc_401024
.text:00401072 loc_401072:
.text:00401072            xor     eax, eax
.text:00401074            jmp     short loc_4010D3
.text:00401076 loc_401076:
.text:00401076            movzx   edx, [ebp+var_18]
.text:0040107A            cmp     edx, 30h
.text:0040107D            jl      short loc_40108E
.text:0040107F            movzx   eax, [ebp+var_18]
.text:00401083            cmp     eax, 39h
.text:00401086            jg      short loc_40108E
.text:00401088            jmp     [ebp+var_8]
.text:0040108B            db 0B8h
.text:0040108C            jmp     short loc_401091
.text:0040108E loc_40108E:
.text:0040108E            jmp     [ebp+var_14]
.text:00401091 loc_401091:
.text:00401091            movzx   ecx, [ebp+var_18]
.text:00401095            cmp     ecx, 61h
.text:00401098            jl      short loc_4010A8
.text:0040109A            movzx   edx, [ebp+var_18]
.text:0040109E            cmp     edx, 7Ah
.text:004010A1            jg      short loc_4010A8
.text:004010A3            jmp     [ebp+var_8]
.text:004010A6            jmp     short loc_4010AC
.text:004010A8 loc_4010A8:
.text:004010A8            jmp     [ebp+var_14]
.text:004010AB            db 0B8h
.text:004010AC loc_4010AC:
.text:004010AC            movzx   eax, [ebp+var_18]
.text:004010B0            cmp     eax, 41h
.text:004010B3            jl      short loc_4010BE
.text:004010B5            movzx   ecx, [ebp+var_18]
.text:004010B9            cmp     ecx, 5Ah
.text:004010BC            jle     short loc_4010C2
.text:004010BE loc_4010BE:
.text:004010BE            jmp     [ebp+var_8]
.text:004010C1            db 0B8h
.text:004010C2 loc_4010C2:
.text:004010C2            movzx   edx, [ebp+var_18]
.text:004010C6            add     edx, 20h
.text:004010C9            mov     [ebp+var_18], dx
.text:004010CD            jmp     [ebp+var_8]
.text:004010D0            db 0Fh, 33h, 0C0h
.text:004010D3 loc_4010D3:
.text:004010D3            mov     esp, ebp
.text:004010D5            pop     ebp
.text:004010D6            retn

Ero gia' confuso quando ho scritto il codice, ora sono confuso ancora di piu' e a dirla tutta... Anche IDA non sembra avere le idee chiare :). OllyDbg al contrario offre un disassemblato piu' coerente e meno istruzioni vengono disassemblate in maniera errata.


Conclusioni

Ora avete tutti gli strumenti necessari per sviluppare tante belle tecniche anti-disasm, sperimentate l'inlining delle funzioni, l'outlining, l'interleaving e fate in modo di mettere il disassembler sempre davanti a delle scelte che non possono essere calcolate correttamente se non a runtime. Allo stato attuale i disassembler non sono ancora molto intelligenti, percio' ognuno di noi puo' trarre vantaggio dal fatto che un semplice predicato opaco puo' metterli in crisi praticamente tutti.

Un predicato realmente opaco e' possibile?
Per rispondere a questa domanda servirebbe una dimostrazione formale, non so se ne esistano gia' ma ad occhio (e potrei sbagliarmi) penso che non sia possibile se assumiamo che il programma finale sia comunque deterministico, analizziamo alcuni casi:

  1. Il predicato viene deciso dal valore di un parametro scritto nel codice -> Il predicato non e' realmente opaco perche il valore e' deducibile anche staticamente.
  2. Il predicato viene deciso da un valore ottenuto indirettamente, magari dal sistema (es: GetTickCount()) -> Il predicato e' opaco ma il programma non e' piu' deterministico.
  3. Il predicato viene deciso da un parametro indiretto del sistema (es: GetTickCount() ^ GetTickCount() o un valore impostato da un thread esterno) -> Il predicato non e' opaco perche' possiamo calcolare staticamente il codominio della funzione che manipola il parametro.

Se riuscite a trovare una falla nel ragionamento e avete un'idea di come ottenere un predicato realmente opaco fatemi sapere.

Quequero


Ringraziamenti & Bibliografia

  • Un saluto a tutti i miei conterranei Abruzzesi in questo momento di grande dolore, quello del 6 Aprile 2009 sara' un giorno duro da dimenticare.
  • Obfuscation of Executable Code to Improve Resistance to Static Disassembly, Linn/Debray.
  • Reversing Secrets of Reverse Engineering Code, Eldad Eilam
  • The IDA Pro Book


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.