====== Laborator 10: Exploatarea memoriei. Shellcodes ======
Ca o continuare a laboratorului precedent, vom merge mai în detaliu cu ce putem face în momentul în care deținem controlul asupra unui buffer într-un program, fără ca acesta să fie protejat de un posibil overflow.
===== Concepte de bază =====
Securitatea aplicațiilor este un domeniu ce a căpătat amploare în ultimii ani și considerăm relevantă explorarea istoricului acesteia din punct de vedere practic. Țineți cont de faptul că noțunile pe care le vom acoperi în cadrul laboratorului sunt la nivelul anului 1996 (același an în care a fost publicat articolul lui Aleph One, [[http://insecure.org/stf/smashstack.html|Smashing the Stack for Fun and Profit]]).
Înainte de toate, trebuiesc stabilite noțiunile cu care vom lucra. Un **bug** constă într-un defect în cadrul unei aplicații ce duce la funcționarea eronată a acesteia (spunem despre program că este //buggy//). O **vulnerabilitate** este un bug ce poate fi exploatat, într-o formă sau alta de către un atacator. Un exemplu comun și relevant pentru noi este cel de buffer overflow.
==== Buffer overflow: Crash ====
Ilustrativ, putem vedea cum arată un stack frame atunci când are loc un buffer overflow.
{{:laboratoare:lab10_-_initial_stack_frame.png?200|}} {{:laboratoare:lab10_-_8a.png?200|}} {{:laboratoare:lab10_-_16a.png?200|}} {{:laboratoare:lab10_-_overwritten_ret.png?200|}}
A se nota că stiva crește în 'sus', de la adrese mari la adrese mici, în timp ce bufferul crește în ordinea firească.
După cum ați văzut și în laboratorul precedent, un buffer overflow poate fi folosit pentru a citi date, pentru a scrie date sau pentru a altera fluxul programului.
În situația de mai sus, programul ne va întâmpina cu binele cunoscut ''Segmentation fault'', cu informația adițională ''EIP at 0x41414141''.
Ce s-a întâmplat, de fapt? Atunci când se iese dintr-o funcție se folosește instrucțiunea ''ret''. Această instrucțiune este echivalentă cu a scrie ''pop eip'' (deși aceasta nu este validă). În momentul în care se execută instrucțiunea ''ret'', se pune în instruction pointer (EIP) valoarea de pe vârful stivei. Efectul va fi echivalent cu cel al unui jump la adresa ce se găsește pe stivă.
Din moment ce adresa de retur a fost suprascrisă cu 4 'A'-uri, programul va încerca să se ”întoarcă” la adresa ''AAAA'', sau în traducerea lui în hexazecimal, ''0x41414141''. Dat fiind că această adresă nu este mapată de către proces, această operație este echivalentă cu un acces invalid la memorie, lucru ce duce la ''Segmentation fault''.
==== Buffer overflow: Exploatare ====
Ce se întâmplă dacă scriem o adresă validă (mapată) în locul celei de retur? La ieșirea din funcție, programul o să se întoarcă la acea adresă.
Putem să mai facem un pas și să considerăm următorul scenariu:
{{:laboratoare:lab10_-_stack_code.png?800|}}
Dacă reușim să suprascriem adresa de retur cu adresa la care se găsește începutul bufferului pe stivă, atunci am putea să executăm un cod pe care-l **injectăm** prin intermediul bufferului. Din motive istorice, acest cod poartă numele generic de **shellcode**, deoarece de cele mai multe ori când un atacator exploatează o aplicație sau un serviciu, dorește să dobândească un shell prin care să poată interacționa cu sistemul respectiv.
Dacă într-o aplicație se descoperă un buffer overflow, nu înseamnă (neapărat) că acest lucru este o vulnerabilitate. Dacă un atacator nu poate **controla** buffer-ul respectiv, atunci rămâne doar un bug în cadrul aplicației respective.
===== Setup =====
Pentru a putea rula ''python'', ''gdb'' și ''objdump'' de oriunde (în cadrul acelei console) recomandăm să faceți următorul setup în ''Command Prompt'':
set PATH=%PATH%;"C:\Program Files (x86)\SASM\MinGW\bin";"C:\Python27"
===== Exerciții =====
În cadrul acestui laborator, vom folosi [[http://elf.cs.pub.ro/asm/res/laboratoare/lab-10-tasks.zip|arhiva de sarcini a laboratorului]].
Recomandarea noastră este ca pentru fiecare task în parte să vă generați câte un //payload// aferent.
Puteți folosi orice fel de consolă vi se pare adecvată.
==== [2p] Tutorial: Găsire offset și suprascrierea adresei de retur ====
Accesați subdirectorul ''tutorial/'' din [[http://elf.cs.pub.ro/asm/res/laboratoare/lab-10-tasks.zip|arhiva de sarcini a laboratorului]].
Inspectați sursa ''tutorial.asm'' și rulați comanda ''build_tutorial''.
Programul citește într-un buffer de la intrarea standard folosind funcția ''gets''. Dacă inputul este prea mare, atunci va avea loc un buffer overflow. Rulați ''tutorial.exe'' și dați un input de cel puțin 40 de caractere. Observați ce se întâmplă.
Rulați ''GDB'' pe executabilul ''tutorial.exe'' folosind comanda:
"C:\Program Files (x86)\SASM\MinGW\bin\gdb" tutorial.exe
Apoi rulați comenzile de mai jos cu obiectivul de a urmări ce se întâmplă la ieșirea din funcția ''read_input'' (în momentul apelării ''ret''):
(gdb) set disassembly-flavor intel
(gdb) start
[...]
(gdb) disas
[...]
(gdb) b read_input
[...]
(gdb) b *0x4013ba
(gdb) c
Continuing.
What is your name?
Comanda ''b *0x4013ba'' creează un breakpoint la sfârșitul funcției ''read_input'', adică în momentul apelării ''ret'' în cadrul acelei funcții.
Introduceți drept input o secvență lungă (circa 50) de caractere ''A''.
(gdb) disas
Dump of assembler code for function read_input:
0x004013a4 <+0>: push ebp
0x004013a5 <+1>: mov ebp,esp
0x004013a7 <+3>: sub esp,0x28
0x004013aa <+6>: sub esp,0xc
0x004013ad <+9>: lea eax,[ebp-0x1c]
0x004013b0 <+12>: push eax
0x004013b1 <+13>: call 0x401b18
0x004013b6 <+18>: add esp,0x10
0x004013b9 <+21>: leave
=> 0x004013ba <+22>: ret
0x004013bb <+23>: add BYTE PTR [ebp-0x77],dl
End of assembler dump.
(gdb) x/x $esp
0x28ff20: 0x41414141
(gdb) x/s $esp
0x28ff20: "AAAAAAA"
Observați că atunci când se ajunge la instrucțiunea ''ret'' din funcția ''read_input'', pe vârful stivei se găsește valoarea ''0x41414141'', adică %%"AAAA"%% din bufferul nostru.
La ce offset față de începutul bufferului se găsește adresa de retur? Putem să introducem combinații de câte 4 pentru a ne ușura viața, în felul următor:
(gdb) r
What is your name?
1111222233334444555566667777888899990000
Breakpoint 3, 0x004013ba in read_input ()
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x39393939 in ?? ()
Putem vedea că adresa de retur a fost suprascrisă cu valoarea hexazecimală ''0x39393939'', adică ''9999'' dacă ne uităm în [[http://www.asciitable.com/|tabela ASCII]]; caracterul ASCII ''9'' are valoarea hexazecimală ''0x39''. Offsetul celor ''4'' de ''9'' este ''32''; adică la ''32'' de octeți de la începutul buffer-ului/șirului se găsește adresa de retur a funcției.
Având offset-ul, putem completa și folosi script-ul Python ''tutorial_payload.py'' pentru a genera un payload care să crape programul în mod consecvent.
Actualizați în fișierul ''tutorial_payload.py'' variabila ''offset'' la offset-ul necesar pentru suprascrierea adresei de retur. În cazul nostru este vorba de valoarea ''32''.
Generati un ''payload'' (in afara GDB-ului) folosind comanda
python tutorial_payload.py
Comanda generează un payload în fișierul ''payload''.
(gdb) b main
(gdb) run
(gdb) p dup2(open("payload", 0), 0)
(gdb) c
Continuing.
What is your name?
Program received signal SIGSEGV, Segmentation fault.
0x0badc0de in ?? ()
Observăm că funcția a încercat să se întoarcă la adresa dată de noi.
Ca un ultim pas, haideți să găsim o adresă validă la care să ne întoarcem.
(gdb) r
(gdb) disas
Dump of assembler code for function main:
0x00401390 <+0>: push ebp
0x00401391 <+1>: mov ebp,esp
=> 0x00401393 <+3>: push 0x402000
0x00401398 <+8>: call 0x401b10
0x0040139d <+13>: call 0x4013a4
0x004013a2 <+18>: leave
0x004013a3 <+19>: ret
End of assembler dump.
Am putea lua adresa la care se pune mesajul //%%"What is your name?"%%// pe stivă iar apoi se apelează ''puts'', adică adresa ''0x401393''. Modificați valoarea ''0x0badc0de'' din ''tutorial_payload.py'' cu cea alesă și mai generați o dată fișierul de intrare. Pentru generarea fișieruli de intrare folosiți comanda
python tutorial_payload.py
Comanda generează un payload în fișierul ''payload''.
.\tutorial.exe < payload
What is your name?
What is your name?
Programul afișează cele două mesaje, așa cum ne așteptam, și apoi crash-uiește. Vom vedea în exercițiile următoare de ce programul a crash-uit.
==== [2p] 1. Recon ====
Accesați subdirectorul ''shellcode'' din [[http://elf.cs.pub.ro/asm/res/laboratoare/lab-10-tasks.zip|arhiva de sarcini a laboratorului]].
Identificați vulnerabilitatea de tip buffer overflow din programul ''shellcode.asm''. Faceți programul să crape suprascriind prin buffer overflow o adresă de retur corespunzătoare.
Pentru a face overflow, cel mai bine este să generați un fișier de tip ''payload'' pe care să îl trimiteți ca intrare programului. Pentru a genera fișierul de tip payload, recomandăm folosirea scriptului Python ''gen_payload.py'' și urmăriți comentariile marcate cu ''TODO''.
Determinați offsetul unde ar trebui să se găsească adresa de retur a funcției vulnerabile și completați corespunzător în fișierul ''gen_payload.py''.
Inspectați codul funcției ''vuln'' și aflați care este offset-ul de la adresa buffer-ului folosit de funcția ''gets()'' până la locul de pe stivă unde este reținută adresa de retur a funcției ''vuln''.
Generați un payload în consola de Windows (adică nu în consola GDB) folosind comanda
python gen_payload.py
Comanda generează payload-ul în fișierul ''payload''. Transmiteți acest payload programului în linia de comandă
.\shellcode.exe < payload
Dacă ați completat corespunzător offset-ul în cadrul scriptului Python, atunci rularea programului va rezulta într-un crash.
==== [3p] 2. First flag ====
Suprascrieți adresa de retur a funcției vulnerabile astfel încât să se execute codul din funcția ''flag1''.
Modificați ''gen_payload.py'' în mod corespunzător. Va trebui să actualizați corespunzător variabila ''offset'', să determinați adresa funcției ''flag1'' și să adăugați valoarea cu care să suprascrieți adresa de retur la sfârșitul payload-ului. Valoarea folosită pentru suprascriere este chiar adresa funcției ''flag1''.
Pentru a adăuga adresa funcției ''flag1'' la sfârșitul payload-ului va trebui să convertiți acea adresă dintr-o valoarea întreagă (pe 4 octeți) într-un șir de octeți. Pentru aceasta folosiți-vă de funcția ''dw'' care exact acest lucru îl face: trece o valoare pe 4 octeți într-un șir de octeți în format little endian. Puteți urmări modul în care este folosită funcția ''dw'' în fișierul ''tutorial_payload.py'' din cadrul tutorialului de mai devreme.
Dacă doriți să aflați adresa unei funcții folosiți, în GDB, comanda
p
unde '''' este numele funcției.
Pentru alte informații puteți dezasambla funcția folosind, în GDB, comanda
disass
unde '''' este numele funcției.
**Dacă întâmpinați probleme**
Pentru a depana funcționarea corespunzătoare a payload-ului, recomandăm următorii pași:
- Porniți programul sub GDB folosind comanda
"C:\Program Files (x86)\SASM\MinGW\bin\gdb" shellcode.exe
- În consola GDB, creați un breakpoint la funcția ''main''
b main
- În consola GDB rulați programul cu oprire acum la funcția ''main''
run
- În consola GDB fortați citirea intrării de standard din fișierul ''payload''
p dup2(open("payload", 0), 0)
- Folosiți un breakpoint în funcția ''vuln'' și continuați până acolo
b vuln
c
- Dezasamblați codul și urmăriți pas cu pas (stepping) ce se întâmplă în cadrul funcției ''vuln''
disass
ni
disass
ni
disass
ni
[...]
- **Imediat înainte** de apelul ''gets'' (după suficient de multe comenzi de dezasamblare și stepping) urmăriți ce aveți în vârful stivei
x/30wx $esp
Identificați unde se găsește adresa de retur a funcției.
- **Imediat după** apelul ''gets''(după suficient de multe comenzi de dezasamblare și stepping) urmăriți ce aveți în vârful stivei
x/30wx $esp
Identificați acum ce se găsește în locul noii adrese de retur a funcției și vedeți dacă totul corespunde.
- Vedeți ce se găsește în vârful stivei **imediat înainte** de revenirea din funcție, adică imediat înainte de instrucțiunea ''ret''
x/wx $esp
- Mai faceți un step
ni
În acest moment veți face jump la adresa stocată în vârful stivei adică acolo unde programul așteaptă adresa de retur. Dacă totul e în regulă, veți face jump în funcția ''flag1''.
Dacă ați completat corect payload-ul, la transmiterea payload-ului către programul ''shellcode.exe'' veți avea afișat mesajul dat de variabila ''honeypot'' din fișierul ''shellcode.asm'', anume //You shouldn't be here!//.
==== [3p] 3. Second flag ====
Modificați sursa ''gen_payload.py'' astfel încât să genereze un payload care să aducă programul să apeleze funcția ''flag2''.
Câte argumente are funcția?
Pentru ce valori ale argumentelor ajunge funcția să treacă testele?
Cum trebuie să arate bufferul de intrare astfel încât atunci când se ajunge în funcție parametrii să se găsească pe pozițiile corespunzătoare pe stivă?
Scrieți modificările necesare în ''gen_payload.py'' și generați un nou payload.
Dacă ați completat corect payload-ul, la transmiterea payload-ului către programul ''shellcode.exe'' veți avea afișat mesajul dat de variabila ''great'' din fișierul ''shellcode.asm'', anume //Mad skills, yo!//.
==== [3p] BONUS Graceful exit ====
Ați observat că, pentru payload-ul anterior, în care apelați funcția ''flag2()'' deși se execută codul dorit de noi, programul în continuare crapă. De ce? Corectați acest lucru. Generați noi payload-uri prin care după ce se execută codul dorit, programul să se termine cu succes.
===== Soluții =====
[[http://elf.cs.pub.ro/asm/res/laboratoare/lab-10-sol.zip|Soluții de referință pentru exercițiile de laborator]]