Software e tools #
Installare GDB #
A seconda del sistema operativo che il lettore sta utilizzando le istruzioni
per l’installazione sono differenti. In generale, se siete su una distro
Linux, basta cercare gdb all’interno del proprio package manager.
Consigliamo di utilizzare Linux o WSL per seguire la wiki al meglio.
Installare pwndbg
#
Rimandiamo al link della documentazione ufficiale. In ogni caso dovrebbero essere sufficienti le seguenti operazioni sia su piattaforma x86/x86_64 che ARM.
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh
Il comando setup.sh cercherà di installare pwndbg nel modo più semplice
possibile e adatto alla piattaforma in cui ci si trova.
ARM e Mac M1 #
Il paragrafo seguente si riferisce a Ubuntu Server 20.04 LTS versione ARM. Non è l’approccio consigliato. Sebbene l’accoppiata
gdb-multiarch+qemu-userfunzioni, è molto limitata. Non supporta un numero elevato di funzionalità (ASLR ad esempio) che troviamo nativamente.Se sei su Mac con processore M1 suggerisco di usare la guida per emulare Ubuntu Server x86 su M1 che trovi qui
Se possedete un Mac con uno degli ultimi processori, vi toccherà installare in macchina virtuale una distribuzione Linux, consiglio Ubuntu Server 20.04 LTS.
Dato che analizzeremo e lavoreremo principalmente con binari x86 e
x86-64 dovremo prendere qualche accorgimento.
Su Ubuntu 20.04 LTS Server, le operazioni da eseguire sono le seguenti
sudo apt install qemu-user
sudo apt install gdb-multiarch
sudo apt install libc6-amd64-i386-cross \
binutils-x86-64-linux-gnu \
gcc-x86-64-linux-gnu \
gcc-i686-linux-gnu
Nel file .bashrc bisogna aggiungere i comandi seguenti alla fine del file
export QEMU_LD_PREFIX=/usr/i686-linux-gnu/
# objdump per x86_64
alias objdump=x86_64-linux-gnu-objdump
alias gdb="pwn debug --exec "
# Se non già presente
export PATH=$PATH:~/.local/bin
In teoria ora siamo in grado di eseguire la maggior parte dei binari che useremo per esercitarci.
Estrarre informazioni da un file ELF #
strings
#
strings è un tool da terminale che permette di raccogliere tutte le
stringhe stampabili che vi sono in un binario. Per ogni file in input,
strings stampa a video tutte le sequenze di caratteri che sono lunghe
almeno 4 caratteri e sono seguite da un carattere non stampabile.
Le stringhe raccolte possono darci qualche informazione sui segreti e sui
dati utilizzati.
Possiamo provare a usare strings sul binario /bin/bash.
L’output di strings /bin/bash è il seguente:
/lib64/ld-linux-x86-64.so.2
DJ
E %
@B
B82
0A4
@P@@
NR L
@@ 0
($B
`
A!I`
BPRYD
2BT^+
a0Tr
l0zxQ
4/fV
bi[(
... altre stringhe
Notiamo che oltre ad un nome di libreria, vi sono una quantità di caratteri completamente random. Questi fanno parte di qualche struttura del binario e incidentalmente sono anche rappresentabili come caratteri ASCII stampabili, ma non significa che questi debbano per forza aver senso.
Vi sono alcune opzioni utili:
-d/--data, le stringhe sono raccolte solo dalle sezioni dati.-n <num>/--bytes=<num>, stampa solo stringhe con un numero<num>di caratteri (di default è 4)
objdump
#
objdump mostra informazioni su uno o più file oggetto.
Alcune opzioni rilevanti:
-f, mostra informazioni dall’header generale del file.
❯ objdump -f /bin/bash
/bin/bash: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x00000000000218f0
-h, mostra informazioni dai section headers del file oggetto.
❯ objdump -h /bin/bash
/bin/bash: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.gnu.property 00000040 0000000000000338 0000000000000338 00000338 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000000378 0000000000000378 00000378 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .note.ABI-tag 00000020 000000000000039c 000000000000039c 0000039c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gnu.hash 000031d8 00000000000003c0 00000000000003c0 000003c0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynsym 0000b7d8 0000000000003598 0000000000003598 00003598 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .dynstr 0000742e 000000000000ed70 000000000000ed70 0000ed70 2**0
...
-j, mostra il contenuto di specifiche sezioni del binario
❯ objdump -s -j .rodata /bin/bash
/bin/bash: file format elf64-x86-64
Contents of section .rodata:
b3000 01000200 474e5520 62617368 2c207665 ....GNU bash, ve
b3010 7273696f 6e202573 2d282573 290a0078 rsion %s-(%s)..x
b3020 38365f36 342d7063 2d6c696e 75782d67 86_64-pc-linux-g
b3030 6e750047 4e55206c 6f6e6720 6f707469 nu.GNU long opti
b3040 6f6e733a 0a00092d 2d25730a 00536865 ons:...--%s..She
b3050 6c6c206f 7074696f 6e733a0a 00092d25 ll options:...-%
b3060 73206f72 202d6f20 6f707469 6f6e0a00 s or -o option..
b3070 72626173 68004920 68617665 206e6f20 rbash.I have no
b3080 6e616d65 21003f3f 686f7374 3f3f0072 name!.??host??.r
b3090 756e5f6f 6e655f63 6f6d6d61 6e64002d un_one_command.-
b30a0 63004241 53485f45 4e560048 49535446 c.BASH_ENV.HISTF
...
-D, disassemblare i binari
❯ objdump -D a.out | grep main
1158: 48 8d 3d 9c 03 00 00 lea 0x39c(%rip),%rdi # 14fb <main>
115f: ff 15 73 2e 00 00 call *0x2e73(%rip) # 3fd8 <__libc_start_main@GLIBC_2.34>
00000000000014fb <main>:
150e: 74 2f je 153f <main+0x44>
153a: e9 3c 01 00 00 jmp 167b <main+0x180>
15df: 74 19 je 15fa <main+0xff>
15f5: e9 81 00 00 00 jmp 167b <main+0x180>
1640: 74 16 je 1658 <main+0x15d>
1656: eb 23 jmp 167b <main+0x180>
readelf
#
Una utility comoda per mostrare informazioni sui file ELF. Alcune flag comode:
-h, mostra le informazioni contenute nell’header ELF all’inizio del file.-l, mostra le informazioni contenute nei segment headers-s, mostra le informazioni contenute nella symbol table del file.
GDB (GNU debugger) #
GDB è un debugger CLI installato su tantissime piattaforme Unix-like. È sviluppato all’interno del progetto GNU ed è in grado di analizzare molti linguaggi di programmazione (tra i quali C e C++).
Il debugger ci permette di lanciare il programma sotto alcune condizioni controllate e di verificarne le operazioni durante l’esecuzioni. Può essere utilizzato anche per monitorare i cambiamenti delle risorse. Infine, e forse è l’utilizzo che più ci interessa, ci permette di mostrare e modificare il contenuto della memoria del programma o dei registri della CPU.
Nelle nostre challenge useremo GDB coadiuvato da pwndbg, un plugin per
GDB che fornisce alcuni pratici comandi per controllare e visualizzare
strutture particolari in memoria che tornano utili per l’exploitation.
Usare GDB #
gdb è uno strumento a linea di comando e può essere lanciato eseguendo
il programma gdb su terminale.
Puoi debuggare un programma usando: gdb <program> oppure puoi debuggare
un processo in esecuzione con gdb program pid, gdb -p pid.
Quando in esecuzione, gdb esegue i seguenti step:
- imposta l’interprete dei comandi specificato dalla command line
gdbpuò essere eseguito in modalità diverse:- batch, esegue una lista di comandi e aspetta il termine.
- quite, non printa i messaggi introduttivi e di copyright.
- carica i file di configurazione:
- definiscono il comportamento di gdb allo startup
- possono essere globali (a livello di sistema) o locali (relativi alla directory corrente)
- carica i simboli del programma debuggato
- si ferma, aspettando i comandi dell’utente
❯ gdb program
GNU gdb (GDB) 11.2
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 197 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from program...
(No debugging symbols found in program)
pwndbg>
Comandi di GDB #
Un comando di gdb consiste di una singola linea di input che contiene il
nome del comando e una sequenza di parametri.
Il comando run può essere usato per lanciare un programma in gdb.
...
pwndbg> run
Starting program: /home/kriive/program
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Hello, world!
[Inferior 1 (process 7634) exited normally]
pwndbg>
Per impostare gli argomenti del programma che stiamo analizzando in gdb,
possiamo usare set args <primo-argomento> <secondo-argomento>.
Per mostrare gli argomenti passati al programma che stiamo analizzando
possiamo usare show args.
L’environment (ambiente) consiste di un insieme di variabili d’ambiente
che contengono informazioni come lo username, la home directory, il tipo
di terminale e il percorso di ricerca ($PATH) dei binari da lanciare.
Le variabili d’ambiente si possono vedere o modificare da dentro gdb
con i comandi seguenti:
path <directory>, aggiunge la directory alla pathshow paths, mostra il contenuto di pathshow environment [<var>], mostra le variabili d’ambiente (o la variabile d’ambiente specificata)set environment <var> <value>, imposta un valore per la variabile specificata.unset environment <var>, cancella la variabile specificata dall’ambiente.
La principale motivazione per la quale usiamo un debugger è che possiamo stoppare il programma prima che questo termini, per controllare il suo status e investigare eventuali problemi.
Dentro a gdb un programma si può fermare a causa:
- un breakpoint
- un segnale
- il completamento dell’esecuzione di uno step
Breakpoints #
Un breakpoint sospende l’esecuzione del programma quando un certo punto del programma viene raggiunto.
Un watchpoint è un tipo speciale di breakpoint che ferma il programma quando il valore di un’espressione cambia.
Un catchpoint è un altro breakpoint speciale che ferma il programma quando un certo tipo di eventi è avvenuto.
In gdb i breakpoint sono impostati con il comando break (abbreviato b).
Alcuni comandi per il breakpoint in gdb:
break locationimposta un breakpoint alla location specificata.breakimposta un breakpoint all’istruzione successivabreak [location] if <cond>imposta un breakpoint con la condizione specificata.
La location del breakpoint può essere:
- un numero di linea
- un nome di funzione
- un indirizzo
Per riprendere l’esecuzione possiamo usare continue. Se vogliamo
eseguire la linea successiva e fermarci possiamo usare step o next.
La differenza è che next, passa sopra alle chiamate a funzione.
Esistono poi anche stepi e nexti, che sono uguali a next e step,
ma lavorano a livello di istruzioni macchina.
info breakpoints, mostra i breakpoint.
delete <numero di BP>, elimina un breakpoint.
info registers, mostra i registri. info locals, mostra le variabili
locali. backtrace, mostra il backtrace dello stack di chiamate.
Valutazione di espressioni #
print <espressione C-like>
Ad esempio:
p 100 + 23, stampa 123p foo, stampa il valore del simbolofoo.p *(int *)0x1234, stampa un int letto dall’indirizzo 0x1234.p $rax, stampa il registrorax.p/x= hex,p/d= decimale
Ispezione della memoria #
x/nfu <addr>
n, numero di word da stamparef, formato di visualizzazione:x, hexd, decimalei, istruzione
u, dimensione wordb, 1 byteh, 2 bytew, 4 byte (32 bit)g, 8 byte (64 bit)
addr, espressione per l’indirizzo a cui leggere
Ad esempio x/16xg 0x1234, stampa 16 quadword in hex da 0x1234.
Modifica dello stato #
Possiamo impostare il contenuto di una variabile usando set var foo = 42.
Oppure settare il valore all’interno di un indirizzo di memoria con
set *(int *)0x1234 = 42.
Esempi pratici con gdb
#
Consideriamo il seguente programma di prova.
#include <stdio.h>
#include <stdlib.h>
int fibonacci(int n) {
int k = n;
if (k<=2) {
return 1;
}
return fibonacci(n-1) + fibonacci(n-2);
}
int main(int argc, char** argv) {
int n = 10;
if (argc > 1) {
n = atoi(argv[1]);
}
printf("fib(%d)=%d\n", n, fibonacci(n));
}
Compiliamo il programma in modalità debug, con il seguente comando:
gcc -o fib fib.c -g.
Carichiamolo su gdb, con gdb fib.
Possiamo mettere un breakpoint sulla funzione fibonacci quando k
è uguale a 3, con il seguente comando: break fibonacci if k==3.
Lanciamo il programma con run e aspettiamo che si arresti.
Quando il programma si stoppa, la prima cosa che dobbiamo capire è dove si è fermato e come è arrivato lì! La prima cosa da considerare è il contenuto dello stack.
Vi sono diversi comandi gdb che ci permettono di esaminare lo stack e
leggere il contenuto dello stack e il contenuto di qualsiasi stack frame.
In uno stack frame si può trovare la location della chiamata nel programma, gli argomenti della chiamata, le variabili locali della funzione chiamata.
Si possono analizzare alcune informazioni sullo stack frame usando
i comandi frame e info frame.
Possiamo usare gdb per disassemblare una funzione qualsiasi, usando
il comando disas.
Ghidra #
Ghidra è uno strumento open source per il reverse engineering sviluppato dalla National Security Agency. Ghidra permette di analizzare, disassemblare, decompilare, graficare codice compilato per varie architetture hardware e diversi sistemi operativi.
Come installare #
Per prima cosa serve installare JavaJDK (minimo 11).
Poi scarica una release da qui, estrai il file compresso e lancia ghidra, lancia
lo script ./ghidraRun.