Nebojte se reverzního inženýrství III.

S menším zpožděním zde máme další díl seriálu o reverzním inženýrství. Teorii věnujeme breakpointům, struktuře procedur a jako zákusek si řekneme něco ke komentářům v OllyDbg. Crackme již bude využívat reálnou techniku generování hesla a poukáže tak na zásadní problém spojený s tímto typem zabezpečení. Takže hurá do toho 🙂

V minulém díle jsme si popsali, z jakých sekcí se skládá standardní PE soubor, popsali si nejznámější volací konvence a crackli crackme s pevně daným heslem. Dnes si řekneme, co jsou to breakpointy a k čemu slouží, popíšeme si strukturu procedur/funkcí a ukážeme si, jak používat komentáře místo tužky a papíru (doufám, že mě old school reverzeři neumlátí debuggery, protože na tužku a papír nedají dopustit 🙂 ).

crackme_3
Zdrojové kódy crackme_3
Breakpointy
I když jsme v předchozích dílech zvládli cracknout dvě crackme, vystačili jsme si prakticky pouze se statickou analýzou bez potřeby krokování (postupné vykonávání instrukcí řádek po řádku). Bylo to částečně dáno rozsahem crackmes a částečně faktem, že jsme se nenořili do složitých cyklů a větvení, což se u velkých aplikací a hlavně virů, polymorfních a metamorfních enginů a crypterů běžně stává. Právě v těchto případech můžeme využít takzvaných breakpointů.
Máme dva základní druhy breakpointů: softwarové a hardwarové. Jak již název napovídá, softwarové breakpointy jsou vyvolány speciální instrukcí assembleru. Hardwarové jsou naopak přímo ‚zadrátované‘ v procesoru.

Softwarový breakpoint je zvláštním typ instrukce (lépe řečeno systémového přerušení), která způsobí přerušení toku vykonávání, pokud operační systém na tuto instrukci během vykonávání narazí. Operační systém zároveň spustí debugger a předá mu řízení na dané adrese, kde k vyvolání breakpointu došlo. Jeho hexadecimální hodnota (takzvaný opcode – operation code) je CCh a zápis v assembleru int3. V OllyDbg pro umístění breakpointu umístíme kurzor v levém horním okně na požadovaný řádek a klikneme myší dvojklikem na druhý sloupeček (sloupeček s opkódy), případně umístíme kurzor na požadovaný řádek a stiskneme klávesy F2. Že je breakpoint umístěný nám OllyDbg ohlásí červeným podbarvením adresového sloupečku v levém horním okně. Pokud budeme chtít breakpoint zrušit, postupujeme úplně stejně tak, jak jsme ho vyvolávali. Pokud je aplikace opravdu velká, může se stát, že ztratíme přehled, kde všude máme breakpointy umístěné. OllyDbg nám s tímto problémem výrazně pomůže, protože obsahuje tabulku všech nastavených breakpointů. Tabulku vyvoláme klávesovou zkratkou Alt + B (případně z menu View->Breakpoints). V tabulce vidíme, na které adrese a ve kterém modulu je breakpoint umístěn, zda je aktivní a jaká instrukce se na daném řádku nachází. Kliknutím na konkrétní řádek a stisknutím klávesy Del můžeme odstranit daný breakpoint rovnou z tohoto místa. Pokud na daný řádek dvakrát klikneme, OllyDbg přepne okno kódu na tuto adresu. Protože ne vždy je žádoucí hned odstraňovat každý aktuálně nepoužívaný/nechtěný breakpoint, můžeme v tabulce označením konkrétního řádku a stisknutím klávesy mezerník aktivovat či deaktivovat daný breakpoint, aniž bychom ho museli mazat.

crackme_3_1

Nastavený breakpoint a okno se seznamem breakpointů

Hardwarové breakpointy nemění kód aplikace v paměti (tak jako softwarové breakpointy, které přepisují instrukci na dané adrese instrukcí int3), ale jsou natvrdo ‚zadrátované‘ v procesoru. Každý procesor disponuje osmi registry pro hardwarové breakpointy, pro zadání adresy breakpointu slouží čtyři z nich. Z toho vyplývá, že můžeme mít v každý okamžik aktivní pouze čtyři hardwarové breakpointy. Na rozdíl od softwarových máme tři druhy hardwarových breakpointů v závislosti na tom, jakou událost chceme sledovat. Máme breakpoint na zápis dat do paměti, na čtení dat z paměti a na vykonávání. Hardwarový breakpoint nastavíme tak, že stiskneme pravé tlačítko myši na řádku, kde chceme umístit breakpoint a vybereme v kontextovém menu volbu Breakpoint->Hardware, on execution (Hardware, on access nebo Hardware, on write). V případě breakpointu na přístup a zápis je ještě třeba uvést velikost paměti, na kterou breakpoint aplikujeme. Byte znamená jeden bajt, word znamená dva bajty a dword pak čtyři bajty (možná to teď zní šíleně, ale představme si situaci, kdy chceme sledovat čtyři různé hodnoty, které jsou v paměti hned za sebou: místo čtyř jednobajtových breakpointů můžeme použít jeden čtyřbajtový). Na rozdíl od softwarových mohou být hardwarové breakpointy nastaveny rovněž v levém dolním okně v dump window. Jako v případě softwarových breakpointů mají i hardwarové svou vlastní tabulku ukazující jejich aktuální stav. Tabulku vyvoláme z menu přes Debug->Hardware breakpoints. Tato tabulka je rovněž jediným místem, odkud se dá hardwarový breakpoint odstranit/deaktivovat.

