Capitol 07 - Analiza executabilelor și proceselor

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/exec-process-demo.zip

și apoi decomprimăm arhiva

unzip exec-process-demo.zip

și accesăm directorul rezultat în urma decomprimării

cd exec-process-demo/

Acum putem parcurge secțiunile cu demo-uri de mai jos.

Secțiuni și adrese în cadrul unui fișier executabil

Dorim să urmărim adresele secțiunilor și simbolurilor în cadrul unui fișier executabil de tip ELF (Executable and Linking Format). Pentru aceasta accesăm subdirectorul exec-addr/; urmărim conținutul fișierului exec-addr.c. În acest fișier definim variabile globale și afișăm adresele acestor variabile și a funcțiilor din modul. Vom observa că adresele variabilelor globale și a funcțiior sunt cunoscute de la link-time, în momentul link-editării și a obținerii executabilului.

Compilăm programul folosind make.

Pentru început investigăm simbolurile din executabil. Ne interesează variabilele globale și funcțiile așa că vom rula comanda de afișare a simbolurilor din care vom extrage liniile de interes:

user@host:$ objdump --syms exec-addr | grep '\(exec_\| main\|simple_func\)'
0000000000600da0 l     O .data	0000000000000004              exec_static_int_global
000000000040076b l     F .text	000000000000001a              simple_func
0000000000600dc4 g     O .bss	0000000000000004              exec_int_global_noinit
0000000000600da4 g     O .data	0000000000000004              exec_int_global
00000000004008b8 g     O .rodata	0000000000000006              exec_array_ro
0000000000400785 g     F .text	000000000000009c              main

Prin rularea comenzii objdump de mai sus afișăm informații despre simboluri, în format pe coloane, astfel:

  • În prima coloană sunt adresele simbolurilor. Aceste adrese se vor regăsi întocmai în proces (vom vedea în continuare). Adresele pot să difere în cazul obținerii executabilului pe alt sistem.
  • A doua coloană este tipul simbolului. Simbolurile statice sunt marcate cu l (local) pentru că vor fi locale modulului. Celelalte sunt marcate cu g (global) și vor putea fi exportate în alte module.
  • A patra coloană este secțiunea din executabil unde este alocat simbolul. Simbolul este alocat în modul încă de la compile-time. Secțiunile sunt după cum urmează:
    • .data: variabile globale inițializate
    • .bss: variabile globale neinițializate
    • .rodata: variabile globale de tip read-only
    • .text: zonă de cod/instrucțiuni (pentru funcții)
  • A cincea coloană este spațiul ocupat de simbol. Variabilele întregi ocupă sizeof(int) = 4 octeți, șirul de caractere ocupă 6 octeți (incluzând NUL-terminatorul) iar funcțiile ocupă spațiul dat de codul acestora.

Observăm că zona de date read-only (.rodata) este apropiată de zona de cod (.text) ambele fiind zone care nu pot fi scrise.

Ca să verificăm faptul că adresele precizate în executabil se vor regăsi și în momentul rulării procesului, la run-time, rulăm executabilul:

$ ./exec-addr 
Inside simple_func
 
Run-time addresses are:
&exec_static_int_global: 0x600da0
&exec_int_global: 0x600da4
&exec_int_global_noinit: 0x600dc4
&exec_array_ro: 0x4008b8
&simple_func: 0x40076b
&main: 0x400785
 
