User Tools

Site Tools


laboratoare:laborator-10

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

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:

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

[2p] 1. Recon

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.

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

Dacă întâmpinați probleme

Pentru a depana funcționarea corespunzătoare a payload-ului, recomandăm următorii pași:

  1. Porniți programul sub GDB folosind comanda
    "C:\Program Files (x86)\SASM\MinGW\bin\gdb" shellcode.exe 
  2. În consola GDB, creați un breakpoint la funcția main
    b main
  3. În consola GDB rulați programul cu oprire acum la funcția main
    run
  4. În consola GDB fortați citirea intrării de standard din fișierul payload
    p dup2(open("payload", 0), 0)
  5. Folosiți un breakpoint în funcția vuln și continuați până acolo
    b vuln
    c
  6. 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
    [...]
  7. 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.

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

  9. 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
  10. 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

laboratoare/laborator-10.txt · Last modified: 2016/01/09 13:39 by razvan.deaconescu