Možná se teď někdo podivuje, k čemu potřebujeme dva druhy breakpointů. Má to svůj význam. Hardwarových je příliš málo a softwarové zase modifikují kód programu v paměti, což se některým typům aplikací nemusí líbit (například aplikace, které náhodně kontrolují svůj kontrolní součet v paměti bude silně protestovat, jestliže do kódu umístíte softwarový breakpoint, čímž změníte jeden bajt v rámci celé binárky). Aby toho nebylo málo, řekneme si ještě o jednom speciálním druhu breakpointu. Jedná se o paměťový breakpoint. Ten slouží pro umístění na velké bloky paměti (celé stránky nebo regiony). Využití najde hlavně u self-modifying kódů nebo aplikací, které jsou kryptované/packované. Umístíme ho podobně jako softwarový. Jen v nabídce kontextového menu vybereme Breakpoints->Memory, on access (Memory, on write). Další možností je v Memory Map Window (pamatujete na zkratku Alt + M? 😉 ) vybrat celý paměťový segment, na který chceme breakpoint aplikovat a kliknutím pravým tlačítkem myši vyvolat kontextové menu, z něhož následně vybereme Breakpoins->Memory, on access (Memory, on write). V nakonec můžeme využít i Dump window, kde nejprve vybereme šipkou oblast paměti, kterou hodláme sledovat a následně opět přes kontextové menu vybereme typ breakpointu.

Problémem klasických breakpointů je fakt, že se aktivují vždy, když tok vykonávání programu doputuje na řádek, kde je breakpoint nastaven. Někdy nám stačí aktivovat breakpoint jen při určitých situacích (registr EAX obsahuje hodnotu menší než pět nebo registry ECX a ESI mají stejnou hodnotu). K tomu slouží takzvané podmíněné breakpointy. Jediný rozdíl oproti normálním softwarovým breakpointům je v tom, že při jejich aktivaci zároveň musíme nastavit i podmínku, při které se má breakpoint aktivovat. Podmíněný breakpoint nastavíme tak, že klikneme pravým tlačítkem myši na řádek, kde má být breakpoint umístěn a následně z kontextového menu vybereme volbu Breakpoints->Conditional a vyplníme podmínku (například EAX > 0). Opět můžeme tento typ breakpointu spravovat v tabulce breakpointů. A to je vlastně vše podstatné, co potřebujeme o breakpointech vědět.

Struktura procedur/funkcí
Procedury/Funkce jsou nedílnou součástí snad všech programovacích jazyků. Jejich účelem je koncentrovat stále se opakující bloky kódu do bloku jediného, čímž zajišťuje redukci velikosti kódu. Dalším účelem je členění rozsáhlého kódu do dílčích a tematických bloků, čímž zajišťuje snadnější čitelnost kódu pro programátory. Z pohledu assembleru je hlavním rozhodujícím faktorem volací konvence. Volací konvence jsme probrali v minulém díle. Nejčastěji se setkáváme s předáváním dat přes stack – hlavně konvence cdecl (C declaration) a stdcall. První jmenovaná je standardem pro deklaraci Cčkových funkcí, s druhou se pak setkáváme například u Win32 API funkcí. Zásadní rozdíl mezi těmito dvěma konvencemi je ve stylu úklidu stacku. Zatímco cdecl na úklid kašle (stack uklízí volající), stdcall je rozená uklízečka a co jí přes stack předáme, to si po sobě uklidí (stack uklízí volaný). V praxi to pak vypadá následovně:

CDECL

....
push 3
push 2
push 1
call funkce1
add ESP, 12
....

STDCALL

....
push 3
push 2
push 1
call funkce2
....

přičemž úklid stacku probíhá na úrovni funkce funkce2 pomocí instrukce RET a počtu bajtů, které mají být ze stacku odstraněny, tedy v tomto případě RET 12. Aby toho nebylo málo, standardně funkce obsahují takzvaný prolog a epilog. Prolog vytváří rámec funkce a epilog ho zase ruší. Rámec funkce slouží ke snadnějšímu přístupu k argumentům funkce na stacku a lokálním proměnným funkce, protože registr ESP se při běhu kódu různě mění. Z pohledu kódu vypadá prolog následovně:

