Laborator 09: Interfața în linia de comandă, analiza statică și dinamică

După un scurt breviar care va explica noțiunile introduse în acest laborator, va urma o parte practică care alternează între secțiuni de tip tutorial, cu parcurgere pas cu pas și prezentarea soluției, și exerciții care trebuie să fie rezolvate.

Interfața în linia de comanda

Deși folosirea unui mediu grafic pentru programare poate părea mai atractivă, de multe ori folosirea liniei de comandă oferă mai multă putere și control asupra a ceea ce vrem să facem. În plus, folosirea utilitarelor din linia de comandă în scripturi poate facilita automatizarea unor task-uri, lucru care ne va face viața mai ușoară în nenumărate cazuri.

În cadrul acestui laborator, vom folosi utilitare în linia de comandă atât pentru asamblarea și link-editarea fișierelor sursă, cât și pentru analiza statică și dinamică a programelor obținute din parcurgerea tutorialelor și a exercițiilor.

Analiza statică

Analiza statică a unui program constă în inspectarea diferitelor aspecte din fișierul obiect sau executabil. Analiza statică a unui program nu implică si rularea acestuia. Aceasta presupune analiza codului imediat dupa s-a terminat partea de codare și înaintea de rularea testelor.

Analiza statică poate fi efectuată de un program, în mod automat, prin parcurgerea codului si verificarea faptului ca sursa a fost scrisa in conformitate cu regulile specifice. Un exemplu clasic in acest sens este compilatorul care identifica erorile lexicale, sintactice si, uneori, semantice dintr-un program. Remarcati faptul ca programul nu este rulat atunci cand compilatorul inspecteaza sursa.

Analiza statică poate fi efectuată si de catre oameni atunci cand codul este revazut (“code review”) pentru a se asigura calitatea si lizibilitatea codului.

Avantajele analizei statice:

  • identifică zona exactă unde apare vreo eroare
  • usureaza intelegerea codului de catre alti (viitori) cititori ai codului
  • nu este nevoie sa fie rulat codul (avantaj in cazul in care rularea codului necesita resurse foarte mari)
  • identifica erori ce nu pot fi gasite printr-un alt tip de analiza: cod mort (“unreachable code”), variabile nefolosite, functii neapelate etc.

Dezavantajele analizei statice:

  • consuma foarte mult timp daca este realizata manual
  • nu poate detecta vulnerabilitati aparute la runtime (spre exemplu “segmentation fault”)
  • ai nevoie de codul sursa pentru a putea realiza acest tip de analiza

Câteve din programele utile pentru analiza statică pe care le vom folosi și în cadrul tutorialelor/exercițiilor sunt:

  • nm - utilitar folosit pentru inspectarea simbolurilor și secțiunilor din executabile
  • objdump - program folosit pentru dezasamblarea (traducerea din cod-mașină în limbaj de asamblare) programelor binare
  • IDA - o unealtă foarte puternică pentru dezasamblarea și inspectarea fișierelor obiect și executabile

Analiza dinamică

Spre deosebire de analiza statică, analiza dinamică constă în inspectarea unui program aflat în execuție. Practic, analiza dinamică se face la runtime.

Avantaje analiza dinamica:

  • identifica erorile aparute la runtime: segmentation fault, arithmetic exception etc.
  • ofera posibilitatea analizarii programului chiar daca nu avem acces la codul sursa
  • identifica vulnerabilitati care ar fi putut fi fals negative in momentul analizei statice
  • permite validarea rezultatelor analizei statice
  • poate fi realizata pentru orice aplicatie

Dezavantaje analiza dinamica:

  • este mai greu de localizat unde exact in cod are loc eroarea
  • programele ce automatizeaza analiza dinamica produc falsuri pozitive si falsuri negative
  • nu poate garanta acoperirea completa a tuturor cazurilor unui fisier sursa

Unul dintre cele mai folosite programe pentru analiză dinamică este gdb. Acesta oferă o gamă largă de operații ce pot fi făcute, de la inspectarea memoriei, la schimbarea control flow-ului și până la modificarea registrelor de pe procesor, în timpul rulării unui program.

