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.
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, 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.
Ilustrativ, putem vedea cum arată un stack frame atunci când are loc un buffer overflow.
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
.
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:
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.
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"
În cadrul acestui laborator, vom folosi arhiva de sarcini a laboratorului.
Accesați subdirectorul tutorial/
din 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?
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 <gets> 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 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 <puts> 0x0040139d <+13>: call 0x4013a4 <read_input> 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.
Accesați subdirectorul shellcode
din 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
.
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.
Suprascrieți adresa de retur a funcției vulnerabile astfel încât să se execute codul din funcția flag1
.
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
.
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.
p <function-name>
unde <function-name>
este numele funcției.
Pentru alte informații puteți dezasambla funcția folosind, în GDB, comanda
disass <function-name>
unde <function-name>
este numele funcției.
Pentru a depana funcționarea corespunzătoare a payload-ului, recomandăm următorii pași:
"C:\Program Files (x86)\SASM\MinGW\bin\gdb" shellcode.exe
main
b main
main
run
payload
p dup2(open("payload", 0), 0)
vuln
și continuați până acolob vuln c
vuln
disass ni disass ni disass ni [...]
gets
(după suficient de multe comenzi de dezasamblare și stepping) urmăriți ce aveți în vârful stiveix/30wx $esp
Identificați unde se găsește adresa de retur a funcției.
gets
(după suficient de multe comenzi de dezasamblare și stepping) urmăriți ce aveți în vârful stiveix/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.
ret
x/wx $esp
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
.
shellcode.exe
veți avea afișat mesajul dat de variabila honeypot
din fișierul shellcode.asm
, anume You shouldn't be here!.
Modificați sursa gen_payload.py
astfel încât să genereze un payload care să aducă programul să apeleze funcția flag2
.
Scrieți modificările necesare în gen_payload.py
și generați un nou payload.
shellcode.exe
veți avea afișat mesajul dat de variabila great
din fișierul shellcode.asm
, anume Mad skills, yo!.
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.