MazeGen, 2010-06-21 | Revize: 1.0 |
Obfuskace kódu je oblíbenou technikou pro ztížení jeho analýzy nebo jeho rozpoznání na základě vzorů, což dnes hojně využívá například malware nebo softwarové protektory spustitelných souborů.
Článek byl původně napsán pro prielom #25. Marek (ilm89) převedl článek do PDF.
Obfuskací kódu (angl. code obfuscation) se obecně označuje technika pro přetvoření kódu (i zdrojového) do formy těžko pochopitelné člověkem, ale i nějakým programem. V tomto kontextu ale půjde jenom o "rozbití" existujícího strojového x86 kódu, i když obfuskátory asemblerovských zdrojáků (za účelem obfuskovaného výsledného strojáku) existují taky (třeba PELock obfuscator). Anglicky se tomu občas říká i code morphing. Nebudeme se tedy zabývat generací úplně nového kódu např. na základě nějakých abstraktních vzorů.
Napsat i jednoduchý obfuskátor strojáku není úplně triviální záležitost, protože na začátku vyžaduje pokročilejší, alespoň statický disassembler, který nějak popíše vstupní instrukce, a na konci reasembler, který celý kód opraví a znovu sestaví do funkční podoby. K tomu, aby bylo možné kód bezpečně automaticky obfuskovat, musí být splněna řada podmínek (např. je potřeba najít všechna návěští v kódu, které musí být možné nakonec opravit). Už to je problém sám pro sebe, který navíc nemusí být řešitelný (viz Halting problem). Řeší se tak, že kód, který má být obfuskovaný, musí být už předem připraven, třeba tak, že obsahuje zvláštní nápovědu pro obfuskátor. To je ale téma na zvláštní článek.
K pochopení celého procesu a jednotlivých metod, jak kód "zamlžit", pomůže pohled do historie vývoje obfuskátorů, který prudce probíhal a dal se sledovat u vývoje klasických virů, napadajících spustitelné soubory. V tomto smyslu se často mluví o metamorfismu (angl. metamorphism), protože virus vlastně obfuskuje sama sebe.
Jedním z nejznámějších průkopníků metamorfních virů byl Z0MBiE. Popsal třeba podmínky, za kterých je možné bezpečně prohazovat instrukce, což je jedna z důležitých metod, jak se zbavit jak konstantních vzorů v kódu, tak ztížit pochopení algoritmu. Zmiňuje tuto posloupnost instrukcí:
[A] add eax, ecx [B] adc eax, edx [C] add ebx, ecx [D] adc ebx, edx
Instrukce B závisí na A
(ADC
testuje příznak CF
nastavený instrukcí
ADD
) a instrukce
D závisí na C. Současně
C nezávisí na A ani na
B, proto je lze prohodit (níž jsou další příklady):
[C] add ebx, ecx [D] adc ebx, edx [A] add eax, ecx [B] adc eax, edx
Aby mohl s kódem manipulovat, vytvořil XDE, eXtended Disassembler Engine, který je založen na ADE, který umožňuje počáteční disasembling a konečný zpětný asembling, což je jedna z nutností popsáných výše.
Metamorfismus později pěkně popsal Mental Driller.
Zmiňuje celý
proces, kterým musí kód projít, aby ho šlo modifikovat a aby
byl na konci stále funkční. Zmiňuje další důležitou techniku, rozložení
instrukce na více jiných, ale se stejným výsledkem, konkrétně
PUSH REG
na (níž jsou další ukázky):
push reg ; původní instrukce, např. push eax -> mov [mem], reg ; předem připravená dočasná paměť push [mem]
Do (automatické) analýzy takového kódu je potom navíc potřeba zahrnout nový odkaz do paměti, což ji zpomaluje a komplikuje.
Zatím byly zmíněny dvě metody obfuskace: Prohazování instrukcí (to zahrnuje i celé bloky instrukcí) a jejich rozklad. Dá se říct, že tyto jsou ty nejdůležitější, protože využívají pouze původních instrukcí a kvůli jejich komplikovanosti není lehké navrátit kód do původního stavu.
Další metoda, jejíž podstatou není přidávání nových
instrukcí, je změna toku řízení (angl. control-flow-graph
mutation). Jde prostě o "vytržení" kusu kódu, jeho přemístění
a slinkování s původním kódem pomocí instrukcí JMP
:
; původní kód mov al, [ebx] inc ebx ; tato instrukce bude přesunuta sub al, 32 ; obfuskovaný kód mov al, [ebx] jmp get_over get_back: sub al, 32 jmp skip ; přeskoč přesunutou instrukci get_over: inc ebx jmp get_back ; vrať se na původní místo skip:
Za splnění dalších podmínek lze využít i instrukce
CALL-RET
a tím vlastně vytvořit falešnou proceduru:
mov al, [ebx] call get_over sub al, 32 jmp skip get_over: inc ebx ret skip:
Další metody už přidávají nové instrukce:
Jde o přidávání nadbytečných skoků:
; původní kód mov al, [ebx] inc ebx sub al, 32 ; obfuskovaný kód mov al, [ebx] jc bebacksoon ; falešný skok, který se možná neprovede, nezáleží na tom get_back: inc ebx sub al, 32 jmp skip ; přeskoč přidaný skok zpět bebacksoon: jmp get_back ; pouze návrat na původní místo skip:
Nebo "zřetězení" existujícího skoku za účelem zakrytí skutečné cílové adresy:
; původní kód get_null: mov al, [ebx] inc ebx cmp al, 0 jne get_null ; obfuskovaný kód get_null: mov al, [ebx] jmp skip ; přeskoč přidaný skok na get_null xget_null: jmp get_null skip: inc ebx cmp al, 0 jne xget_null ; zřetězení s meziskokem
Nebo vytvoření skoku do instrukce, což dokáže zmást ty
disasemblery, které nedokáží disasemblovat na návěští
skoku, které je uvnitř už dříve disasemblované instrukce. Přidáme
např. operační kód B8
, který vytvoří instrukci
MOV EAX, původní_4_bajty
. Dál přidáme instrukci
skoku, která zajistí, že se spustí skutečná původní instrukce (ve
sloupci vlevo je strojový kód instrukcí):
; původní kód 8A83 00010000 mov al, [ebx+100] ; obfuskovaný kód EB 01 jmp $+3 ; přeskoč přidaný bajt 0xB8 B8 8A830001 mov eax, 100838A ; 8A, 83, 00, 01 = 4 bajty z původního kódu 0000 add [eax], al ; 0000 = zbývající 2 bajty ; jinak zapsáno EB 01 jmp skip B8 db 0B8 skip: 8A83 00010000 mov al, [ebx+100]
Takže jsme vlastně přidali jenom 3 bajty EB
,
01
, B8
před původní instrukci.
Pro zpomalení jak automatické analýzy (emulátoru), tak ručního prohledávání kódu obfuskátory vkládají kód, jenž ve výsledku nemá žádný význam. Třeba Z0MBiE proto vytvořil ETG, Executable Trash Generator, který měl za úkol zpomalit antivirové emulátory vytvořením spousty nicnedělajícího kódu, vloženého mezi dekryptor a samotný kód viru.
Takovým nicnedělajícím kódem může být třeba následující funkce, volaná z náhodně vybraných míst původního kódu:
do_nothing: push ebp mov ebp, esp ; simuluj vytvoření stack frame push ecx mov ecx, 0 ; nastav na nula, takže repe cmpsw ; REPE se neprovede ani jednou pop ecx pop ebp ; uvolni stack retn
Dál se dá uvažovat například o vkládání méně známých NOP
instrukcí jako FNENI
nebo FNDISI
a dalších
metodách.
Pro ilustraci všech uvedených metod budeme krok za krokem obfuskovat jednoduchou implementaci funkce strcpy ze standardní knihovny:
strcpy: push ebp mov ebp, esp ; parametry adresuj přes EBP mov edx, [ebp+08] ; zapamatuj si parametr r pro výstup mov esi, [ebp+0C] ; zdrojový řetězec cs mov edi, [ebp+08] ; cílový řetězec r copy: mov al, [esi] mov [edi], al ; kopíruj bajt po bajtu inc esi inc edi ; posuň ukazatele cmp al, 0 ; konec řetězce? jne copy mov eax, edx ; vrať r pop ebp ; obnov EBP ret
Začneme rozkladem instrukcí, který nám zvýší počet instrukcí pro aplikaci ostatních metod.
push ebp
- interně jde o dvě operace: snížení
ESP
o 4 bajty a vložení EBP
na
[ESP]
. Toho využijeme, ale celou operaci ještě víc
zamotáme. Nejdřív provedeme push
náhodně vybrané
konstanty, kterou potom přepíšeme registrem EBP
:
push ebp -> push 0F4DCE10 mov [esp], ebp
mov ebp, esp
- použijeme rozklad, který nejprve
dočasně modifikuje zdrojový registr ESP
náhodně vybraným
registrem, vloží ho do EBP
a potom opraví oba registry na
správné hodnoty. Poznámka: předpokládáme, že kód běží v user mode, kde
můžeme dočasně narušit hodnotu ESP
.
mov ebp, esp -> sub esp, ebx mov ebp, esp add ebp, ebx add esp, ebx
mov edx, [ebp+08]
a následující podobné
instrukce: pokud nemá obfuskátor informaci o tom, že zdrojová paměť je
zapisovatelná (jde o stack), nemůže použít řadu rozkladů, které
nejprve nějak modifikují zdrojovou paměť. Pořád ale existuje několik
možností, konkrétně využijeme možnosti postupného plnění registru
EDX
:
mov edx, [ebp+08] -> mov dx, [ebp+0A] ; vyšší WORD shl edx, 10 ; do vyššího WORD EDX mov dh, [ebp+09] mov dl, [ebp+08]
Instrukce SHL
na rozdíl od původního MOV
,
který rozkládáme, sice přepíše příznaky (jako CF
,
ZF
atd.), ale obfuskátor dokáže analýzou zjistit, že
příznaky lze přepsat, protože na nich žádná z následujících instrukcí
nezávisí (nenásleduje např. instrukce ADC
nebo
JZ
).
mov esi, [ebp+0C]
- použijeme třeba jednoduchý
push-pop
rozklad:
mov esi, [ebp+0C] -> push dword [ebp+0C] pop esi
mov edi, [ebp+08]
- můžeme použít např. načtení
adresy do náhodně vybraného registru (který si nejprve uložíme
a potom zase obnovíme):
mov edi, [ebp+08] -> push edx lea edx, [ebp+08] mov edi, [edx] pop edx
mov al, [esi]
- obfuskátor může využít faktu,
že nakonec je celý EAX
přepsán, takže nezáleží na původní
hodnotě v AH
:
mov al, [esi] -> mov ah, [esi] xchg ah, al
mov [edi], al
- pro vytvoření iluze závislosti
na předchozí hodnotě [EDI]
ji nejdřív nastavíme na
konkrétní hodnotu:
mov [edi], al -> mov byte [edi], 5F xor [edi], al xor byte [edi], 5F
inc esi
- pro ztížení analýzy použijeme konstantu ze
systémového registru, konkrétně CR0
, jehož spodních 16
bitů (registr MSW
) můžeme na libovolné úrovni oprávnění
číst instrukcí SMSW
. Využijeme faktu, že bit 0 (Protection
Enable) je v chráněném módu vždy nastaven (MSW
načteme do
náhodně vybraného registru, který napřed uložíme):
inc esi -> push ebp smsw bp and ebp, 1 add esi, ebp pop ebp
inc edi
- operaci zakryjeme odečtením náhodně
vybraného registru a jeho opětovným přičtením pomocí ADC
s nastaveným CF
:
inc edi -> sub edi, eax stc adc edi, eax
cmp al, 0
- nevýhoda instrukce CMP
je, že nezapisuje do cílového operandu, na rozdíl od
SUB
, takže není tolik možností, jak rozkládat. V tomto
případě ale dokáže obfuskátor zjistit, že hodnota AL
je ve
všech možných větvých kódu nakonec přepsána (na návěští
copy
je instrukce MOV AL
a dál v kódu
zase MOV EAX
), takže AL
může být před
testováním nějak upraven, aby konstanta (0) nebyla tak
viditelná. Zase využijeme náhodně vybraný registr:
cmp al, 0 -> add al, ch cmp al, ch
jne copy
- JNE
skočí na návěští, pokud
není nastaven ZF
. Protože ale předchází aritmetická
instrukce CMP
, můžeme výsledek testovat na
vyšší nebo nižší pomocí dvou instrukcí JA
a JB
. A ještě to vylepšíme inverzí na JNB
a pomocným skokem:
jne copy -> ja copy jnb finished jmp copy finished:
mov eax, edx
- budeme předpokládat, že
obfuskátor ví (např. díky tomu, že zná volací konvenci této funkce),
že může přepsat příznaky i registr EDX
(jde o volatile
registr). Potom jde tato instrukce rozložit třeba na:
mov eax, edx -> xor eax, edx xor edx, eax xor eax, edx
pop ebp
- POP
nejdřív vloží hodnotu do
EBP
a potom přičte k ESP
4 bajty. Samotné
přičtení provedeme malým trikem pro pobavení.
pop ebp -> mov ebp, [esp] pop dword [esp+4]
ret
- znovu předpokládáme, že obfuskátor zná volací
konvenci a tudíž ví, že registry EAX
, ECX
a EDX
může přepsat, a proto jde RET
nahradit
následující sekvencí.
ret -> pop ecx jmp ecx
Celá funkce vypadá v tuto chvíli takto:
strcpy: push 0F4DCE10 ; push ebp mov [esp], ebp sub esp, ebx ; mov ebp, esp mov ebp, esp add ebp, ebx add esp, ebx mov dx, [ebp+0A] ; mov edx, [ebp+08] shl edx, 10 mov dh, [ebp+09] mov dl, [ebp+08] push dword [ebp+0C]; mov esi, [ebp+0C] pop esi push edx ; mov edi, [ebp+08] lea edx, [ebp+08] mov edi, [edx] pop edx copy: mov ah, [esi] ; mov al, [esi] xchg ah, al mov byte [edi], 5F ; mov [edi], al xor [edi], al xor byte [edi], 5F push ebp ; inc esi smsw bp and ebp, 1 add esi, ebp pop ebp sub edi, eax ; inc edi stc adc edi, eax add al, ch ; cmp al, 0 cmp al, ch ja copy ; jne copy jnb finished jmp copy finished: xor eax, edx ; mov eax, edx xor edx, eax xor eax, edx mov ebp, [esp] ; pop ebp pop dword [esp+4] pop ecx ; ret jmp ecx
Pro zjednodušení se pokusíme prohazovat pouze sousedící instrukce, ne celé bloky instrukcí:
strcpy: push 0F4DCE10 A) mov [esp], ebp B) sub esp, ebx mov ebp, esp add ebp, ebx add esp, ebx
Tyto nejde prohodit, protože A zapisuje na paměť
adresovanou ESP
a současně B mění
ESP
.
B) sub esp, ebx mov ebp, esp add ebp, ebx C) add esp, ebx D) mov dx, [ebp+0A] D) shl edx, 10 mov dh, [ebp+09] mov dl, [ebp+08]
Tyto (C a D) prohodit jde, protože žádný z operandů instrukcí se nepřekrývá:
sub esp, ebx mov ebp, esp add ebp, ebx D) mov dx, [ebp+0A] D) shl edx, 10 C) add esp, ebx mov dh, [ebp+09] E) mov dl, [ebp+08] F) push dword [ebp+0C] ; původně mov esi, [ebp+0C] pop esi
Protože předpokládáme, že vrchol zásobníku (ESP
)
nekoliduje s ebp+08
ani s ebp+0C
(jinak
bysme nemohli rozložit původní MOV
z [ebp+OC]
na PUSH
), instrukce můžeme
prohodit:
mov dh, [ebp+09] F) push dword [ebp+0C] E) mov dl, [ebp+08] G) pop esi H) push edx lea edx, [ebp+08] mov edi, [edx] pop edx
Instrukce G a H prohodit nejde, protože obě manipulují se zásobníkem.
push edx lea edx, [ebp+08] mov edi, [edx] I) pop edx copy: J) mov ah, [esi] xchg ah, al
Instrukce I a J zase prohodit nejde, protože je mezi nimi návěští
skoku. Jinými slovy, POP EDX
se nemůže stát součástí
copy
smyčky, nebo MOV AH
z ní nemůže
vypadnout.
mov ah, [esi] K) xchg ah, al L) mov byte [edi], 5F xor [edi], al xor byte [edi], 5F
Přehodit K a L je možné, žádné z operandů instrukcí spolu nekolidují:
mov ah, [esi] L) mov byte [edi], 5F K) xchg ah, al xor [edi], al M) xor byte [edi], 5F N) push ebp N) smsw bp and ebp, 1 add esi, ebp pop ebp
Přehodit M a N je zase možné. XOR
sice na rozdíl od
PUSH
a SMSW
přepisuje příznaky, ale žádná
z následujících instrukcí na nich nezávisí.
xor [edi], al N) push ebp N) smsw bp M) xor byte [edi], 5F and ebp, 1 add esi, ebp O) pop ebp P) sub edi, eax P) stc adc edi, eax
Instrukce O a P spolu prohodit
můžeme, protože POP
nemění příznaky (ADC
závisí na příznaku CF
):
M) xor byte [edi], 5F and ebp, 1 add esi, ebp P) sub edi, eax P) stc O) pop ebp Q) adc edi, eax R) add al, ch cmp al, ch
Naopak instrukce Q a R nejde
prohodit, protože ADD
by změnila CF
.
add al, ch S) cmp al, ch T) ja copy T) jnb finished T) jmp copy finished: U) xor eax, edx xor edx, eax xor eax, edx
Žádnou z instrukcí S, T a U přehodit nejde, protože instrukce T mění řízení a navíc mezi T a U je návěští, přes které nelze nikdy instrukce přehazovat.
xor eax, edx xor edx, eax V) xor eax, edx W) mov ebp, [esp] W) pop dword [esp+4]
Instrukce V a W žádné z operandů nesdílejí, takže je prohodíme.
xor eax, edx xor edx, eax W) mov ebp, [esp] W) pop dword [esp+4] V) xor eax, edx X) pop ecx jmp ecx
Instrukce V a X spolu také můžeme ze stejného důvodu prohodit:
W) mov ebp, [esp] W) pop dword [esp+4] X) pop ecx V) xor eax, edx jmp ecx
A poslední instrukce JMP
je zase neprohoditelná.
A už to začíná vypadat pěkně:
strcpy: push 0F4DCE10 mov [esp], ebp sub esp, ebx mov ebp, esp add ebp, ebx mov dx, [ebp+0A] shl edx, 10 add esp, ebx mov dh, [ebp+09] push dword [ebp+0C] mov dl, [ebp+08] pop esi push edx lea edx, [ebp+08] mov edi, [edx] pop edx copy: mov ah, [esi] mov byte [edi], 5F xchg ah, al xor [edi], al push ebp smsw bp xor byte [edi], 5F and ebp, 1 add esi, ebp sub edi, eax stc pop ebp adc edi, eax add al, ch cmp al, ch ja copy jnb finished jmp copy finished: xor eax, edx xor edx, eax mov ebp, [esp] pop dword [esp+4] pop ecx xor eax, edx jmp ecx
Obfuskátor náhodně vybere části kódu pro přesun. S použitím
instrukcí JMP
nebývá problém, v případě instrukcí
závisejících na zásobníku nesmí být pro přesun použita instrukce
CALL
, která vkládá na zásobník návratovou
adresu. Je vidět, že došlo k docela slušné fragmentaci kódu:
strcpy: push 0F4DCE10 jmp moved1 ; instrukce mov [esp], ebp až back1: ; mov dx, [ebp+0A] přemístěny shl edx, 10 add esp, ebx mov dh, [ebp+09] push dword [ebp+0C] mov dl, [ebp+08] jmp skip_moved2 ; vytvoř prostor pro přemístěný kód moved2: smsw bp xor byte [edi], 5F and ebp, 1 add esi, ebp sub edi, eax stc retn skip_moved2: pop esi push edx lea edx, [ebp+08] mov edi, [edx] pop edx copy: mov ah, [esi] mov byte [edi], 5F xchg ah, al xor [edi], al push ebp call moved2 ; smsw bp až stc pop ebp adc edi, eax add al, ch cmp al, ch jmp moved3 ; instrukce ja copy a jnb finished back3: jmp copy moved1: ; za původní "jmp copy" šlo bezpečně vložit kód mov [esp], ebp sub esp, ebx mov ebp, esp add ebp, ebx mov dx, [ebp+0A] jmp back1 finished: xor eax, edx xor edx, eax jmp skip_moved3 ; vytvoř prostor pro přemístěný kód moved3: ja copy jnb finished jmp back3 skip_moved3: mov ebp, [esp] pop dword [esp+4] pop ecx xor eax, edx jmp ecx
Obfuskátor přidá jeden nadbytečný podmíněný skok
JNC
, dva skoky (CALL
a JMP
)
zřetězí a vytvoří několik skoků do instrukce.
Pro prohlížení skoků do instrukce v debuggeru může být potřeba vypnout analýzu, která je odhalí a kód zobrazí správně.
strcpy: jmp skip_new_byte db 0B8 ; vytvoří falešnou instrukci MOV EAX, xxxxxxxx skip_new_byte: ; (pohltí následující čtyři bajty) push 0F4DCE10 jmp moved1 bebacksoon: ; za nový "jmp moved1" šlo bezpečně vložit kód jmp goingback back1: shl edx, 10 add esp, ebx jmp skip_moved2_chain ; vytvoř prostor pro meziskok moved2_chain: jmp moved2 ; meziskok skip_moved2_chain: mov dh, [ebp+09] push dword [ebp+0C] mov dl, [ebp+08] jmp skip_moved2 moved2: smsw bp xor byte [edi], 5F and ebp, 1 add esi, ebp jnc bebacksoon ; vložený podmíněný skok goingback: ; (někdy se provede, někdy ne, nezáleží na tom) sub edi, eax stc retn skip_moved2: pop esi push edx jmp skip_new_word db 05, 12 ; vytvoří falešnou instrukci ADD EAX, xxxxxx12 skip_new_word: ; (pohltí následující tři bajty) lea edx, [ebp+08] mov edi, [edx] pop edx copy: mov ah, [esi] mov byte [edi], 5F xchg ah, al xor [edi], al push ebp ; call moved2 ; zřetězení skoku call moved2_chain pop ebp adc edi, eax add al, ch cmp al, ch jmp moved3 back3: ; jmp copy ; zřetězení skoku jmp copy_chain moved1: mov [esp], ebp sub esp, ebx jmp skip_new_dword db 66, 81, 34, 9C ; vytvoří falešnou instrukci XOR WORD [ESP+EBX*4], xxxx skip_new_dword: ; (pohltí následující dva bajty) mov ebp, esp add ebp, ebx mov dx, [ebp+0A] jmp back1 finished: xor eax, edx xor edx, eax jmp skip_moved3 moved3: ja copy jnb finished jmp back3 copy_chain: ; za nový "jmp back3" šlo bezpečně vložit meziskok jmp copy skip_moved3: mov ebp, [esp] pop dword [esp+4] pop ecx xor eax, edx jmp ecx
Nakonec obfuskátor vloží nicnedělající
kód. Použijeme falešnou proceduru zmíněnou výše, která bude vložená
na náhodně určené místo a náhodně volaná. Ještě ji ale vylepšíme
tím, že bude jakoby přijímat jeden parametr (který se předává přes
zásobník pomocí PUSH
). Tím bude náhodně zvolená hodnota
nebo registr.
strcpy: jmp skip_new_byte db 0B8 skip_new_byte: push 0F4DCE10 push edx ; náhodný parametr call do_nothing_2 ; volání falešné funkce jmp moved1 bebacksoon: jmp goingback back1: shl edx, 10 add esp, ebx jmp skip_moved2_chain moved2_chain: jmp moved2 skip_moved2_chain: mov dh, [ebp+09] push dword [ebp+0C] mov dl, [ebp+08] jmp skip_moved2 ; za nový "jmp skip_moved2" šlo bezpečně vložit falešnou funkci do_nothing_2: push ebp mov ebp, esp ; simuluj vytvoření stack frame push ecx mov ecx, 0 ; nastav na nula, takže repe cmpsw ; REPE se neprovede ani jednou pop ecx pop ebp ; uvolni stack retn 4 ; odstraň vstupní DWORD parametr moved2: smsw bp xor byte [edi], 5F and ebp, 1 add esi, ebp jnc bebacksoon push 1E3D22 ; náhodný parametr call do_nothing_2 ; volání falešné funkce goingback: sub edi, eax stc retn skip_moved2: pop esi push edx jmp skip_new_word db 05, 12 skip_new_word: lea edx, [ebp+08] mov edi, [edx] pop edx copy: mov ah, [esi] mov byte [edi], 5F xchg ah, al xor [edi], al push ebp call moved2_chain pop ebp adc edi, eax add al, ch cmp al, ch jmp moved3 back3: jmp copy_chain moved1: mov [esp], ebp sub esp, ebx jmp skip_new_dword db 66, 81, 34, 9C skip_new_dword: mov ebp, esp add ebp, ebx mov dx, [ebp+0A] jmp back1 finished: xor eax, edx push ebp ; náhodný parametr call do_nothing_2 ; volání falešné funkce xor edx, eax jmp skip_moved3 moved3: ja copy jnb finished jmp back3 copy_chain: jmp copy skip_moved3: mov ebp, [esp] pop dword [esp+4] push 80001 ; náhodný parametr call do_nothing_2 ; volání falešné funkce pop ecx xor eax, edx jmp ecx
Zdrojáky ve flat assembler syntaxi
a spustitelné soubory pro Windows i Linux jsou tady. Program nemá
pro zjednodušení žádný výstup, takže je potřeba ho natáhnout do
debuggeru. Pro ověření správného výstupu stačí provést step
over první instrukci CALL
a podívat se do dat,
jestli se překopírování povedlo.
strcpy_orig_win.asm, strcpy_orig_win.exe
strcpy_obfus_win.asm, strcpy_obfus_win.exe
strcpy_orig_nix.asm, strcpy_orig_nix
strcpy_obfus_nix.asm, strcpy_obfus_nix
fasm můžete stáhnout tady. Překlad přímo do spustitelné podoby jde prostě přes fasm source.asm.
Spousta metod obfuskace, které jde také automaticky provádět, nebyla zmíněna. Jde třeba o samomodifikující se kód (SMC), kdy kód sám vytváří následující instrukce, takže je téměř nemožné je zjistit statickou analýzou kódu:
; původní kód 8BC3 mov eax, ebx smc_me: 03C1 add eax, ecx ; SMC zajistí přidání instrukce ADD dynamicky: 8BC3 mov eax, ebx 66C705xxxxxxxx mov word [smc_me], 0C103 EB00 jmp $+2 smc_me: 0401 add al, 1 ; falešná instrukce, která bude přepsána
Skok jmp $+2
na následující
instrukci zajistí, že dojde ke znovunačtení kódu do cache procesoru,
jinak by skutečně mohlo dojít ke spuštění původní instrukce
add al, 1
.
Další zajímavou cestou pro zabránění statické analýzy je dynamická alokace paměti pro proměnné. V kapitole o prohazování instrukcí není zmíněno prohazování celých nezávislých bloků instrukcí, jako třeba prohození dvou funkcí.
Další oblíbenou a dnes často používanou ochranou kódu na úrovni instrukcí je tenký virtuální stroj. Představit si ho jde tak, že instrukce nejsou vykonávány přímo, ale vykonává je emulátor. Aby ale nebylo možné snadno získat a analyzovat původní kód, který emulátor spouští, tak jsou vstupní instrukce ve formátu x86 převedeny do virtuálního formátu, který zná jenom emulátor. Pro zjištění formátu instrukcí je potom potřeba analyzovat samotný emulátor. V kombinaci s obfuskací a proměnlivým formátem instrukcí jde o docela slušnou ochranu proti analýze. Virtualizace je ale téma na samostatný článek.
Jak je vidět, kromě prohazování instrukcí přidávají metody obfuskace spoustu instrukcí. Jak je to ale potom s rychlostí kódu? Na dnešních procesorech, na rozdíl od nějakého 80386, už nehraje samotný počet instrukcí takovou roli, snad jedině ve smyčkách, vykonávaných v řádu miliónů. Důležitějším faktorem je, jak program zachází s datovou cache procesoru, a to obfuskace většinou neovliňuje.
Pokud jste zkoušeli volání obfuskované funkce v debuggeru, určitě jste si všimli, že k určení, co vlastně dělá, není potřeba studovat kód, stačí sledovat paměť (analyzovat data). Obfuskace instrukcí není všemocná právě proto, že pracuje jenom na úrovni instrukcí. Jinak řečeno, v první řadě to musí být design, který je těžké analyzovat, obfuskace je až doplňující prvek.
Ideální by bylo mít nástroj, který pochopí, co vstupní kód dělá, a ten stejný algoritmus vygeneruje jednou z mnoha (tisíc, miliónů) možných cest. Implementovat ale něco takového je prakticky nemožné. Zajímavou cestou, jak se k tomu trochu přiblížit, je poskytnout obfuskátoru nějaké abstraktní vzory namísto konkrétního kódu, který by obfuskátor už generoval sám. Toto by ale bylo téma na jiný článek, protože jde o samostatnou metodu.
Pokračujte na diskuzním fóru.
2010-06-21 | 1.0 | Publikace kopie článku pro prielom | MazeGen |
(formáty dat odpovídají ISO 8601)