push EBP
mov EBP, ESP

a epilog následovně:

mov ESP, EBP
pop EBP

Jednoduše řečeno: Dojde k zazálohování registru EBP na stack a nastavení registru EBP na hodnotu v registru ESP. Tím pádem máme v tento okamžik v registrech EBP a ESP stejné hodnoty určující vrchol zásobníku a zároveň rámec funkce. V případě epilogu se situace obrací. Bez toho, aniž bychom řešili, kde se nachází aktuální vrchol zásobníku, nastavíme registr ESP na hodnotu registru EBP, což je rámec funkce a původní vrchol zásobníku. Prolog a epilog může být z funkce vypuštěn, ale pak musí programátor velmi bedlivě kontrolovat redukci registru ESP na konci funkce, aby dosáhl požadované adresy a nedošlo tak k poškození běhu aplikace.

Komentáře
Komentáře jsou velmi mocným nástrojem a žádný reverser by je neměl opomínat. Jak jsem psal již v úvodu, old school reverzeři používají velmi často tužku a papír (upřímně, také po něm občas sáhnu, protože to má své kouzlo a výhody, ale i nevýhody). OllyDbg však poskytuje funkcionalitu komentářů, takže se obejdeme i bez tužky a papíru. Komentář je možné umístit na daný řádek tak, že stisknete klávesu středník. Pokud bych měl připojit pár tipů, k čemu komentáře používat, jmenoval bych v prvé řadě přepis algoritmu do podoby pseudokódu. Nejen že se výsledný kód snáze čte, je pak podstatně snazší z něj například přepsat algoritmus do vyšších jazyků. Klasickým příkladem může být keygenme, což je speciální druh crackme, který požaduje po řešiteli, aby vytvořil pro danou aplikaci generátor hesel, aby tak bylo možné jednoduše generovat hesla pro konkrétní loginy. Pokud to jen trochu jde, líní reverzeři takzvaně vyripují algoritmus v assembleru a zakomponují ho přímo do kódu ve formě buď inline assembleru (způsob vkládání kódu assembleru do kódu vyšších jazyků jako jsou C/C++ nebo Pascal/Delphi) nebo z něj udělají statickou knihovnu přímo v assembleru, přilinkují ho k jejich keygenu a pouze volají funkci. Na straně druhé jsou tady potom reverzeři, kteří mají rádi výzvy a pokusí se proto přepsat cílový algoritmus přímo do jimi použitého programovacího jazyka.

crackme_3_2

Komentáře vládnou světu :)

Dalším využitím jsou například komentáře důležitých podmínek podmíněných skoků. Zde si reverzer může popsat, co daná konstanta vyjadřuje, případně odkud došlo ke skoku na tento řádek kódu. Obecné pořekadlo, že méně je někdy více zde úplně neplatí, přesto je vhodné nepřehánět to s délkou komentářů jednotlivých řádků. Pokud potřebujeme obsáhleji pospat nějaký řádek, je vhodnější zvolit tužku a papír a v komentáři se pouze na tento papír odkázat.

Crackme
Dnešní crackme je zase o krůček složitější než ta předchozí, ale stále se jedná o velice jednoduchou záležitost. Kód je záměrně psán místy trochu chaoticky, aby nebylo crackme opět příliš přímočaré a čtenář tak byl donucen použít hlavu 🙂 Otevřeme si crackme v OllyDbg a podíváme se na něj.

