BREAD (BIOS Reverse Engineering & Advanced Debugger) ist ein „injizierbarer“ Real-Mode-x86-Debugger, der beliebigen Real-Mode-Code (auf echter Hardware) von einem anderen PC über ein serielles Kabel debuggen kann.
BREAD entstand aus vielen gescheiterten Versuchen, das alte BIOS zurückzuentwickeln. Angesichts der Tatsache, dass die überwiegende Mehrheit – wenn nicht alle – BIOS-Analysen statisch mithilfe von Disassemblern durchgeführt werden, wird das Verständnis des BIOS äußerst schwierig, da es keine Möglichkeit gibt, den Wert von Registern oder Speicher in einem bestimmten Codeabschnitt zu kennen.
Trotzdem kann BREAD auch beliebigen Code im Real-Modus debuggen, beispielsweise bootfähigen Code oder DOS-Programme.
Kurze Demo:
CPU-Stringnamen über BREAD ändern
Dieser Debugger ist in zwei Teile unterteilt: den Debugger (vollständig in Assembler geschrieben und auf der zu debuggenden Hardware ausgeführt) und die Bridge, die in C geschrieben ist und unter Linux ausgeführt wird.
Der Debugger ist der injizierbare Code, der im 16-Bit-Realmodus geschrieben ist und im BIOS-ROM oder jedem anderen Realmoduscode platziert werden kann. Bei der Ausführung richtet es die entsprechenden Interrupt-Handler ein, versetzt den Prozessor in den Einzelschrittmodus und wartet auf Befehle an der seriellen Schnittstelle.
Die Bridge hingegen ist die Verbindung zwischen dem Debugger und GDB. Die Bridge kommuniziert über TCP mit GDB und leitet die Anfragen/Antworten über die serielle Schnittstelle an den Debugger weiter. Die Idee hinter der Brücke besteht darin, die Komplexität von GDB-Paketen zu beseitigen und ein einfacheres Protokoll für die Kommunikation mit der Maschine zu etablieren. Darüber hinaus ermöglicht das einfachere Protokoll eine geringere endgültige Codegröße, wodurch der Debugger leichter in verschiedene Umgebungen eingefügt werden kann.
Wie im folgenden Diagramm dargestellt:
+---------+ simple packets +----------+ GDB packets +---------+
| | --------------- > | | --------------- > | |
| dbg | | bridge | | gdb |
| ( real HW ) | <- -------------- | ( Linux ) | <- -------------- | ( Linux ) |
+---------+ serial +----------+ TCP +---------+
Durch die Implementierung des GDB-Stubs verfügt BREAD über viele sofort einsatzbereite Funktionen. Die folgenden Befehle werden unterstützt:
Das Reverse Engineering einer rohen Binärdatei, beispielsweise eines BIOS, in GDB bedeutet automatisch, dass die ursprünglichen Symbole nicht vorhanden sind. Mit fortschreitendem RE-Prozess gewinnt der Benutzer/Programmierer/Hacker jedoch ein besseres Verständnis für bestimmte Teile des Codes, und statische Analysetools wie IDA, Cutter, Ghidra und andere ermöglichen das Hinzufügen von Anmerkungen, Kommentaren, Funktionsdefinitionen usw. und mehr. Diese Verbesserungen steigern die Produktivität des Benutzers erheblich.
Aus diesem Grund gibt es im Projekt ein begleitendes Python-Skript namens symbolify.py
. Anhand einer Liste von Symbolen (Adressetikett) wird eine minimale ELF-Datei mit diesen hinzugefügten Symbolen generiert. Dieses ELF kann dann später in GDB geladen und verwendet werden, um den Debugging-Prozess erheblich zu vereinfachen.
Die Symboldatei kann Leerzeichen, Leerzeilen, Kommentare (#) und Kommentare in der Adresszeile enthalten. Adressen können im Dezimal- oder Hexadezimalformat vorliegen und Beschriftungen/Symbole (getrennt durch ein oder mehrere Leerzeichen) können die Form [a-z0-9_]+ haben, wie in (ein reales Beispiel finden Sie in symbols/ami_ipm41d3. txt):
#
# This is a comment
#
0xdeadbeef my_symbol1
0x123 othersymbol # This function does xyz
# Example with decimal address
456 anotherone
Betrachtet man beispielsweise die Symboldatei, die unter „symbols/ami_ipm41d3.txt“ verfügbar ist, kann der Benutzer Folgendes tun:
$ ./simbolify.py symbols/ami_ipm41d3.txt ip41symbols.elf
Laden Sie es dann in GDB wie folgt:
(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_
Beachten Sie, dass sogar die automatische GDB-Vervollständigung wie erwartet funktioniert, erstaunlich?
Wie viele? Ja. Da der zu debuggende Code nicht weiß, dass er debuggt wird, kann er den Debugger auf verschiedene Weise stören, um nur einige zu nennen:
Sprung in den geschützten Modus: Wenn der debuggte Code in den geschützten Modus wechselt, werden die Strukturen für Interrupt-Handler usw. geändert und der Debugger wird an dieser Stelle im Code nicht mehr aufgerufen. Es ist jedoch möglich, dass ein Sprung zurück in den Real-Modus (Wiederherstellung des vollständigen vorherigen Zustands) dazu führt, dass der Debugger wieder funktioniert.
IDT-Änderungen: Wenn der debuggte Code aus irgendeinem Grund den IDT oder seine Basisadresse ändert, werden die Debugger-Handler nicht ordnungsgemäß aufgerufen.
Stack: BREAD verwendet einen Stack und geht davon aus, dass dieser existiert! Es sollte nicht an Stellen eingefügt werden, an denen der Stapel noch nicht konfiguriert wurde.
Für das BIOS-Debugging gibt es weitere Einschränkungen, wie zum Beispiel: Es ist nicht möglich, den BIOS-Code von Anfang an zu debuggen (Bootblock), da eine Mindestkonfiguration (z. B. RAM) erforderlich ist, damit BREAD ordnungsgemäß funktioniert. Es ist jedoch möglich, einen „Warmneustart“ durchzuführen, indem CS:EIP auf F000:FFF0
gesetzt wird. In diesem Szenario kann die BIOS-Initialisierung erneut verfolgt werden, da BREAD bereits ordnungsgemäß geladen ist. Bitte beachten Sie, dass der „Codepfad“ der BIOS-Initialisierung während eines Warm-Neustarts sich möglicherweise von einem Kalt-Neustart unterscheidet und der Ausführungsablauf möglicherweise nicht genau derselbe ist.
Für die Erstellung sind lediglich GNU Make, ein C-Compiler (wie GCC, Clang oder TCC), NASM und eine Linux-Maschine erforderlich.
Der Debugger verfügt über zwei Betriebsmodi: Polling (Standard) und Interrupt-basiert:
Der Abfragemodus ist der einfachste Ansatz und sollte in einer Vielzahl von Umgebungen gut funktionieren. Aufgrund des Polling-Charakters kommt es jedoch zu einer hohen CPU-Auslastung:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make
Der Interrupt-basierte Modus optimiert die CPU-Auslastung, indem er UART-Interrupts nutzt, um neue Daten zu empfangen, anstatt diese ständig abzufragen. Dies führt dazu, dass die CPU im „Halt“-Zustand bleibt, bis sie Befehle vom Debugger empfängt, und somit verhindert, dass sie 100 % der CPU-Ressourcen verbraucht. Da Interrupts jedoch nicht immer aktiviert sind, ist dieser Modus nicht als Standardoption festgelegt:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make UART_POLLING=no
Für die Verwendung von BREAD ist lediglich ein serielles Kabel (und ja, Ihr Motherboard verfügt über einen COM-Header, schauen Sie im Handbuch nach) und das Einfügen des Codes an der entsprechenden Stelle erforderlich.
Zum Injizieren müssen minimale Änderungen in dbg.asm (dem Quellcode des Debuggers) vorgenommen werden. Die „ORG“ des Codes muss geändert werden und auch die Art und Weise, wie der Code zurückgegeben werden soll (suchen Sie im Code nach „ >> CHANGE_HERE <<
“ für Stellen, die geändert werden müssen).
Am Beispiel einer AMI-Legacy-Version wird das Debugger-Modul anstelle des BIOS-Logos ( 0x108200
oder FFFF:8210
) platziert und die folgenden Anweisungen im ROM wurden durch einen Fernaufruf an das Modul ersetzt:
...
00017EF2 06 push es
00017EF3 1E push ds
00017EF4 07 pop es
00017EF5 8BD8 mov bx , ax - ┐ replaced by: call 0xFFFF : 0x8210 (dbg.bin)
00017EF7 B8024F mov ax , 0x4f02 - ┘
00017EFA CD10 int 0x10
00017EFC 07 pop es
00017EFD C3 ret
...
der folgende Patch reicht aus:
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 <<
; Overwritten BIOS instructions below (if any)
- nop
- nop
+ mov ax, 0x4F02
+ int 0x10
nop
nop
Es ist wichtig zu beachten, dass, wenn Sie einige Anweisungen in Ihrem ROM geändert haben, um den Debugger-Code aufzurufen, diese wiederhergestellt werden müssen, bevor Sie vom Debugger zurückkehren.
Der Grund für das Ersetzen dieser beiden Anweisungen besteht darin, dass sie unmittelbar vor der Anzeige des Logos durch das BIOS auf dem Bildschirm ausgeführt werden, das nun als Debugger fungiert, wodurch einige wichtige Punkte sichergestellt werden:
Es kann schwierig sein, einen guten Ort zum Aufrufen des Debuggers zu finden (wo das BIOS bereits ausreichend initialisiert wurde, aber nicht zu spät), aber es ist möglich.
Danach kann dbg.bin
an der richtigen Position im ROM eingefügt werden.
Das Debuggen von DOS-Programmen mit BREAD ist etwas knifflig, aber möglich:
dbg.asm
so, dass DOS es als gültiges DOS-Programm erkennt:times
).int 0x20
)Der folgende Patch behebt dieses Problem:
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 ; keep some distance,
+ ; 40kB should be enough
%include "constants.inc"
@@ -140,7 +143,7 @@ _start:
; >> CHANGE_HERE <<
; Overwritten BIOS instructions below (if any)
- nop
+ int 0x20 ; DOS interrupt to exit process
nop
Erstellen Sie ein bootfähiges FreeDOS- (oder DOS-)Disketten-Image, das nur den Kernel und das Terminal enthält: KERNEL.SYS
und COMMAND.COM
. Fügen Sie diesem Disketten-Image außerdem das zu debuggende Programm und DBG.COM
( dbg.bin
) hinzu.
Nach der Erstellung des Bildes sollten folgende Schritte durchgeführt werden:
bridge
(Anweisungen finden Sie im nächsten Abschnitt).DBG.COM
aus.DBG.COM
-Prozess bis zum Abschluss weiterlaufen.Es ist wichtig zu beachten, dass DOS das Prozessabbild nach dem Beenden nicht löscht. Dadurch kann der Debugger wie jedes andere DOS-Programm konfiguriert und die entsprechenden Haltepunkte gesetzt werden. Der Anfang des Debuggers ist mit NOPs gefüllt, daher wird davon ausgegangen, dass der neue Prozess den Speicher des Debuggers nicht überschreibt, sodass dieser auch dann weiter funktionieren kann, wenn er scheinbar „abgeschlossen“ ist. Dadurch kann BREaD andere Programme, einschließlich DOS selbst, debuggen.
Bridge ist das Bindeglied zwischen dem Debugger und GDB und kann auf unterschiedliche Weise verwendet werden, sei es auf realer Hardware oder einer virtuellen Maschine.
Seine Parameter sind:
Usage: ./bridge [options]
Options:
-s Enable serial through socket, instead of device
-d <path> Replaces the default device path (/dev/ttyUSB0)
(does not work if -s is enabled)
-p <port> Serial port (as socket), default: 2345
-g <port> GDB port, default: 1234
-h This help
If no options are passed the default behavior is:
./bridge -d /dev/ttyUSB0 -g 1234
Minimal recommended usages:
./bridge -s (socket mode, serial on 2345 and GDB on 1234)
./bridge (device mode, serial on /dev/ttyUSB0 and GDB on 1234)
Um es auf echter Hardware zu verwenden, rufen Sie es einfach ohne Parameter auf. Optional können Sie den Gerätepfad mit dem Parameter -d
ändern:
./bridge
oder ./bridge -d /path/to/device
)Single-stepped, you can now connect GDB!
und starten Sie dann GDB: gdb
. Für den Einsatz in einer virtuellen Maschine ändert sich die Ausführungsreihenfolge geringfügig:
./bridge
oder ./bridge -d /path/to/device
)make bochs
oder make qemu
“).Single-stepped, you can now connect GDB!
und dann GDB starten: gdb
.Stellen Sie in beiden Fällen sicher, dass Sie GDB im BRIDGE-Stammordner ausführen, da sich in diesem Ordner Hilfsdateien befinden, damit GDB in 16-Bit ordnungsgemäß funktioniert.
BREAD ist immer offen für die Community und bereit, Beiträge anzunehmen, sei es bei Problemen, Dokumentation, Tests, neuen Funktionen, Bugfixes, Tippfehlern usw. Willkommen an Bord.
BREAD ist unter der MIT-Lizenz lizenziert. Geschrieben von Davidson Francis und (hoffentlich) anderen Mitwirkenden.
Haltepunkte werden als Hardware-Haltepunkte implementiert und verfügen daher über eine begrenzte Anzahl verfügbarer Haltepunkte. In der aktuellen Implementierung immer nur 1 aktiver Haltepunkt! ↩
Hardware-Watchpoints (wie Haltepunkte) werden ebenfalls jeweils nur einzeln unterstützt. ↩
Bitte beachten Sie, dass Debug-Register auf VMs nicht standardmäßig funktionieren. Für Bochs muss es mit dem Flag --enable-x86-debugger=yes
kompiliert werden. Für Qemu muss es mit aktiviertem KVM laufen: --enable-kvm
( make qemu
tut dies bereits). ↩