Run `pmap -p $(pidof exec-addr)' to show process map.
 
     Press ENTER to continue ...

Observăm din rezultatul rulării că adresele de la run-time sunt aceleași cu cele din executabil.

După cum ni se indică la rulare, vom rula pmap pentru a consulta spațiul virtual de adresă al procesului:

$ pmap -p $(pidof exec-addr)
13545:   ./exec-addr
0000000000400000      4K r-x-- /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/exec-addr/exec-addr
0000000000600000      4K rw--- /home/razvan/school/2011-2012/so/git-repos/cursuri.git/curs-07-demo/exec-addr/exec-addr
00007fd884842000   1664K r-x-- /lib/x86_64-linux-gnu/libc-2.18.so
00007fd8849e2000   2044K ----- /lib/x86_64-linux-gnu/libc-2.18.so
00007fd884be1000     16K r---- /lib/x86_64-linux-gnu/libc-2.18.so
00007fd884be5000      8K rw--- /lib/x86_64-linux-gnu/libc-2.18.so
00007fd884be7000     16K rw---   [ anon ]
00007fd884beb000    128K r-x-- /lib/x86_64-linux-gnu/ld-2.18.so
00007fd884dcf000     12K rw---   [ anon ]
00007fd884e06000     16K rw---   [ anon ]
00007fd884e0a000      4K r---- /lib/x86_64-linux-gnu/ld-2.18.so
00007fd884e0b000      4K rw--- /lib/x86_64-linux-gnu/ld-2.18.so
00007fd884e0c000      4K rw---   [ anon ]
00007fffb51d1000    132K rw---   [ stack ]
00007fffb51fe000      8K r-x--   [ anon ]
ffffffffff600000      4K r-x--   [ anon ]
 total             4068K

Legat de partea de executabil, observăm că avem două pagini (4K) mapate din executabilul exec-addr. Una este readable/executable (r-x) și începe de la adresa 0x400000, iar alta este readable/writable (rw-) și începe de la adresa 0x600000. În prima pagină sunt mapate secțiunile .text și .rodata, iar în cealaltă sunt mapate secțiunile .data și .bss; observăm acest lucru pe baza adreselor.

Executabile statice și dinamice

Pentru a obține un executabil, folosim linker-ul pentru a lega unul sau mai multe module obiect cu biblioteci. Legarea (linking) presupune:

  • comasarea secțiunilor similare din modulele obiect și din bilioteci: de exemplu secțiunile de cod (.text) sunt comasate într-o singură secțiune
  • atașarea de adrese de start fiecărei secțiuni și de adrese fiecărui simbol (address binding): fiecare simbol (adică fiecare variabilă, funcție) are acum o adresă unică în cadrul executabilului
  • rezolvarea referințelor în cadrul secțiunilor comasate sau inter-secțiuni: o dată știute adresele simobolurilor în cadrul executabilului, se pot completa instrucțiunile de procesor (cod mașină) care foloseau aceste simboluri

În modul simplu, legarea este statică (static linking) iar rezultatul este un executabil static. Un executabil static realizează complet toți pașii de mai sus și are în cadrul său toate sețiunile de care are nevoie și toate referințele rezolvate.

O alternativă la legarea statică este legarea dinamică (dynamic linking) care are ca rezultat un executabil dinamic. Un executabil dinamic nu include toate secțiunile și menține referințe nerezolvate; rezolvarea acestora este amânată la momentul lansării în execuție a executabilului, numit și încărcare (loading, load time), când se creează procesul corespunzător.

Un executabil dinamic nu include în secțiunile sale părțile corespunzătoare din biblioteci. Aceste biblioteci sunt biblioteci dinamice și vor fi adăugate în spațiul virtual de adrese al procesului după încărcare (loading), fără a încărca executabilul.

Astfel, un executabil dinamic va fi semnificativ mai mic decât un executabil static. Pe lângă dimensiunea redusă a executabilului, un alt avantaj este partajarea zonelor de cod din cadrul bibliotecilor dinamice cu alte procese. Întrucât aceste zone nu sunt modificate, sunt mapate în spațiul virtual al tuturor proceselor care le foloesc. De exemplu, în cazul bibliotecii standard C, folosită de toate procesele sistemului, zona sa de cod este prezentă o singură dată în memoria RAM și mapată în spațiul virtual de adrese al tuturor proceselor provenite din executabile dinamice. Un proces obținut dintr-un executabil static nu poate partaja nici o parte din zona sa cu alte procese obținute din executabile static, însemnând un consum mai mare de memorie. Bibliotecile dinamice, partajabile, poartă în Linux denumirea de shared objects; de aici și extensia fișierelor de tip bibliotecă dinamică .so.

Executabilele dinamice au dezavantajul unui timp mai mare de încărcare. La orice lansare în execuție a executabilului trebuie realizat procesul de rezolvare a referințelor, numit dynamic binding. Suplimentar, dacă un executabil dinamic este transferat pe un sistem unde nu este prezentă o bibliotecă dinamică de care are nevoie, sau este prezentă o versiune incompatibilă, acesta nu va rula.

Sumarizând, executabilele statice și dinamice au următoarele avantaje:

  • Executabilele dinamice ocupă mai puțin spațiu pe disc, iar procesele aferente ocupă mai puțină memorie fizică, partajând zonele read-only și read-execute (.text, .rodata) din bibilioteci cu alte procese obținute din executabile dinamice care folosesc aceleași biblioteci.
  • Executabilele statice sunt portabile, pot fi transferate pe un alt sistem (cu aceeași arhitectură a sistemului pentru care a fost compilat binarul) fără a fi nevoie de prezența unor biblioteci sau biblioteci compatibile. Lansarea lor în execuție (loading) este mai rapidă, nefiind nevoie de rezolvarea referințelor (dynamic binding); toate referințele au fost rezolvate la link time.

În subdirectorul static-dynamic/ se găsește fișierul sursă hello.c și un fișier Makefile pe care îl folosim pentru a compila un executabil static (hello-static) și unul dinamic (hello-dynamic):

$ ls
hello.c  Makefile

$ make
cc -Wall -fno-PIC   -c -o hello.o hello.c
cc -no-pie -static -o hello-static hello.o
cc -no-pie -o hello-dynamic hello.o

$ ls -F
hello.c  hello-dynamic*  hello.o  hello-static*  Makefile

Putem observa că executabilul static are dimensiunea mai mare decât cel dinamic (de circa 100 de ori mai mare) și că are mai multe simboluri:

$ ls -lh hello-*
-rwxr-xr-x 1 student student 8.4K Mar 29 21:57 hello-dynamic
-rwxr-xr-x 1 student student 826K Mar 29 21:57 hello-static

$ nm hello-static | wc -l
1664

$ nm hello-dynamic | wc -l
38

Așa cum am discutat, un executabil dinamic are referințe nerezolvate către biblioteci (în cazul de față biblioteca standard C), referințe ce vor fi rezolvate la load time. Executabilul static nu are referințe nerezolvate:

$ nm hello-dynamic | grep ' U '
                 U fflush@@GLIBC_2.2.5
                 U fgets@@GLIBC_2.2.5
                 U __libc_start_main@@GLIBC_2.2.5
                 U printf@@GLIBC_2.2.5
                 U puts@@GLIBC_2.2.5
                 U __stack_chk_fail@@GLIBC_2.4

$ nm hello-static | grep ' U '
$

Utilitarul ldd afișează bibliotecile dinamice necesare pentru executabilele dinamice, iar pentru un executabil static nu afișează nimic:

$ ldd hello-dynamic
        linux-vdso.so.1 (0x00007ffcda18d000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6dbc088000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f6dbc479000)

$ ldd hello-static
        not a dynamic executable

La rularea oricărui executabil (loading), acesta așteaptă să apăsăm Enter ca să continue. Acest lucru ne permite să investigăm spațiul de adresă pentru procesul proaspăt pornit folosind utilitarul pmap ca mai jos:

$ pmap $(pidof hello-dynamic)
24227:   ./hello-dynamic
0000000000400000      4K r-x-- hello-dynamic
0000000000600000      4K r---- hello-dynamic
0000000000601000      4K rw--- hello-dynamic
0000000001986000    132K rw---   [ anon ]
00007f556ba27000   1948K r-x-- libc-2.27.so
00007f556bc0e000   2048K ----- libc-2.27.so
00007f556be0e000     16K r---- libc-2.27.so
00007f556be12000      8K rw--- libc-2.27.so
00007f556be14000     16K rw---   [ anon ]
00007f556be18000    156K r-x-- ld-2.27.so
00007f556c01d000      8K rw---   [ anon ]
00007f556c03f000      4K r---- ld-2.27.so
00007f556c040000      4K rw--- ld-2.27.so
00007f556c041000      4K rw---   [ anon ]
00007ffeceb5d000    132K rw---   [ stack ]
00007ffeceba4000     12K r----   [ anon ]
00007ffeceba7000      4K r-x--   [ anon ]
ffffffffff600000      4K --x--   [ anon ]
 total             4508K

$ pmap $(pidof hello-static)
24245:   ./hello-static
0000000000400000    728K r-x-- hello-static
00000000006b6000     24K rw--- hello-static
00000000006bc000      4K rw---   [ anon ]
000000000091c000    140K rw---   [ anon ]
00007ffdb1a33000    132K rw---   [ stack ]
00007ffdb1b80000     12K r----   [ anon ]
00007ffdb1b83000      4K r-x--   [ anon ]
ffffffffff600000      4K --x--   [ anon ]
 total             1048K

Observăm că în cazul executabilului dinamic, spațiul virtual de adrese cuprinde bibliotecile dinamice partajate (biblioteca standard C libc-2.27.so și dynamic linker/loader-ul ld-2.27.so). Spațiul virtual de adrese al executabilului static este mai redus, cuprinzând doar zonele specifice executabilului și zonele dinamice ale procesului (stivă, heap, mapări anonime), fără biblioteci.

Executabile fără libc

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 mod obișnuit un executabil este obținut prin legarea unui sau a mai multor module obiect la biblioteca standard C (libc) și, la nevoie, alte biblioteci. Așa cum am prezentat mai sus, în cazul legării statice (static linking) linker-ul adaugă în executabil toate componentele necesare (cod, date) din cadrul bibliotecii statice; în cazul legării dinamice (dynamic linking) linker-ul adaugă referințe la simbolurile din bibliotecile dinamice, iar rezolvarea acestor simboluri și adăugarea componentelor se întămplă la încărcare (load time).

Putem crea programe și folosi linker-ul să nu folosească deloc biblioteca standard C. Acest lucru este util pentru medii de lucru specializate (numite free standing environments) în care nu avem acces la o bibliotecă standard C sau nu o putem folosi. Astfel de programe vor implementa singure funcționalități similare celor oferite de biblioteca standard C sau vor realiza direct apeluri de sistem.

În directorul no-libc/ avem un exemplu de astfel de program, care folosește direct apeluri de sistem pentru a afișa un mesaj la ieșirea standard. Programul este scris în limbaj de asamblare pentru arhictectura x86_64 în sintaxa AT&T folosind asamblorul GNU (gas parte din GCC) în fișierul hello.s și în sintaxa Intel folosind asamblorul NASM în fișierul hello.asm.

În ambele fișiere (hello.s și hello.asm) invocăm apelul de sistem write(1, “Hello, world!\n”, 14);. Pentru invocarea acestui apel de sistem, urmărim convenția de apel de sistem specifică arhitecturii x86_64 pe Linux:

  • registrul rax reține numărul apelului de sistem (1 pentru apelul de sistem sys_write)
  • registrul rdi reține primul argument (1 pentru descriptorul de fișier corespunzător ieșirii standard - standard output)
  • registrul rsi reține al doilea argument (adresa mesajului de afișat, adică adresa șirului Hello, world!\n)
  • registrul rdx reține al treilea argument (14 pentru lungimea mesajului de afișat)

Pentru încheierea execuției, invocăm apelul de sistem exit(0).

Pentru a obține executabilele este nevoie de un pas de asamblare, care obține fișierul obiect corespunzător și un pas de legare care obține fișierul executabil. Realizăm acești pași prin intermediul comenzii make:

$ ls
hello.asm  hello.s  Makefile

$ make
nasm -f elf64 -o hello-nasm.o hello.asm
cc -nostdlib -no-pie -Wl,--entry=main -Wl,--build-id=none  hello-nasm.o   -o hello-nasm
gcc -c -o hello-gas.o hello.s
cc -nostdlib -no-pie -Wl,--entry=main -Wl,--build-id=none  hello-gas.o   -o hello-gas

$ ls
hello.asm  hello-gas  hello-gas.o  hello-nasm  hello-nasm.o  hello.s  Makefile

În urma rulării comenzii am obținut fișierele obiect hello-nasm.o și hello-gas.o și fișierele executabile hello-nasm și hello-gas.

Rulăm cele două executabile care vor afișa mesajul Hello, world! la ieșirea standard:

$ ./hello-nasm
Hello, world!
$ ./hello-gas
Hello, world!

Putem observa diferențe între aceste executabile (obținute fără suport de bibliotecă standard C) și cele de la demo-ul anterior, obținute cu suport de bibliotecă standard C.

Fișierele sunt mai mici, nu conțin elemente de setup și cleanup din biblioteca standard C, au doar miminul necesar pentru a funcționa: au minimul de simboluri, minimul de secțiuni, minimul de cod, așa cum putem observa rulând comenzile de mai jos:

$ ls -l hello-gas hello-nasm
$ nm hello-gas
$ nm hello-nasm
$ objdump -d hello-gas
$ objdump -d -M intel hello-nasm
$ readelf -S hello-gas
$ readelf -S hello-nasm

Putem obține aceste executabile fără să apelăm comanda gcc cu un număr complicat de parametri. Putem invoca direct linker-ul folosind comenzile:

ld -o hello-gas --entry=main hello-gas.o
ld -o hello-nasm --entry=main hello-nasm.o

Nu în ultimul rând, putem reduce dimensiunea executabilelor eliminând tabela de simboluri cu ajutorul comenzii strip:

$ strip hello-nasm
$ strip hello-gas

Vedeți aici un articol despre cum puteți obține un executabil cât mai mic pe un sistem Linux.

Apelarea unor funcționalități ascunse

În mod obișnuit, un program execută fluxul de apel dat de codul său și de intrarea venită de la utilizator sau alte procese sau alte forme de I/O. În anumite situații putem dori să declanșăm mai repede un apel al unei funcționalități prezente în executabil, dar care nu se găsește pe fluxul de apel curent.

La nivel de provocare (challenge) în lumea competițiilor de tip Capture the Flag (CTF), sunt situații în care un program conține funcționalități ascunse; adică sunt funcții și variabile prezente în program dar care nu sunt apelate sau folosite explicit. Un astfel de exemplu este în subdirectorul hidden/.

În subdirectorul hidden/ se găsește executabilul hidden obținut din compilarea și legarea fișierelor hidden.c și reader.c. Obiectivul nostru este să afișăm mesajul success cu ajutorul acestui program, în orice mod posibil.

Pentru început, observăm că acest program se apelează funcția hidden_function(). Numai că apelul se realizează astfel încât depinde de valori aleatoare. În plus valorile variabilelor test și success_message nu ajută la obiectivul nostru: afișarea mesajului success. Identificăm următoarele limitări ale programului, limitări peste care trebuie să trecem:

  • variabila test are o valoare predefinită care nu este modificată în program
  • variabila success_message are o valoare predefinită care nu este modificată în program
  • al doilea apel al funcției hidden_function() depinde de două valori aleatoare, generate la rulare (run time)

Primul apel de funcție: analiză statică

Pentru început ne propunem să realizăm primul apel al funcției hidden_function(), adică apelul hidden_function(100) apel ce nu depinde de valori aleatoare generate la rulare. Putem să realizăm acest apel folosind doar analiză statică (pe executabil) și modificând executabilul. Adică urmărim:

  • Să suprascriem valoarea variabilei test cu valoarea 100, argumentul apelului hidden_function(). În felul acesta vom trece de condiția if din cadrul funcției hidden_function() (linia 11 din fișierul hidden.c).
  • Să suprascriem valoarea variabilei success_message cu valoarea success\0. în felul acesta apelul funcției puts() din cadrul funcției hidden_function() (linia 12 din fișierul hidden.c).

Pentru a realiza aceste suprascrieri trebuie să știm la ce offset în cadrul fișierului executabil se găsesc cele două valori. Pentru a determina acest offset, vom face pașii:

  1. Aflăm ce adresă le este asociată (address binding) celor două variabile (test și success_message). Fie aceste adrese test_address și success_message_address.
  2. Aflăm ce adresă are asociată zona .data în care se găsesc cele două variabile. Fie această adresă data_address.
  3. Aflăm la ce offset în cadrul fișierului executabil se găsește zona .data. Fie acest offset data_offset.
  4. Calculăm offset-ul celor două variabile în cadrul fișierului: test_offset = test_address - data_address + data_offset, success_message_offset = success_message_address - data_address + data_offset.

Pasul 1 îl putem realiza folosind utilitarul nm sau utiltarul readelf:

$ nm hidden | grep ' test$' | awk -F '[ \t]+' '{print $1;}'
0000000000601080

$ readelf -s hidden | grep ' test$' | awk -F '[ \t]+' '{print $3;}'
0000000000601080

Pasul 2 și pasul 3 îi realizăm folosind utilitarul readelf:

$ readelf -S hidden | grep ' .data ' | awk -F '[ \t]+' '{print $5;}'
0000000000601060

$ readelf -S hidden | grep ' .data ' | awk -F '[ \t]+' '{print $6;}'
00001060

Pasul 4 îl realizăm prin operații aritmetice simple.

O dată aflat offset-ul putem folosi utilitarul dd pentru a suprascrie părți din fișier, cu template-ul:

$ echo -en "<value>" | dd seek=<offset> of=hidden bs=1 conv=notrunc

Acești pași sunt realizați în scriptul rewrite_exec.sh. În urma rulării acestui script obținem fișierul hidden_copy cu modificările realizate pentru a afișa mesajul success:

$ ./rewrite_exec.sh
test_offset: 0x1080
success_message_offset: 0x10a0
Generated updated executable in hidden_copy.

$ ./hidden_copy
success
Give number: ^C

În acest moment avem realizat cu succes primul apel al funcției hidden_function() în urmă căruia este apelat mesajul success.

Pentru mod programatic și avansat de analiză și modificare de executabile putem folosi LIEF sau Modulul ''pwnlib.elf.elf'' din pwntools.

Al doilea apel de funcție: analiză dinamică

Folosind doar analiză statică (lucru pe executabil) am realizat primul apel reușit al funcției hidden_function(). Urmărim să realizăm și al doilea apel. Pentru al doilea apel depindem de informații aleatoare, a căror valoarea este știută doar la rulare (run time). De aceea avem nevoie de analiză dinamică, adică investigația folosind un debugger, în cazul nostru GDB.

Pentru a apela corespunzător a doua oară funcția hidden_function(), trebuie ca cele două condiții if (liniile 11 și 26 din fișierul hello.c) să fie satisfăcute. Pentru aceasta trebuie:

  • să aflăm valorile r1 și r2 generate aleator
  • să transmitem la intrarea standard în variabila in valoarea variabilei r1 ca să satisfacem condiția if de la linia 26
  • să suprascriem valoarea variabilei test cu valorea variabilei r2 ca să satisfacem condiția if de la linia 11

Ne vom folosi de facilitățile GDB pentru acest lucru:

  • folosirea de breakpoint-uri și suspendarea execuției programului/procesului într-un punct convenabil
  • aflarea adreselor simbolurilor din cadrul programului
  • investigarea conținutului memoriei programului/procesului
  • suprascrierea conțiinutului memoriei programului/procesului

Adică vom urma pașii:

  1. Vom suspenda execuția programului după generarea celor două numere aleatoare r1 și r2.
  2. Vom afla valorile celor două numere.
  3. Vom afla adresa variabilei test în memorie.
  4. Vom suprascrie valoarea variabilei test cu valoarea variabilei r2 ca să satisfacem condiția if de la linia 11.
  5. Vom continua execuția programului și la citirea valorii de la intrarea standard (prin intermediul funcției read_uint()) vom transmite valoarea variabilei r1 ca să satisfacem condiția if de la linia 26.
  6. Programul își va continua execuția și va apela corespunzător funcția hidden_function() care va apela a doua oară mesajul success.

Pașii sunt urmați în GDB în secvența de mai jos:

$ gdb -q ./hidden_copy
Reading symbols from ./hidden_copy...done.
(gdb) start
Temporary breakpoint 1 at 0x400702: file hidden.c, line 20.
Starting program: /home/student/so/demo/exec-process/hidden/hidden_copy

Temporary breakpoint 1, main () at hidden.c:20
20              hidden_function(100);
(gdb) set disassembly-flavor intel
(gdb) disass
Dump of assembler code for function main:
   0x00000000004006fa <+0>:     push   rbp
   0x00000000004006fb <+1>:     mov    rbp,rsp
   0x00000000004006fe <+4>:     sub    rsp,0x10
=> 0x0000000000400702 <+8>:     mov    edi,0x64
   0x0000000000400707 <+13>:    call   0x4006d7 <hidden_function>
   0x000000000040070c <+18>:    mov    edi,0x0
   0x0000000000400711 <+23>:    call   0x4005d0 <time@plt>
   0x0000000000400716 <+28>:    mov    edi,eax
   0x0000000000400718 <+30>:    call   0x4005a0 <srand@plt>
   0x000000000040071d <+35>:    call   0x4005e0 <rand@plt>
   0x0000000000400722 <+40>:    mov    DWORD PTR [rbp-0x4],eax
   0x0000000000400725 <+43>:    call   0x4005e0 <rand@plt>
   0x000000000040072a <+48>:    mov    DWORD PTR [rbp-0x8],eax
   0x000000000040072d <+51>:    mov    edi,0x400834
   0x0000000000400732 <+56>:    call   0x400753 <read_uint>
   0x0000000000400737 <+61>:    mov    DWORD PTR [rbp-0xc],eax
   0x000000000040073a <+64>:    mov    eax,DWORD PTR [rbp-0xc]
   0x000000000040073d <+67>:    cmp    eax,DWORD PTR [rbp-0x4]
   0x0000000000400740 <+70>:    jne    0x40074c <main+82>
   0x0000000000400742 <+72>:    mov    eax,DWORD PTR [rbp-0x8]
   0x0000000000400745 <+75>:    mov    edi,eax
   0x0000000000400747 <+77>:    call   0x4006d7 <hidden_function>
   0x000000000040074c <+82>:    mov    eax,0x0
   0x0000000000400751 <+87>:    leave
   0x0000000000400752 <+88>:    ret
End of assembler dump.
(gdb) b *0x0000000000400732
Breakpoint 2 at 0x400732: file hidden.c, line 25.
(gdb) c
Continuing.
success

Breakpoint 2, 0x0000000000400732 in main () at hidden.c:25
25              in = read_uint("Give number: ");
(gdb) x/2wd $rbp-8
0x7fffffffdc08: 1586380876      1175232931
(gdb) p test
$1 = 100
(gdb) p &test
$2 = (unsigned int *) 0x601080 <test>
(gdb) set *0x601080=1586380876
(gdb) p test
$1 = 1586380876
(gdb) c
Continuing.
Give number: 1175232931
success
[Inferior 1 (process 28011) exited normally]
(gdb) q

Comenzile GDB folosite în secvența de mai sus sunt:

  • start: pentru a porni procesul și a crea un breakpoint la începutul funcției main
    • se apelează prima instanță a funcției hidden_function, se afișează success
  • set disassembly-flavor intel: pentru a dezasambla în sintaxă Intel
  • disass: pentru a dezasambla codul, să urmărim unde putem plasa un breakpoint
  • b *0x0000000000400732: pentru a crea un breakpoint înainte de apelul funcției read_uint; adică *după* ce au fost generate în variabilele r1 și r2
    • variabilele r1 și r2 sunt reținute pe stivă respectiv la adresele rbp-4 și rbp-8; observăm în secvența dezasamblată între adresele 0x40071d și 0x40072a
  • c: pentru continuarea execuției până la breakpoint-ul de mai sus
    • acum suspendat programul la breakpoint, putem citi valorile variabilelor r1 și r2
  • x/2wd $rbp-8: pentru citirea a două (2) numere pe 32 de biți (w) și afișarea lor în format zecimal (d) de la adresa rbp-8, adică valorile de la rbp-8 (r2) și rbp-4 (r1). Rezultă de aici că r1 este 1175232931 și r2 este 1586380876.
  • p test: pentru afișarea valorii variabilei test, inițial 100
  • p &test: pentru afișarea adresei variabilei test, adică 0x601080
  • set *0x601080=1586380876: pentru modificarea valorii de la adresa 0x601080 (adică a variabilei test) în valoarea variabilei r2 (1586380876)
  • p test: pentru verificarea noii valori a variabilei test; se confirmă că am modificat valoarea la 1586380876
  • c: pentru continuarea execuției programului
    • rulează funcția read_uint care solicită furnizarea unui număr ce va fi comparat cu r1; furnizăm valoarea lui r1 adică 1175232931
    • programul continuă rularea, apelează funcția hidden_function, afișează a doua oară success
  • q: pentru încheierea sesiunii curente și închiderea GDB

Alte trucuri

Pașii de mai sus pot fi simplificați prin folosirea în GDB a comenzii call care permite apelul direct al unei funcții. Astfel, la începutul sesiunii GDB a programului, putem declanșa apelul direct al funcției hidden_function ca mai jos:

(gdb) start
Temporary breakpoint 1 at 0x400702: file hidden.c, line 20.
Starting program: /home/student/so/demo/exec-process/hidden/hidden_copy 

Temporary breakpoint 1, main () at hidden.c:20
20              hidden_function(100);
(gdb) call hidden_function(100)
success

În felul acesta putem apela orice funcție din executabil fără a fi nevoie să facem operații mai complicate de investigare și modificare a memoriei.

Ba mai mult, putem apela direct funcția puts:

(gdb) call puts("success")
success
$1 = 8

GDB nu are o interfață mai puțin prietenoasă și lipsuri pe partea de exploiting și reverse engineering. Pentru acestă, vă recomandăm să augmentați GDB cu proiecte dedicate pentru acest lucru:

PEDA este o versiune mai puțin menținută curentă, dar cu o istorie mai lungă în comunitatea de securitatea. pwndbg și GEF sunt proiecte mai recente, cu funcționalități mai multe și comunități active de dezvoltare.

Cele trei proiecte pot fi configurate să fie prezente simultan (dar folosite alternativ) în GDB, așa cum este descris aici (și configurat automat cu acest repository).

so/curs/exec-process.txt · Last modified: 2021/04/14 06:24 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