00401000 >/$ 6A F5          PUSH -0B                                 ; /DevType = STD_OUTPUT_HANDLE
00401002  |. E8 D1010000    CALL <JMP.&kernel32.GetStdHandle>        ; \GetStdHandle
00401007  |. A3 96314000    MOV DWORD PTR DS:[403196],EAX
0040100C  |. 6A F6          PUSH -0A                                 ; /DevType = STD_INPUT_HANDLE
0040100E  |. E8 C5010000    CALL <JMP.&kernel32.GetStdHandle>        ; \GetStdHandle
00401013  |. A3 9A314000    MOV DWORD PTR DS:[40319A],EAX
00401018  |. 6A 00          PUSH 0                                   ; /pReserved = NULL
0040101A  |. 68 8E314000    PUSH crackme_.0040318E                   ; |pWritten = crackme_.0040318E
0040101F  |. 68 29000000    PUSH 29                                  ; |CharsToWrite = 29 (41.)
00401024  |. 68 00304000    PUSH crackme_.00403000                   ; |Buffer = crackme_.00403000
00401029  |. FF35 96314000  PUSH DWORD PTR DS:[403196]               ; |hConsole = NULL
0040102F  |. E8 B0010000    CALL <JMP.&kernel32.WriteConsoleA>       ; \WriteConsoleA
00401034  |. 6A 00          PUSH 0                                   ; /pReserved = NULL
00401036  |. 68 92314000    PUSH crackme_.00403192                   ; |pRead = crackme_.00403192
0040103B  |. 68 15000000    PUSH 15                                  ; |ToRead = 15 (21.)
00401040  |. 68 AB304000    PUSH crackme_.004030AB                   ; |Buffer = crackme_.004030AB
00401045  |. FF35 9A314000  PUSH DWORD PTR DS:[40319A]               ; |hConsole = NULL
0040104B  |. E8 8E010000    CALL <JMP.&kernel32.ReadConsoleA>        ; \ReadConsoleA
00401050  |. FF0D 92314000  DEC DWORD PTR DS:[403192]
00401056  |. FF0D 92314000  DEC DWORD PTR DS:[403192]
0040105C  |. 833D 92314000 >CMP DWORD PTR DS:[403192],3
00401063  |. 0F8E E9000000  JLE crackme_.00401152
00401069  |. 833D 92314000 >CMP DWORD PTR DS:[403192],0E
00401070  |. 0F87 DC000000  JA crackme_.00401152
00401076  |. 6A 00          PUSH 0                                   ; /pReserved = NULL
00401078  |. 68 8E314000    PUSH crackme_.0040318E                   ; |pWritten = crackme_.0040318E
0040107D  |. 68 12000000    PUSH 12                                  ; |CharsToWrite = 12 (18.)
00401082  |. 68 29304000    PUSH crackme_.00403029                   ; |Buffer = crackme_.00403029
00401087  |. FF35 96314000  PUSH DWORD PTR DS:[403196]               ; |hConsole = NULL
0040108D  |. E8 52010000    CALL <JMP.&kernel32.WriteConsoleA>       ; \WriteConsoleA
00401092  |. 6A 00          PUSH 0                                   ; /pReserved = NULL
00401094  |. 68 92314000    PUSH crackme_.00403192                   ; |pRead = crackme_.00403192
00401099  |. 68 64000000    PUSH 64                                  ; |ToRead = 64 (100.)
0040109E  |. 68 25314000    PUSH crackme_.00403125                   ; |Buffer = crackme_.00403125
004010A3  |. FF35 9A314000  PUSH DWORD PTR DS:[40319A]               ; |hConsole = NULL
004010A9  |. E8 30010000    CALL <JMP.&kernel32.ReadConsoleA>        ; \ReadConsoleA

Podobný kód jsme viděli v předchozím díle a proto nemá smysl ho znovu vysvětlovat. Za zmínku stojí pouze úsek mezi adresami 00401050h a 00401070h. Na adrese 403192h je uložen počet přečtených znaků z klávesnice. Tato hodnota je na adrese 00401050h snížena o jedna a na dalším řádku znovu o jedna. Takže je snížena o hodnotu dvě. Proč? Protože součástí čtecího procesu Win API funkce ReadConsole je i stisknutá klávesa enter, která je reprezentována dvěma znaky (pro programátory: jsou to znaky \r\n). Na dalším řádku je výsledná délka přečteného řetězce porovnána s hodnotou 3 a pokud je menší nebo rovna, skočí na dalším řádku na adresu 00401152h. Na adrese 0040105Ch se provede opět porovnání délky stringu, tentokrát s hodnotou 0Eh, tedy 14. Pokud je hodnota vyšší, dojde na dalším řádku ke skoku (opět) na adresu 00401152h. Pokud se na tuto adresu podíváme, uvidíme následující kód:

00401152  |> 6A 00          PUSH 0                                   ; /pReserved = NULL
00401154  |. 68 8E314000    PUSH crackme_.0040318E                   ; |pWritten = crackme_.0040318E
00401159  |. 68 39000000    PUSH 39                                  ; |CharsToWrite = 39 (57.)
0040115E  |. 68 72304000    PUSH crackme_.00403072                   ; |Buffer = crackme_.00403072
00401163  |. FF35 96314000  PUSH DWORD PTR DS:[403196]               ; |hConsole = NULL
00401169  |. E8 76000000    CALL <JMP.&kernel32.WriteConsoleA>       ; \WriteConsoleA
0040116E  \.^EB DB          JMP SHORT crackme_.0040114B

Pokud se podíváme, co je na adrese 00403072h, což je zásobník, který má být vypsán do konzole, uvidíme text:

00403070        4C 6F 67 69 6E 20 6D 75 73 74 20 68 61 76    Login must hav
00403080  65 20 6D 6F 72 65 20 74 68 61 6E 20 33 20 63 68  e more than 3 ch
00403090  61 72 73 20 61 6E 64 20 6C 65 73 73 20 74 68 65  ars and less the
004030A0  6E 20 31 35 20 63 68 61 72 73 00                 n 15 chars.

Víme tedy, že na řádcích 00401050h až 00401076h probíhá kontrola délky zadaného loginu. Ten musí mít mezi čtyřmi až čtrnácti znaky. Pojďme dále.

