(Snad nejen) První díl seriálu s názvem „Nebojme se shellcodů“ by měl čtenáře provést a zasvětit do tajů tvorby shellcodů pro platformu Win32 NT na procesorech rodiny x86. V prvním díle se čtenář dozví něco málo o historii, zjistí, co vlastně shellcode je, a na závěr si jeden ukázkový vytvoří.

Obsah:
——————————————
i. Úvod
ii. Historie
iii. Obecný popis
iv. Potřebné vybavení
v. Začínáme
vi. Problém zvaný NULL bajt
vii. Výsledek

Odkazy
——————————————

i. Úvod
O shellcodech již bylo napsáno skutečně mnoho knih. V českém/slovenském jazyce však mnoho knižních titulů nebo článků nenajdeme. Tento seriál by měl mít za úkol seznámit čtenáře se základními informacemi z tohoto velice zajímavého a plnohodnotného oboru, jakým vývoj a psaní shellcodů dozajista je. Tvůrčí myšlení a kreativita zde mají úplnou přednost a zaručují tvořit opravdu skvělá a zajímavá dílka.

Zatímco se celosvětově problematika velikosti a efektivnosti kódu vytrácí pod záminkou výkonů a kapacity současných počítačů, zde se cení každý ušetřený bajt, který neohrozí funkčnost shellcodu, což může být někdy hodně náročné (jinými slovy: člověk se snaží pomocí optimalizace a nejrůznějších hacků dosáhnout dokonalosti svého řešení). Protože o *NIX-like shellcodech se všeobecně píše daleko častěji, rád bych se zaměřil na Win32 shellcody, jejichž vývoj je v mnoha ohledech odlišný.

ii. Historie
Shellcody/Shellkódy vznikly jako reakce na potřebu vytvořit malý/krátký kód využitelný při zneužívání chyb typu buffer overflow (přetečení zásobníku). První shellcody představil pravděpodobně Elias Levy vystupující pod nickem „Aleph One“ v dnes již legendárním článku[1] publikovaném v populárním e-zinu PHRACK.

První shellcody měly za úkol, jak již název napovídá, spustit shell s co možná nejvyšším oprávněním (nejlépe s právy roota). S postupem času (a se stále se stupňujícím využíváním/zneužíváním shellcodů ve virech a červech) docházelo k jeho modifikacím a rozšíření vlastností od otevírání a naslouchání na konkrétním portu, přes navázání reverzního spojení, až například po stáhnutí a vykonání definované binárky. Od prvotních nevinných pokusů jedinců se tvorba shellcodů vyvinula v obor, který má svá pravidla, specifikace a doporučení, která by měla být dodržena v zájmu funkčnosti a bezpečnosti.

iii. Obecný popis
Každý, kdo se setkal s shellcody si jistě všiml, že jsou distribuovány ve formě podivně vyhlížejícího textového řetězce, který na první pohled neříká nic o tom, co vlastně máme před sebou.

Obecně, shellcode je kód, většinou napsaný v jazyku symbolických adres (assembleru), tvořený jednotlivými instrukcemi, tvořený tzv. opkódy (opcodes), což není nic jiného než reprezentace jednotlivých instrukcí, registrů, adres, hodnot a skoků (nejčastěji) v hexadecimální soustavě. Shellcode je při exploitaci vložen na zásobník, kde čeká do okamžiku, než je na něj přesměrováno zpracování a ten se následně vykoná.

Protože je většinou využíván při exploitaci vstupních dat textového typu (chápej: vstupů přijímajících jako vstupní data textové řetězce), je obecným (nikoliv však nezbytným) doporučením, aby neobsahoval tzv. NULL bajty (v ASCII tabulce má hodnotu 0, hexadecimální reprezentace je 0x00 nebo \x00). Důvod je evidentní na první pohled: Každý textový řetězec je ukončen právě NULL bajtem. Pokud by shellcode obsahoval byť jediný NULL bajt, došlo by v lepším případě k pádu aplikace, v horším případě k zacyklení nebo dokonce zaseknutí celého systému vlivem předčasného ukončení vykonávání shellcodu.

iv. Potřebné vybavení
Při psaní shellcodů budeme potřebovat několik aplikací. Předpokládám znalosti assembleru alespoň na základní úrovni (doporučená literatura[2]), protože nemám v úmyslu psát příručku k assembleru. Nějaké vývojové prostředí: např.: NASM[3], MASM[4] (používám já), TASM, Céčkový kompiler (pokud máte v plánu vyvíjet shellcody v inline assembleru) např.: Visual Studio[5], Code::Blocks[6], Dev-Cpp[7]. Dále se hodí nějaký debugger: např.: OllyDbg[8], IdaPro[9], Immunity Debugger[10], WinDbg[11]. Občas se hodí i programátorská kalkulačka, ale bez té se dá přežít. Vhodnou Win32 API dokumentaci[12]. Občas se může hodit znát adresy WIN API funkcí ve vašem systému. Můžete provést dump vaší paměti a dohledat si příslušné informace (např.: pomocí WinDbg) a nebo použít třeba jednoduchý program[13]. A v neposlední řadě kreativního ducha a (občas) pevné nervy 🙂 To je vše 😉