Tutoriale și exerciții

În cadrul exercițiilor vom folosi arhiva de laborator.

Descărcați arhiva, decomprimați-o și accesați directorul aferent.

Doar dacă se specifică altfel în cerință, toate utilitarele vor fi rulate din linia de comandă. Bineînțeles, puteți folosi orice editor text pentru a rezolva exercițiile (chiar și SASM), însă asamblarea, link-editarea etc. vor fi făcute din interfața în linia de comandă.

[0.5p] 1. Tutorial: Asamblarea din linia de comandă

Deschideți fișierul hello-world.asm din directorul 1-2-3-tutorial-and-cat și înțelegeți codul.

Pentru a asambla fișierul hello-world.asm, vom folosi utilitarul nasm (program care este folosit în spate și de către SASM).

nasm -g -f elf32 hello-world.asm -o hello-world.o

-g spune asamblorului să adauge simboluri de debug în fișierul obiect rezultat

-f menționează formatul executabilului (în cazul nostru elf32)

Pentru a verifica “corectitudinea” asamblării, haideți să dezasamblăm fișierul hello-world.o folosind utilitarul objdump, astfel:

objdump -M intel -d hello-world.o

-M intel spune dezasamblorului să folosească sintaxa Intel și nu pe cea AT&T

Putem observa similaritatea dintre codul inițial și codul dezasamblat, mai puțin la instrucțiunea call, unde adresa pare greșită. Acest fapt se întâmplă din cauza faptului că fișierul obiect obținut nu “știe” cine este printf. Acest lucru se va afla la pasul de link-editare, iar adresa va fi modificată la cea corespunzătoare.

Link-editarea unuia sau mai multor fișiere obiect constă în rezolvarea tuturor simbolurilor externe și crearea unui singur fișier executabil din toate fișiere primite la intrare.

Pentru link-editare vom folosi gcc. De asemenea și gcc este folosit de SASM pentru link-editarea fișierelor obiect.

gcc -g -m32 hello-world.o -o hello-world

-g este folosit cu același scop ca la nasm, de a introduce simboluri de debug în executabil

-m32 specifică arhitectura pentru care executabilul este generat (în cazul nostru, arhitectură pe 32 biți)

Acum puteți rula executabilul pentru a vedea că toți pașii au funcționat. Pentru lansarea în execuție din linie de comandă folosiți construcția

./hello-world

Ar trebui să vi se afișeze pe ecran Hello, World.

[1.5p] 3. Implementare minimală ''cat''

Pornind de la fișierul hello-world.asm, implementați funcționalitatea de bază a utilitarului cat: citește o singură linie de la intrarea standard și o afișează la ieșirea standard.

Folosiți funcțiile gets și puts pentru a nu mai adăuga șiruri de formatare pentru scanf și printf.

Funcția gets primește bufferul în care citește ca argument. Adică va trebui să definiți/alocați un buffer și apoi să folosiți o construcție ca cea de mai jos pentru a apela gets:

    push buffer
    call gets
    add esp, 4

În cazul de mai sus după apelul gets veți avea în variabila buffer mesajul citit de la intrarea standard.

Puteți aloca un buffer în zona de date folosind o construcție de forma

    buffer: times 20 db 0

Construcția de mai sus alocă un buffer de 20 de octeți inițializați la 0.

După ce ați terminat de implementat, asamblați fișierul sursă și link-editați fișierul obiect obținut din asamblare, pentru a obține un executabil. Pentru a primi punctajul aferent exercițiului, trebuie să prezentați atât codul cât și funcționalitatea programului când este în execuție.

Ce facem atunci când vrem să modularizăm programul și să avem mai multe fișiere sursă, fiecare cu un anume rol? Practic va trebui să creăm pentru fiecare fișier sursă, fișierul obiect corespunzător, iar apoi să link-edităm toate fișierele obiect obținute într-un singur executabil care să conțină tot codul.

În directorul 4-5-linking-multiple avem două fișiere: main.asm și helpers.asm. Deschideți ambele fișiere și observați “legătura” dintre ele (cine apelează ce funcție și din ce fișier). După ce ați înțeles flow-ul programului asamblați fiecare fișier în parte, pentru a obține două fișiere obiect: main.o și helpers.o.