004010AE  |. BB AB304000    MOV EBX,crackme_.004030AB
004010B3  |. BA C0304000    MOV EDX,crackme_.004030C0
004010B8  |> 803B 00        /CMP BYTE PTR DS:[EBX],0
004010BB  |. 74 1F          |JE SHORT crackme_.004010DC
004010BD  |. 0FB603         |MOVZX EAX,BYTE PTR DS:[EBX]
004010C0  |. 68 8B314000    |PUSH crackme_.0040318B                  ; /Arg2 = 0040318B
004010C5  |. 50             |PUSH EAX                                ; |Arg1
004010C6  |. E8 B1000000    |CALL crackme_.0040117C                  ; \crackme_.0040117C
004010CB  |. 83C4 08        |ADD ESP,8
004010CE  |. 66:A1 8B314000 |MOV AX,WORD PTR DS:[40318B]
004010D4  |. 66:8902        |MOV WORD PTR DS:[EDX],AX
004010D7  |. 42             |INC EDX
004010D8  |. 42             |INC EDX
004010D9  |. 43             |INC EBX
004010DA  |.^EB DC          \JMP SHORT crackme_.004010B8

Na adrese 004030ABh je uložený námi zadaný login. Adresa 004030C0h je nějaký datový prostor, u kterého zatím nevíme, k čemu slouží. Kód kontroluje, zda není bajt na adrese uložené v EBX roven nule. Vzhledem k tomu, že již víme, že EBX obsahuje adresu námi zadaného loginu, je patrné, že kód kontroluje konec řetězce. Pokud na něj narazí, skočí za poslední řádek tohoto bloku. Tento bajt je uložen do registru EAX. Instrukce MOVZX je zvláštní případ instrukce MOV. Můžeme s její pomocí kopírovat obsah menšího registru do většího, přičemž rozdílová část velikosti obou registrů je nastavena na nuly. Tedy, v tomto případě je do registru EAX uložen jeden bajt. Protože má ale EAX bajty čtyři, jsou zbývající tři nastaveny na samé nuly, čímž je zajištěno, že bude v registru opravdu pouze naše hodnota, tedy jeden znak z loginu. Dále jsou na stack umístěny argumenty funkce. První je adresa dalšího neznámého prostoru, druhý je pak znak z loginu. Dochází k volání funkce na adrese 0040117Ch. Stack je redukován (takže předchozí funkce využívá volací konvenci cdecl) a do registru ax je uložena hodnota z adresy 0040318Bh, což je adresa předávaná do výše jmenované funkce. Tato hodnota je uložena na adresu v registru EDX. Jak víme z dřívějška, jedná se o nějaký paměťový prostor. Následně je hodnota registru EDX dvakrát zvýšena o jedna. Takže dojde k posunu adresy v registru EDX o dva bajty a dojde ke skoku zpět na adresu 004010B8h. Pojďme se ještě podívat na záhadnou funkci.

0040117C  /$ 55             PUSH EBP
0040117D  |. 8BEC           MOV EBP,ESP
0040117F  |. 56             PUSH ESI
00401180  |. 51             PUSH ECX
00401181  |. 8B75 0C        MOV ESI,DWORD PTR SS:[EBP+C]
00401184  |. 8B45 08        MOV EAX,DWORD PTR SS:[EBP+8]
00401187  |. B9 10000000    MOV ECX,10
0040118C  |. 46             INC ESI
0040118D  |> F6F1           /DIV CL
0040118F  |. 80FC 09        |CMP AH,9
00401192  |. 7F 05          |JG SHORT crackme_.00401199
00401194  |. 80C4 30        |ADD AH,30
00401197  |. EB 03          |JMP SHORT crackme_.0040119C
00401199  |> 80C4 57        |ADD AH,57
0040119C  |> 8826           |MOV BYTE PTR DS:[ESI],AH
0040119E  |. 4E             |DEC ESI
0040119F  |. 32E4           |XOR AH,AH
004011A1  |. 3C 00          |CMP AL,0
004011A3  |.^75 E8          \JNZ SHORT crackme_.0040118D
004011A5  |. C606 00        MOV BYTE PTR DS:[ESI],0
004011A8  |. 5E             POP ESI
004011A9  |. 59             POP ECX
004011AA  |. 8BE5           MOV ESP,EBP
004011AC  |. 5D             POP EBP
004011AD  \. C3             RETN

