Software e tools

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-user funzioni, è 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:

  1. imposta l’interprete dei comandi specificato dalla command line
    • gdb può essere eseguito in modalità diverse:
      • batch, esegue una lista di comandi e aspetta il termine.
      • quite, non printa i messaggi introduttivi e di copyright.
  2. 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)
  3. carica i simboli del programma debuggato
  4. 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 path
  • show paths, mostra il contenuto di path
  • show 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 location imposta un breakpoint alla location specificata.
  • break imposta un breakpoint all’istruzione successiva
  • break [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 123
  • p foo, stampa il valore del simbolo foo.
  • p *(int *)0x1234, stampa un int letto dall’indirizzo 0x1234.
  • p $rax, stampa il registro rax.
  • p/x = hex, p/d = decimale

Ispezione della memoria #

x/nfu <addr>

  • n, numero di word da stampare
  • f, formato di visualizzazione:
    • x, hex
    • d, decimale
    • i, istruzione
  • u, dimensione word
    • b, 1 byte
    • h, 2 byte
    • w, 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.