Pentru link-editarea multiplă se folosește aceeași comandă gcc, numai că se dau mai multe fișiere de intrare. Spre exemplu:

gcc -g -m32 main.o helpers.o -o palindrome

[1.5p] 5. Completarea fișierului cu funcții ajutătoare

După cum probabil ați observat, funcția reverse din 4-5-linking-multiple/helpers.asm nu face nimic. În cadrul acestui exercițiu, va trebui să implementați corpul funcției, unde este comentariul TODO, astfel încât șirul de caractere care a fost trimis ca parametru să fie întors in-place.

Aveți (pseudo-)codul în C care ar face acest lucru:

void reverse(char *s)
{
  int n = strlen(s);
  int i;
  char tmp;
 
  for (i = 0; i < n / 2; ++i) {
    tmp = s[i];
    s[i] = s[n - i - 1];
    s[n - i - 1] = tmp;
  }
}

După implementare va trebui să asamblați și link-editați programul.

[1p] 6. Tutorial: GDB

GDB este o unealtă foarte utilă pentru analiza dinamică a programelor. Acesta este folosit foarte des pentru găsirea cauzelor care duc la erori într-un program. În continuare vă vom prezenta câteva dintre comenzile cele mai importante.

Primul pas este să urmăriți și să înțelegeți codul din 6-7-8-gdb/gdb-tutorial.asm. Pe scurt, programul primește un parametru index și citește de la tastatură o linie. Programul afișează doar un caracter, mai exact al index-lea caracter din șirul dat la intrare.

Când rulați în GDB să dați acel parametru ca argument comenzii run sau start vedeți mai jos. Altfel, rularea nu va fi corespunzătoare.

După ce ați citit codul sursă, asamblați și link-editați fișierul. După ce ați obținut executabilul gdb-tutorial (sau ce nume i-ați dat), vom porni GDB-ul cu acel fișier:

gdb ./gdb-tutorial

Recomandăm folosirea extensiei PEDA (Python Exploit Development Assistance) pentru GDB. Pentru a putea să o folosiţi, verificați faptul că fişierul ~/.gdbinit conţine linia source ~/peda/peda.py. Dacă fişierul nu există sau nu conţine acea linie, rulaţi în terminal comanda:

echo 'source ~/peda/peda.py' >> ~/.gdbinit

Dacă lucrați pe alt sistem decât cele din sala de laborator, va trebui să clonați în prealabil repository-ul PEDA, așa cum este indicat în README-ul proiectului:

git clone https://github.com/longld/peda.git ~/peda

După ce ați pornit programul gdb, toată interacțiunea cu acesta se face prin prompt-ul de gdb.

Lansarea în execuție a programului

Pentru a lansa programul urmărit în execuție există două comenzi disponibile:

  • run - această comandă va lansa în execuție programul
  • start - spre deosebire de run, această comandă va începe execuția programului, însă se va opri imediat după intrarea în main

Aceste două comenzi mai pot fi folosite în două feluri:

  1. start 1 2 3 4
  2. start < file.in

În locul comenzii start puteți folosi mai sus comanda run.

Utilizarea aceasta este similară cu execuția programului direct din linia de comandă (fără GDB), prima variantă însemnând că se trimit 4 parametri (1, 2, 3 și 4) programului, iar a doua, că file.in se redirectează ca intrare standard pentru program.

Lansați programul în execuție folosind comanda GDB run. Ce observați? Rulați din nou programul, de data aceasta dând comenzii run parametrul corespunzător.

GDB se blochează la citirile de la input. Haideți să corectăm asta folosind un fișier de intrare. Creați un fișier (spre exemplu text.in) în directorul cu executabilul care să conțină textul “ana are mere”. Porniți din nou GDB și lansați în execuție programul cu parametrul de intrare 11 și cu fișierul text.in, redirectat.

Ce observați? Programul își termină execuția cu succes. Deoarece nu a existat niciun breakpoint setat în program, programul nu s-a oprit din execuție decât când a terminat treaba.

