
Debugger x86 in modalità reale iniettabile per reverse engineering di BIOS e debugging di codice arbitrario in modalità reale tramite cavo seriale, con integrazione GDB e supporto per breakpoint/watchpoint hardware.
BREAD (BIOS Reverse Engineering & Advanced Debugger) è un debugger x86 in modalità reale 'iniettabile' in grado di eseguire il debug di codice arbitrario in modalità reale (su hardware reale) da un altro PC tramite cavo seriale.
BREAD è nato da molti tentativi falliti di fare reverse engineering di BIOS legacy. Dato che la stragrande maggioranza – se non tutta – l'analisi dei BIOS viene eseguita staticamente usando disassemblatori, comprendere il BIOS diventa estremamente difficile, poiché non c'è modo di conoscere il valore dei registri o della memoria in un dato pezzo di codice.
Nonostante ciò, BREAD può anche eseguire il debug di codice arbitrario in modalità reale, come codice avviabile o programmi DOS.
Demo rapida:
https://user-images.githubusercontent.com/8294550/217709970-9007a1e3-7352-470d-a22f-cbb5219d5547.mp4
Cambiare il nome della stringa della CPU tramite BREAD
Questo debugger è diviso in due parti: il debugger (scritto interamente in assembly e in esecuzione sull'hardware sottoposto a debug) e il bridge, scritto in C e in esecuzione su Linux.
Il debugger è il codice iniettabile, scritto in modalità reale a 16 bit, e può essere posizionato all'interno della ROM del BIOS o in qualsiasi altro codice in modalità reale. Quando viene eseguito, imposta i gestori di interrupt appropriati, mette il processore in modalità a passo singolo e attende comandi sulla porta seriale.
Il bridge, d'altra parte, è il collegamento tra il debugger e GDB. Il bridge comunica con GDB tramite TCP e inoltra le richieste/risposte al debugger attraverso la porta seriale. L'idea alla base del bridge è quella di rimuovere la complessità dei pacchetti GDB e stabilire un protocollo più semplice per comunicare con la macchina. Inoltre, il protocollo più semplice consente di ridurre le dimensioni finali del codice, rendendo più facile l'iniettabilità del debugger in vari ambienti diversi.
Come mostrato nel seguente diagramma:
+---------+ pacchetti semplici +----------+ Pacchetti GDB +---------+
| |------------------->| |------------------>| |
| dbg | | bridge | | gdb |
|(HW reale)|<-------------------| (Linux) |<------------------| (Linux) |
+---------+ seriale +----------+ TCP +---------+
Implementando lo stub GDB, BREAD ha molte funzionalità integrate. Sono supportati i seguenti comandi:
Fare reverse engineering di un binario grezzo, come un BIOS, in GDB implica automaticamente non avere i suoi simboli originali. Tuttavia, con l'avanzare del processo di RE, l'utente/programmatore/hacker acquisisce una migliore comprensione di alcune parti del codice, e strumenti di analisi statica come IDA, Cutter, Ghidra e altri consentono di aggiungere annotazioni, commenti, definizioni di funzioni e altro. Questi miglioramenti aumentano notevolmente la produttività dell'utente.
Con questo in mente, nel progetto è presente uno script Python complementare chiamato symbolify.py. Data una lista di simboli (etichette di indirizzo), genera un file ELF minimale con questi simboli aggiunti. Questo ELF può poi essere caricato in GDB successivamente e utilizzato per semplificare notevolmente il processo di debug.
Il file dei simboli può includere spazi bianchi, righe vuote, commenti (#) e commenti sulla riga dell'indirizzo. Gli indirizzi possono essere in formato decimale o esadecimale, e le etichette/simboli (separati da uno o più caratteri di spazio bianco) possono essere della forma [a-z0-9_]+, come in (un esempio reale si trova in symbols/ami_ipm41d3.txt):
#
# Questo è un commento
#
0xdeadbeef mio_simbolo1
0x123 altro_simbolo # Questa funzione fa xyz
# Esempio con indirizzo decimale
456 ancora_uno
Ad esempio, considerando il file dei simboli disponibile in symbols/ami_ipm41d3.txt, l'utente può fare qualcosa come:
$ ./symbolify.py symbols/ami_ipm41d3.txt ip41symbols.elf
Quindi, caricarlo in GDB come in:
(gdb) add-symbol-file ip41symbols.elf 0
add symbol table from file "ip41symbols.elf" at
.text_addr = 0x0
(y or n) y
Reading symbols from ip41symbols.elf...
(No debugging symbols found in ip41symbols.elf)
(gdb) p cseg_
cseg_change_video_mode_logo cseg_get_cpuname
(gdb) p cseg_
Notate che anche il completamento automatico di GDB funziona come previsto, sorprendente?
Quante? Sì. Poiché il codice sottoposto a debug non è consapevole di essere sottoposto a debug, può interferire con il debugger in diversi modi, per citarne alcuni:
Salto in modalità protetta: Se il codice sottoposto a debug passa alla modalità protetta, le strutture per i gestori di interrupt, ecc. vengono alterate e il debugger non verrà più invocato a quel punto del codice. Tuttavia, è possibile che un salto di ritorno alla modalità reale (ripristinando lo stato precedente completo) permetta al debugger di funzionare di nuovo.
Modifiche IDT: Se per qualsiasi motivo il codice sottoposto a debug modifica la IDT o il suo indirizzo base, i gestori del debugger non verranno invocati correttamente.
Stack: BREAD utilizza uno stack e presuppone che esista! Non dovrebbe essere inserito in posizioni in cui lo stack non è ancora stato configurato.
Per il debug del BIOS, ci sono altre limitazioni come: non è possibile eseguire il debug del codice del BIOS dall'inizio (bootblock), poiché è necessaria una configurazione minima (come la RAM) affinché BREAD funzioni correttamente. Tuttavia, è possibile eseguire un "warm-reboot" impostando CS:EIP su F000:FFF0. In questo scenario, l'inizializzazione del BIOS può essere seguita di nuovo, poiché BREAD è già stato caricato correttamente. Si noti che il "percorso del codice" dell'inizializzazione del BIOS durante un warm-reboot potrebbe essere diverso da un cold-reboot e il flusso di esecuzione potrebbe non essere esattamente lo stesso.
Per compilare sono necessari solo GNU Make, un compilatore C (come GCC, Clang o TCC), NASM e una macchina Linux.
Il debugger ha due modalità operative: polling (predefinita) e basata su interrupt:
La modalità polling è l'approccio più semplice e dovrebbe funzionare bene in una varietà di ambienti. Tuttavia, a causa della natura del polling, l'utilizzo della CPU è elevato:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make
La modalità basata su interrupt ottimizza l'utilizzo della CPU utilizzando gli interrupt UART per ricevere nuovi dati, invece di eseguire continuamente il polling. Ciò fa sì che la CPU rimanga in uno stato di 'halt' fino alla ricezione di comandi dal debugger, impedendo così di consumare il 100% delle risorse della CPU. Tuttavia, poiché gli interrupt non sono sempre abilitati, questa modalità non è impostata come opzione predefinita:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make UART_POLLING=no
Usare BREAD richiede solo un cavo seriale (e sì, la vostra scheda madre ha un header COM, consultate il manuale) e l'iniezione del codice nella posizione appropriata.
Per iniettare, è necessario apportare modifiche minime in dbg.asm (il sorgente del debugger). L''ORG' del codice deve essere cambiato e anche come il codice deve tornare (cercate ">> CHANGE_HERE <<" nel codice per i punti che necessitano di modifiche).
Usando un AMI legacy come esempio, dove il modulo debugger sarà posizionato al posto del logo del BIOS (0x108200 o FFFF:8210) e le seguenti istruzioni nella ROM sono state sostituite con una chiamata far al modulo:
...
00017EF2 06 push es
00017EF3 1E push ds
00017EF4 07 pop es
00017EF5 8BD8 mov bx,ax -┐ sostituito da: call 0xFFFF:0x8210 (dbg.bin)
00017EF7 B8024F mov ax,0x4f02 -┘
00017EFA CD10 int 0x10
00017EFC 07 pop es
00017EFD C3 ret
...
la seguente patch è sufficiente:
diff --git a/dbg.asm b/dbg.asm
index caedb70..88024d3 100644
--- a/dbg.asm
+++ b/dbg.asm
@@ -21,7 +21,7 @@
; SOFTWARE.
[BITS 16]
-[ORG 0x0000] ; >> CHANGE_HERE <<
+[ORG 0x8210] ; >> CHANGE_HERE <<
%include "constants.inc"
@@ -140,8 +140,8 @@ _start:
; >> CHANGE_HERE <<
; Istruzioni BIOS sovrascritte di seguito (se presenti)
- nop
- nop
+ mov ax, 0x4F02
+ int 0x10
nop
nop
È importante notare che se avete modificato alcune istruzioni all'interno della vostra ROM per invocare il codice del debugger, devono essere ripristinate prima di tornare dal debugger.
Il motivo per cui queste due istruzioni vengono sostituite è che vengono eseguite appena prima che il BIOS visualizzi il logo sullo schermo, che ora è il debugger, garantendo alcuni punti chiave:
Trovare una buona posizione per chiamare il debugger (dove il BIOS ha già inizializzato abbastanza, ma non troppo tardi) può essere impegnativo, ma è possibile.
Dopo questo, dbg.bin è pronto per essere inserito nella posizione corretta nella ROM.
Il debug dei programmi DOS con BREAD è un po' complicato, ma possibile:
dbg.asm in modo che DOS lo riconosca come un programma DOS valido:times)int 0x20)La seguente patch risolve questo:
diff --git a/dbg.asm b/dbg.asm
index caedb70..b042d35 100644
--- a/dbg.asm
+++ b/dbg.asm
@@ -21,7 +21,10 @@
; SOFTWARE.
[BITS 16]
-[ORG 0x0000] ; >> CHANGE_HERE <<
+[ORG 0x100]
+
+times 40*1024 db 0x90 ; mantenere una certa distanza,
+ ; 40 kB dovrebbero bastare
%include "constants.inc"
@@ -140,7 +143,7 @@ _start:
; >> CHANGE_HERE <<
; Istruzioni BIOS sovrascritte di seguito (se presenti)
- nop
+ int 0x20 ; Interrupt DOS per uscire dal processo
nop
Create un'immagine floppy FreeDOS (o DOS) avviabile contenente solo il kernel e il terminale: KERNEL.SYS e COMMAND.COM. Aggiungete anche a questa immagine floppy il programma da eseguire in debug e DBG.COM (dbg.bin).
I seguenti passaggi dovrebbero essere eseguiti dopo aver creato l'immagine:
bridge già aperto (fare riferimento alla sezione successiva per le istruzioni).DBG.COM.DBG.COM di continuare fino al termine.È importante notare che DOS non cancella l'immagine del processo dopo che questo termina. Di conseguenza, il debugger può essere configurato come qualsiasi altro programma DOS e possono essere impostati i punti di interruzione appropriati. L'inizio del debugger è riempito con NOP, quindi si prevede che il nuovo processo non sovrascriva la memoria del debugger, permettendogli di continuare a funzionare anche dopo che sembra essere "terminato". Ciò consente a BREaD di eseguire il debug di altri programmi, incluso DOS stesso.
Bridge è la colla tra il debugger e GDB e può essere utilizzato in diversi modi, sia su hardware reale che su macchina virtuale.
I suoi parametri sono:
Utilizzo: ./bridge [opzioni]
Opzioni:
-s Abilita la seriale tramite socket, invece del dispositivo
-d <percorso> Sostituisce il percorso predefinito del dispositivo (/dev/ttyUSB0)
(non funziona se -s è abilitato)
-p <porta> Porta seriale (come socket), default: 2345
-g <porta> Porta GDB, default: 1234
-h Questo aiuto
Se non vengono passate opzioni, il comportamento predefinito è:
./bridge -d /dev/ttyUSB0 -g 1234
Utilizzi minimi consigliati:
./bridge -s (modalità socket, seriale sulla porta 2345 e GDB sulla 1234)
./bridge (modalità dispositivo, seriale su /dev/ttyUSB0 e GDB sulla 1234)
Per utilizzarlo su hardware reale, è sufficiente invocarlo senza parametri. Opzionalmente, è possibile modificare il percorso del dispositivo con il parametro -d:
./bridge o ./bridge -d /percorso/dispositivo)Single-stepped, you can now connect GDB! e quindi avviate GDB: gdb.Per l'uso in una macchina virtuale, l'ordine di esecuzione cambia leggermente:
./bridge o ./bridge -d /percorso/dispositivo)make bochs o make qemu)Single-stepped, you can now connect GDB! e quindi avviate GDB: gdb.In entrambi i casi, assicuratevi di eseguire GDB all'interno della cartella principale di BRIDGE, poiché in questa cartella sono presenti file ausiliari necessari affinché GDB funzioni correttamente in modalità 16 bit.
BREAD è sempre aperto alla comunità e disposto ad accettare contributi, sia con issue, documentazione, test, nuove funzionalità, correzioni di bug, refusi, ecc. Benvenuti a bordo.
BREAD è concesso in licenza con licenza MIT. Scritto da Davidson Francis e (si spera) altri contributori.
I punti di interruzione sono implementati come breakpoint hardware e pertanto hanno un numero limitato di punti di interruzione disponibili. Nell'implementazione attuale, solo 1 punto di interruzione attivo alla volta! ↩
Anche i watchpoint hardware (come i punti di interruzione) sono supportati solo uno alla volta. ↩
Si noti che i registri di debug non funzionano per impostazione predefinita sulle VM. Per bochs, deve essere compilato con il flag --enable-x86-debugger=yes. Per Qemu, deve essere eseguito con KVM abilitato: --enable-kvm (make qemu lo fa già). ↩