Zde vidíme prolog vytvářející rámec funkce. Na zásobník jsou uloženy hodnoty registrů ESI a ECX. Do ESI je uložen druhý argument a do registru EAX ten první. Proč je třeba se posunout o osm respektive o dvanáct bajtů, pokud chceme získat argumenty funkce? Protože na stacku jsou ještě další hodnoty. Konkrétně z prologu zazálohovaný registr EBP a pak návratová adresa, kterou na stack umisťuje volání instrukce call. Call vezme aktuální adresu v rámci kódu, přičte k ní délku instrukce call a získá adresu následujícího řádku za instrukcí call. Teprve až za touto návratovou adresou se ukrývají argumenty funkce. Dále je registr ECX nastaven na hodnotu 16. Hodnota v registru EAX je vydělena hodnotou v registru cl, tedy hodnotou 16. Z dřívějšího kódu (na adrese 004010BDh) víme, že do EAX je uložen jeden znak z loginu, takže hodnota tohoto znaku je vydělena hodnotou 16. V konzolových aplikacích jsou většinou textové řetězce reprezentovány jako posloupnosti hodnot ASCII znaků. Nejedná se tedy přímo o znaky, ale o jejich kódy. Takže v našem případě je hodnota v registru EAX vydělena hodnotou v registru cl. Ve skutečnosti je vydělena pouze hodnota v registru al hodnotou v registru cl (pokud někdo neví, proč to tak je, doporučuji detailně prostudovat dokumentaci, protože hned následující řádek už může být pro někoho matoucí). Dále dojde k porovnání hodnoty v registru ah s hodnotou 9. V registru ah najdeme zbytek po dělení na předchozí řádce. Protože jsme dělili číslem 16, hodnota registru ah bude v rozsahu 0 až 15. Zbytek po celočíselném dělení totiž udává, kolik musíme přičíst k výsledku celočíselného dělení, abychom dostali původní hodnotu. A právě tato hodnota je menší než 16. Pokud obsah registru ah porovnáváme s hodnotou 9 a víme, že v registru ah může být hodnota v rozsahu 0 až 15, můžeme směle předpokládat, že zde dochází ke kontrole, zda je daná hodnota číslo od 0 do 9. Potvrzené to ale zatím nemáme. Pojďme dále. Pokud je větší, odskakuje se na adresu 00401199h, pokud je menší nebo roven hodnotě 9, přičteme ke stávající hodnotě konstantu 030h a skočíme na adresu 0040119Ch. Na adrese 00401199h dochází k přičítání konstanty 057h k hodnotě v registru ah. Výsledná hodnota je následně uložena na adrese danou registrem ESI. Adresa v registru ESI je dekrementována a registr ah vynulován pomocí instrukce xor. Pak proběhne kontrola, zda je registr al nulový. Pokud ne, dojde ke skoku na adresu 0040118Dh, pokud ano, dojde k vložení hodnoty 0 na adresu danou registrem ESI. Pojďme si blíže ukázat, co se v této funkci děje. Dejme tomu, že na vstupu je znak R. Znak R má v ASCII tabulce hodnotu 052h. Pokud vydělíme hodnotu 052h hodnotou 010h, dostaneme 5, zbytek 2. Hodnotu 2 zkontrolujeme, zda je v rozsahu 0 až 9 a pokud ano, přičítáme k ní konstantu 030h. Konstanta 030h reprezentuje v ASCII tabulce znak 0 (nula). Pokud k hodnotě 030h přičteme hodnotu 5, získáme hodnotu 035h, což je hodnota 5 v rámci ASCII tabulky. To stejné provedeme tolikrát, dokud nebude hodnota v registru al rovna nula. Z toho vyplývá, že tuto operaci provedeme (téměř) vždy dvakrát (pokud někdo bude mít tendenci cpát do funkce hodnoty menší nebo rovnu 16, proběhne cyklus právě jednou). Funkce tedy převádí ASCII znak na jeho hexadecimální kód, který je uložen ve formě textu (lépe řečeno: jako posloupnost dvou tisknutelných znaků každý v rozsahu 0 až F). Teď možná někomu vrtá hlavou, co má znamenat konstanta 057h. To neprozradím a zadávám zájemcům jako domácí úkol zjistit to a případně se podělit v komentářích 🙂 Napovím jen, že je tam určitá spojitost s konstantou 030h 😉 Vraťme se na adresu 004010DCh.

004010DC  |> BE 25314000    MOV ESI,crackme_.00403125
004010E1  |. E8 8A000000    CALL crackme_.00401170
004010E6  |. 51             PUSH ECX
004010E7  |. B9 25314000    MOV ECX,crackme_.00403125
004010EC  |. C64408 FE 00   MOV BYTE PTR DS:[EAX+ECX-2],0
004010F1  |. 59             POP ECX
004010F2  |. 56             PUSH ESI
004010F3  |. BE 25314000    MOV ESI,crackme_.00403125
004010F8  |. E8 B1000000    CALL crackme_.004011AE
004010FD  |. 5E             POP ESI