În cazul pornirii programului, puteți folosi instrucțiunea start care va opri execuția după intrarea în main.

Breakpoints

Elementul esențial al GDB-ului este breakpoint-ul. Practic, un breakpoint setat la o anumită instrucțiune face ca execuția programului să se oprească de fiecare dată când se ajunge la acest punct.

Adăugarea unui breakpoint se face cu construcția

break [location]

, unde location poate fi numele unei funcții sau o adresă din zona .text. În cazul cel din urmă, adresa trebuie să fie precedată de * (star). Exemplu: break *0x004013af.

Pentru continuarea programului după eventuala sa oprire într-un breakpoint, puteți folosi comanda continue.

Un alt lucru interesant în GDB este comanda commands, care poate asocia unui breakpoint un bloc de comenzi GDB ce vor fi executate la fiecare oprire în breakpoint-ul respectiv.

Exemplu:

(gdb) break *0x004013af
Breakpoint <n> at 0x4013af
(gdb) commands <n>
Type commands for breakpoint(s) <n>, one per line.
End with a line saying just "end"
> print $eax
> x/i $eip
> end

Pentru a nu rămâne blocat în breakpoint (spre exemplu dacă scrieți un script de gdb), puteți adăuga în blocul de instrucțiuni și comanda continue.

Haideți să adăugăm un breakpoint la label-ul ok. Dacă dăm continue, vom observa că programul s-a oprit în breakpoint-ul tocmai creat.

Variaţii:
break label - breakpoint la labelul label
break *(label + <offset>) - breakpoint la label + offset

Parcurgerea instrucțiunilor

Atunci când execuția programului este oprită (de exemplu la un breakpoint), putem da comenzi care continuă execuția “pas cu pas”. Pentru a face asta, cel mai des sunt folosite două comenzi:

  • stepi - care practic trimite o instrucțiune spre execuție și după execuția acesteia întoarce control-ul la debugger (programul se oprește)
  • nexti - comandă similară cu stepi, însă dacă instrucțiunea curentă este un apel de funcție, debugger-ul nu va intra în funcție (va chema funcția și se va opri la următoarea instrucțiune după call)

Dacă emitem comanda stepi, putem observa că se afișează instruction pointer-ul instrucțiunii următoare dupa cea la care am făcut break (prima de la label-ul ok).

Dezasamblarea programului

Pentru a dezasambla o porțiune de executabil, se poate folosi comanda disassemble din GDB. Dacă aceasta nu primește niciun parametru, va afișa dezasamblarea funcției curente din cadrul execuției.

Default, sintaxa folosită de GDB la dezasamblare este cea “AT&T”. Pentru a folosi sintaxa cunoscută vouă (sintaxa intel), executați în GDB comanda set disassembly-flavor intel.

În cadrul exemplului nostru, dacă cerem dezasamblarea funcției curente (folosind disassemble fără parametri) putem observa că ne aflăm la label-ul ok. Observație: GDB iterpretează label-ul ok ca o funcție din cauza codului inițial, care este scris în limbaj de asamblare.

Pentru a vedea mai clar efectul stepi/nexti putem rula commanda disassemble înainte și după stepping.

Dacă ați intrat într-o funcție lungă și nu vreți să dați de nexti de foarte multe ori, vă recomandăm instrucțiunea GDB finish, care “termină” o funcție. Atenție la funcțiile recursive.

disassemble label, +<length> - afişează <length> bytes de cod dezasamblat începând de la labelul label.

Inspectarea memoriei și a registrelor

Pentru a afișa diferite valori accesibile GDB-ului se folosește comanda print. De exemplu, pentru a afișa valoarea unui registru (de exemplu eax), vom folosi construcția print $eax.

Pentru inspectarea memoriei se folosește comanda x (examine). Modul de folosire al acestei comenzi este următorul:

x/nfu address

, unde:

  • n este numărul de elemente afișate
  • f este formatul de afișare (x pentru hexa, d pentru zecimal, s pentru șir de caractere și i pentru instrucțiuni)
  • u este dimensiunea unui element (b pentru 1 octet, h pentru 2, w pentru 4 și g pentru 8 octeți)