v. Začínáme
Ještě v dnešním dílu si vytvoříme první funkční shellcode, který pomocí Win API funkce Sleep „zamrzne“ na danou dobu a pak se regulérně ukončí pomocí funkce ExitProcess. Náš kód by vypadal v jazyce C asi takhle:

1
2
3
4
5
6
7
8
#include <windows.h>
 
#define KONSTANTA 5000 /* doplň časový interval v milisekundách, třeba 5000 */
 
int main(){
	Sleep(KONSTANTA);
	ExitProcess(0);
}

Jak je vidět, kód je opravdu jednoduchý. Když teď celý kód přepíšeme pomocí assembleru, dostaneme třeba následující kód:

1
2
3
4
5
6
7
mov eax, 5000 	 ; pocet milisekund
push eax
call Sleep
 
mov eax, 0		 ; navratova hodnota programu
push eax
call ExitProcess

Ani v assembleru nevypadá kód nijak složitě. Když si teď kód přeložíme a podíváme se na něj v debuggeru uvidíme něco takového:

1
2
3
4
5
6
7
8
00401000 >/$ B8 88130000    MOV EAX,1388
00401005  |. 50             PUSH EAX                                 ; /Timeout => 5000. ms
00401006  |. E8 11000000    CALL <JMP.&kernel32.Sleep>               ; \Sleep
0040100B  |. B8 00000000    MOV EAX,0
00401010  |. 50             PUSH EAX                                 ; /ExitCode => 0
00401011  \. E8 00000000    CALL <JMP.&amp;kernel32.ExitProcess>         ; \ExitProcess
00401016   .-FF25 04204000  JMP DWORD PTR DS:[<&kernel32.ExitProcess>;  kernel32.ExitProcess
0040101C   $-FF25 00204000  JMP DWORD PTR DS:[<&kernel32.Sleep>]     ;  kernel32.Sleep

Sleep_1

Ti všímavější již jistě postřehli, že tady něco nehraje. A to konkrétně adresa Win API funkcí. V binárkách (executable files) jsou totiž uloženy adresy funkcí v tzv. Import Address Table (IAT). Tato tabulka se nachází v PE hlavičce EXE souboru a je vytvářena během kompilace. Při spuštění se pak tato tabulka v paměti doplní o odkazy na jednotlivé funkce. Z toho vyplývá, že při volání některé z funkcí je z této tabulky vybrána skutečná adresa funkce v paměti a na tuto adresu se odskakuje (viz. JMP DWORD PTR DS:[<&kernel32.Sleep>]). Za předpokladu, že bychom měli jistotu, že se v exploitovaném programu skutečně požadovaná funkce nachází, mohli bychom ji volat přímo. Ovšem to se stává málokdy. Proto je jednodušší si zajistit přístup k těmto funkcím osobně. To není problém 🙂 Buď si můžeme dumpnout paměť pomocí WinDbg, najít si relativní virtuální adresu (RVA) dané funkce a tu pak přičíst k základní adrese, na kterou je knihovna nahrána, nebo použít jednoduchý program[13], který nám vrátí přímo adresu funkce v paměti.

Například funkce Sleep z kernel32.dll je na mých Windows XP SP3 CZ na adrese 07c802446h a funkce ExitProcess taktéž z kernel32.dll je na adrese 07c81cb12h. Nutno podotknout, že s každou úpravou těchto dynamicky linkovaných knihoven se mění i samotná adresa každé z těchto funkcí. Proto se shellcody většinou píší pro konkrétní verzi Windows, pro konkrétní Service Pack a pro konkrétní jazykovou mutaci. Když teď vezmeme tyto adresy a zaměníme je za názvy volaných funkcí dostaneme následující kód:

1
2
3
4
5
6
7
8
9
mov eax, 5000
push eax
mov ebx, 07c802446h
call ebx
 
mov eax, 0
push eax
mov ebx, 07c81cb12h
call ebx

A když si program prohlédneme v debuggeru uvidíme něco podobného:

1
2
3
4
5
6
7
8
00401000 > $ B8 88130000    MOV EAX,1388
00401005   . 50             PUSH EAX                                 ; /Timeout => 5000. ms
00401006   . BB 4624807C    MOV EBX,kernel32.Sleep                   ; |
0040100B   . FFD3           CALL EBX                                 ; \Sleep
0040100D   . B8 00000000    MOV EAX,0
00401012   . 50             PUSH EAX                                 ; /ExitCode => 0
00401013   . BB 12CB817C    MOV EBX,kernel32.ExitProcess             ; |
00401018   . FFD3           CALL EBX                                 ; \ExitProcess

Sleep_2

vi. Problém zvaný NULL bajt
To už vypadá mnohem nadějněji 🙂 Ale pokud si dobře vzpomínáte, shellcode nesmí obsahovat NULL bajty a my jich tam máme hned 6!! Musíme se jich nějak zbavit.

Existují dvě základní koncepce jak se takových NULL bajtů zbavit. První používá základní aritmetické operace (+,- , *, /) a bitové posuny vpravo a vlevo. V našem případě bychom například do eax místo hodnoty 5000d (01388h) mohli uložit hodnotu vyšší (např.: 010101388h) a následně ji nejdříve bitově posunout doleva a hned zpět doprava:

1
2
shl eax, 16
shr eax, 16

Druhá koncepce je mnohem přívětivější. Použít pouze tak veliký registr, jaký skutečně potřebujeme. Tedy:

1
2
3
4
xor eax, eax
mov ax, 5000	; do ax se vleze až 16-bitová hodnota
push eax
....

Což bude v debuggeru vypadat následovně:

1
2
3
4
00401000 > $ 33C0           XOR EAX,EAX
00401002   . 66:B8 8813     MOV AX,1388
00401006   . 50             PUSH EAX                                 ; /Timeout
....

Už jsme na dobré cestě! 🙂 Teď ještě druhou posloupnost NULL bajtů v případě argumentu funkce ExitProcess.

1
2
3
xor eax, eax
mov al, 0		; do al se vleze až 8-bitová hodnota
....

Ale pozor! Tady nám NULL bajt zůstane při každém pokusu o vložení hodnoty 0 do registru al. Vypomůžeme si instrukcí xor, která provádí exkluzivní součet dvou hodnot. Pokud jsou dvě xorované hodnoty stejné (mají stejnou hodnotu), pak je výsledkem operace hodnota 0. Proto nám bude stačit pouze odmazat mov al, 0. Výsledný kód pak vypadá následovně:

1
2
3
4
5
6
7
8
9
10
xor eax, eax
mov ax, 5000
push eax
mov ebx, 07c802446h
call ebx
 
xor eax, eax
push eax
mov ebx, 07c81cb12h
call ebx

vii. Výsledek
Nyní náš kód neobsahuje žádný NULL bajt a vypadá skvěle 🙂 V debuggeru:

1
2
3
4
5
6
7
8
9
00401000 > $ 33C0           XOR EAX,EAX
00401002   . 66:B8 8813     MOV AX,1388
00401006   . 50             PUSH EAX                                 ; /Timeout
00401007   . BB 4624807C    MOV EBX,kernel32.Sleep                   ; |
0040100C   . FFD3           CALL EBX                                 ; \Sleep
0040100E   . 33C0           XOR EAX,EAX
00401010   . 50             PUSH EAX                                 ; /ExitCode => 0
00401011   . BB 12CB817C    MOV EBX,kernel32.ExitProcess             ; |
00401016   . FFD3           CALL EBX                                 ; \ExitProcess

Sleep_3

Teď už jen stačí náš první shellcode přepsat a vyzkoušet ho spustit.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>
 
int main(){
    char shellcode[] = "\x33\xC0\x66\xB8\x88\x13\x50\xBB\x46\x24"
                       "\x80\x7C\xFF\xD3\x33\xC0\x50\xBB\x12\xCB"
                       "\x81\x7C\xFF\xD3";
 
    void (*pfunc)();
    pfunc = &amp;shellcode;
 
    printf("strlen(shellcode) = %d\n", strlen(shellcode));
 
    pfunc();
 
    return 0;
}

Odkazy:
[1] http://www.phrack.com/issues.html?issue=49&id=14

[2] http://www.asmcommunity.net/

[3] http://www.nasm.us/

[4] http://www.masm32.com/

[5] http://www.microsoft.com/exPress/

[6] http://www.codeblocks.org/

[7] http://sourceforge.net/projects/dev-cpp/

[8] http://www.ollydbg.de/

[9] http://www.hex-rays.com/idapro/

[10] http://www.immunityinc.com/products-immdbg.shtml

[11] http://windbg.info/doc/2-windbg-a-z.html

[12] http://msdn.microsoft.com/cs-cz/default.aspx

[13] http://bflow.security-portal.cz/jak-zjistit-adresy-api-funkci/