Do registru se uloží adresa 00403125h. Pokud se na ni podíváme, zjistíme, že je nyní zcela prázdná, takže bude zřejmě teprve naplněna. Z dřívějšího rozboru víme, že na této adrese bude uložen login (viz volání Windows API funkce na adrese 0040109Eh). Na dalším řádku dochází k volání funkce na adrese 00401170h (viz níže). Následuje uložení hodnoty v registru ECX na stack a uložení adresy 00403125h do registru ECX. Na adresu danou součtem registrů EAX a ECX, od nichž je odečtena hodnota 2 je uložena hodnota 0. Následně je obnovena hodnota v registru ECX. Kód na řádcích 004010E6h a 004010F1h je standardní kód v případech, kdy se programátorům nedostává volných registrů. Rovněž je to ale také jedna z možností, jak udělat analýzu kódu výrazně složitější. Jak? Představme si, že mezi tyto dvě instrukce nacpe šílený programátor změť instrukcí mov, add, sub, shr, shl, call a jiné a ve všech případech se budou hodnoty ukládat pouze do registru ECX. Na konci se pak registr jednoduše obnoví a celý ten mumraj zcela pozbude na relevantnosti. Proto je občas dobré zapojit instinkty 🙂 A právě na dalších řádcích vidíme něco podobného. Teď si jen ověřit, zda se skutečně jedná o snahu znepříjemnit nám práci 🙂 Do registru ESI se uloží adresa 00403125h a volá se funkce na adrese 004011AEh. Následně se registr ESI obnoví. Pojďme se nejprve podívat na funkce na adrese 00401170h.

00401170  /$ 33C0           XOR EAX,EAX
00401172  |> 803C30 00      /CMP BYTE PTR DS:[EAX+ESI],0
00401176  |. 74 03          |JE SHORT crackme_.0040117B
00401178  |. 40             |INC EAX
00401179  |.^EB F7          \JMP SHORT crackme_.00401172
0040117B  \> C3             RETN

Registr EAX se vynuluje. Kontroluje se přítomnost hodnoty NULL na adrese dané součtem registrů EAX a ESI. Pokud je NULL bajt nalezen, dojde ke skoku na adresu 0040117Bh. V opačném případě dojde k inkrementování hodnoty v registru EAX a skoku zpět na adresu 00401172h. Zde nemusíme ani dlouze přemýšlet a vidíme, že se jedná o ekvivalent funkce strlen z jazyka C. Nyní se podíváme na druhou z funkcí, tedy na 004011AEh.

004011AE  /$ 53             PUSH EBX
004011AF  |. 51             PUSH ECX
004011B0  |. 52             PUSH EDX
004011B1  |. E8 BAFFFFFF    CALL crackme_.00401170
004011B6  |. 48             DEC EAX
004011B7  |. 33DB           XOR EBX,EBX
004011B9  |> 3BD8           /CMP EBX,EAX
004011BB  |. 7D 10          |JGE SHORT crackme_.004011CD
004011BD  |. 8A0C33         |MOV CL,BYTE PTR DS:[EBX+ESI]
004011C0  |. 8A1430         |MOV DL,BYTE PTR DS:[EAX+ESI]
004011C3  |. 881433         |MOV BYTE PTR DS:[EBX+ESI],DL
004011C6  |. 880C30         |MOV BYTE PTR DS:[EAX+ESI],CL
004011C9  |. 43             |INC EBX
004011CA  |. 48             |DEC EAX
004011CB  |.^EB EC          \JMP SHORT crackme_.004011B9
004011CD  |> 5A             POP EDX
004011CE  |. 59             POP ECX
004011CF  |. 5B             POP EBX
004011D0  \. C3             RETN

Na začátku máme zálohování tří registrů na stack a volání funkce na adrese 00401170h. Tu jsme rozebírali výše a víme, že se jedná o ekvivalent k C funkci strlen. Návratová hodnota z funkce je tedy délka řetězce uložená v registru EAX. Ta je následně dekrementována. Registr EBX je vynulován a dále porovnán s hodnotou v registru EAX. Pokud je rovna nebo větší, dojde ke skoku na adresu 004011CDh. Pokud je menší, jeden bajt z adresy dané součtem registrů ESI a EBX se uloží do registru cl. Stejně tak se uloží do registru dl jeden bajt z adresy dané součtem registrů ESI a EAX. Obsah registru dl je uložen na adresu danou součtem registrů EBX a ESI a obsah registru cl je uložen na adresu danou součtem registrů EAX a ESI. Registr EBX je inkrementován a registr EAX dekrementován. Následně dojde ke skoku na adresu 004011b9 a celý proces proběhne znovu. Jak vidno, dochází ve funkci k převrácení zadaného řetězce. Z textu ‚Ahoj‘ se po průchodu funkcí stane text ‚johA‘. To je vše 🙂 Opět se vrátíme, tentokrát na adresu 004010fd.