De exemplu, o funcționalitate similară cu disassemble o putem obține și folosind x unde formatul este instrucțiune. Astfel, putem afișa, de exemplu, 10 instrucțiuni începând de la instrucțiunea curentă cu construcția x/10i $eip.

[1p] 7. Afișarea unor informații la fiecare trecere printr-un breakpoint

Folosind executabilul creat la exercițiul anterior (gdb-tutorial.asm), trebuie să setați un breakpoint la intrare în bucla din program (când se mută în subregistrul al un caracter din șirul input). În plus, trebuie să adăugați o serie de comenzi astfel încât la fiecare intrare în buclă, GDB să afișeze valoarea subregistrului al și valoarea counter-ului (în cazul nostru ecx). Hint! folosiți comanda commands.

[1p] 8. Afișarea adreselor de retur ale unor funcții

Folosind tot executabilul de mai înainte, afișați adresele de return ale tuturor funcțiilor din program (gets, atoi, printf, usage). Pentru cazul funcției usage, trebuie să porniți programul fără parametri.

[1p] 9. Tutorial: Depanarea unui Segfault folosind GDB

Pentru acest tutorial pornim de la fișierul sursă segfault-tutorial.asm din directorul 9-segfault-tutorial. Înainte de a începe tutorialul, citiți sursa, înțelegeți ce face și apoi asamblați și link-editați programul.

Dacă încercați să rulați programul fără parametri, se poate observa că progamul “crapă”. Dacă executăm programul sub gdb, putem observa că programul primește SIGSEGV. Pentru a putea determina problema, executăm comanda backtrace, care arată ultimele stack frame-uri prin care execuția programului a trecut. În cazut nostru, doar două:

gdb-peda$ backtrace 
#0  __strcpy_sse2 () at ../sysdeps/i386/i686/multiarch/strcpy-sse2.S:1616
#1  0x08048478 in main ()
#2  0xb7e1f637 in __libc_start_main (main=0x8048460 <main>, argc=0x1, argv=0xbffff5d4, 
    init=0x8048490 <__libc_csu_init>, fini=0x80484f0 <__libc_csu_fini>, rtld_fini=0xb7fea8a0 <_dl_fini>, 
    stack_end=0xbffff5cc) at ../csu/libc-start.c:291
#3  0x08048381 in _start ()

Ne dăm seama că frame-ul interesant pentru noi este #0. Pentru a schimba frame-ul curent folosim comana frame <nr. frame>. Odată ce suntem pe frame-ul ce ne interesează putem să încercăm dezasamblarea programului pentru a identifica problema.

După instrucțiunea disassemble, putem observa instruction pointer-ul (notat pe dezasamblarea din GDB cu în dreptul unei instrucțiuni) că a rămas la instrucțiunea

cmp    BYTE PTR [ecx],0x0

Deja putem bănui o posibilă cauză a segmentation fault-ului. Inspectați registrul ecx. Ce valoare are? Ce încearcă să facă instrucțiunea cu probleme?

[1.5p] 10. Depanarea unui Segfault

Pornind de la executabilul segfault, aflat in directorul 10-solve-segfault, rulat sub gdb, analizați atât backtrace-ul cât și pas cu pas codul pentru a identifica cauza care duce la Segmentation Fault.

Puteți să vă folosiți și de objdump pentru a vedea exact conținutul secțiunii .data din cadrul binarului segfault

Cu ce este diferit objdump -d de objdump -D?

[2p] 11. Bonus: Dangerous gets

Pornind de la executabilul dangerous, aflat in directorul 11-dangerous-gets, determinați mai întâi modul de folosire al acestuia și rezultatul așteptat. Rulați executabilul de mai multe ori, crescând treptat lungimea șirului de intrare până când observați Segmentation Fault. Explicați comportamentul în detaliu aducând în același timp și o implementare alternativă ce nu suferă de aceeași problemă.

Puteți să folosiți objdump pentru a dezasambla binarul și gdb pentru a analiza flow-ul acestuia

iocla/laboratoare/laborator-09.txt · Last modified: 2017/11/29 15:18 by razvan.deaconescu
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