004010FD  |. 5E             POP ESI
004010FE  |. 68 C0304000    PUSH crackme_.004030C0                   ; /String2 = ""
00401103  |. 68 25314000    PUSH crackme_.00403125                   ; |String1 = ""
00401108  |. E8 DD000000    CALL <JMP.&kernel32.lstrcmpA>            ; \lstrcmpA
0040110D  |. 85C0           TEST EAX,EAX
0040110F  |. 74 1E          JE SHORT crackme_.0040112F
00401111  |. 6A 00          PUSH 0                                   ; /pReserved = NULL
00401113  |. 68 8E314000    PUSH crackme_.0040318E                   ; |pWritten = crackme_.0040318E
00401118  |. 68 22000000    PUSH 22                                  ; |CharsToWrite = 22 (34.)
0040111D  |. 68 50304000    PUSH crackme_.00403050                   ; |Buffer = crackme_.00403050
00401122  |. FF35 96314000  PUSH DWORD PTR DS:[403196]               ; |hConsole = NULL
00401128  |. E8 B7000000    CALL <JMP.&kernel32.WriteConsoleA>       ; \WriteConsoleA
0040112D  |. EB 1C          JMP SHORT crackme_.0040114B
0040112F  |> 6A 00          PUSH 0                                   ; /pReserved = NULL
00401131  |. 68 8E314000    PUSH crackme_.0040318E                   ; |pWritten = crackme_.0040318E
00401136  |. 68 15000000    PUSH 15                                  ; |CharsToWrite = 15 (21.)
0040113B  |. 68 3B304000    PUSH crackme_.0040303B                   ; |Buffer = crackme_.0040303B
00401140  |. FF35 96314000  PUSH DWORD PTR DS:[403196]               ; |hConsole = NULL
00401146  |. E8 99000000    CALL <JMP.&kernel32.WriteConsoleA>       ; \WriteConsoleA
0040114B  |> 6A 00          PUSH 0                                   ; /ExitCode = 0
0040114D  |. E8 80000000    CALL <JMP.&kernel32.ExitProcess>         ; \ExitProcess
00401152  |> 6A 00          PUSH 0                                   ; /pReserved = NULL
00401154  |. 68 8E314000    PUSH crackme_.0040318E                   ; |pWritten = crackme_.0040318E
00401159  |. 68 39000000    PUSH 39                                  ; |CharsToWrite = 39 (57.)
0040115E  |. 68 72304000    PUSH crackme_.00403072                   ; |Buffer = crackme_.00403072
00401163  |. FF35 96314000  PUSH DWORD PTR DS:[403196]               ; |hConsole = NULL
00401169  |. E8 76000000    CALL <JMP.&kernel32.WriteConsoleA>       ; \WriteConsoleA
0040116E  \.^EB DB          JMP SHORT crackme_.0040114B
crackme_3_3

Shodují se tyto dva řetězce? :)

Obnoví se adresa v registru ESI a dojde k porovnání dvou řetězců pomocí Windows API funkce lstrcmpA. Pokud se rovnají, dojde ke skoku na adresu 0040112f. Pokud ne, Dojde k vypsání textu do konzole pomocí Windows API funkce WriteConsoleA. Konkrétní text je na adrese 00403050.

00403050  4E 6F 2E 20 54 68 69 73 20 69 73 6E 27 74 20 67  No. This isn't g
00403060  6F 6F 64 20 70 61 73 73 77 6F 72 64 2E 2E 20 3A  ood password.. :
00403070  28 00                                            (.

Hmmm.. Tuhle hlášku asi nechceme vidět, co? 🙂 Následuje skok na adresu 0040114b, kde na nás čeká Windows API funkce ExitProcess a zmar. Takže teď už víme, že musíme projít testem řetězců na adrese 00401108, pokud chceme Crackme úspěšně dokončit a přistát na adrese 0040112F (viz podmíněný skok na adrese 0040110F). Pokud se totiž podíváme na adresu, kde je uložen text, který vypisuje, uvidíme následující:

00403030                                   47 6F 6F 64 20             Good
00403040  6A 6F 62 20 63 72 61 63 6B 65 72 21 20 3A 29 00  job cracker! :).

Když teď dáme naše poznatky dohromady a znovu si projdeme kód, řešení vyplave na povrch 🙂
Předně: Zadaný login musí mít mezi čtyřmi a čtrnácti znaky. Login je zpracován znak po znaku a je převeden na hexadecimální reprezentaci. Takto získaný textový řetězec je otočen a je následně porovnán se zadaným heslem. Z toho všeho nám vyplývá, že námi zadané heslo musí být hexadecimální reprezentací našeho loginu, které je zrcadlově obráceno. V případě loginu Duck je heslo b6365744. Zkusíme a zadáme: BAM!!
Good job cracker! 🙂

crackme_3_4

Tak a je vymalováno :)

Pro opravdu zapálené zájemce je zde další úkol: Napište funkci, která bude generovat pro zadaný login použitelné heslo. Řešení si můžete nechat pro sebe, poslat mi ho mailem nebo ho přiložit do komentářů.

Poděkování
MazeGen – za konzultaci a korekci

2 komentáře u „Nebojte se reverzního inženýrství III.

Napsat komentář

Vaše emailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *