
Injectable Real-Mode x86-Debugger für BIOS-Reverse-Engineering und Debugging von beliebigem Real-Mode-Code über serielles Kabel, mit GDB-Integration und Hardware-Breakpoint-/Watchpoint-Unterstützung.
BREAD (BIOS Reverse Engineering & Advanced Debugger) ist ein 'injizierbarer' x86-Real-Mode-Debugger, der beliebigen Real-Mode-Code (auf echter Hardware) von einem anderen PC über ein serielles Kabel debuggen kann.
BREAD entstand aus vielen fehlgeschlagenen Versuchen, Legacy-BIOS zu reverse-engineeren. Da die überwiegende Mehrheit – wenn nicht alle – BIOS-Analysen statisch mit Disassemblern durchgeführt werden, wird das Verständnis des BIOS extrem 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, wie bootfähigen Code oder DOS-Programme.
Kurze Demo:
https://user-images.githubusercontent.com/8294550/217709970-9007a1e3-7352-470d-a22f-cbb5219d5547.mp4
CPU-String-Namen mit BREAD ändern
Dieser Debugger ist in zwei Teile aufgeteilt: den Debugger (vollständig in Assembler geschrieben und auf der zu debuggenden Hardware laufend) und die Brücke, in C geschrieben und unter Linux laufend.
Der Debugger ist der injizierbare Code, der in 16-Bit-Real-Mode geschrieben ist und im BIOS-ROM oder in jedem anderen Real-Mode-Code platziert werden kann. Bei Ausführung richtet er die entsprechenden Interrupt-Handler ein, versetzt den Prozessor in den Single-Step-Modus und wartet auf Befehle auf der seriellen Schnittstelle.
Die Brücke hingegen ist die Verbindung zwischen dem Debugger und GDB. Die Brücke kommuniziert mit GDB über TCP und leitet die Anfragen/Antworten über die serielle Schnittstelle an den Debugger weiter. Die Idee hinter der Brücke ist es, die Komplexität der GDB-Pakete zu vermeiden und ein einfacheres Protokoll für die Kommunikation mit der Maschine zu etablieren. Darüber hinaus ermöglicht das einfachere Protokoll eine kleinere endgültige Codegröße, was es dem Debugger erleichtert, in verschiedene Umgebungen injiziert zu werden.
Wie im folgenden Diagramm gezeigt:
+---------+ einfache Pakete +----------+ GDB-Pakete +---------+
| |--------------->| |--------------->| |
| dbg | | bridge | | gdb |
|(echte HW)|<---------------| (Linux) |<---------------| (Linux) |
+---------+ seriell +----------+ TCP +---------+
Durch die Implementierung des GDB-Stubs bietet BREAD viele Funktionen out-of-the-box. Die folgenden Befehle werden unterstützt:
Das Reverse-Engineering eines rohen Binärs, wie eines BIOS, in GDB bedeutet automatisch, dass man keine ursprünglichen Symbole hat. Jedoch gewinnt der Benutzer/Programmierer/Hacker im Laufe des RE-Prozesses ein besseres Verständnis bestimmter Codeteile, und statische Analysetools wie IDA, Cutter, Ghidra und andere ermöglichen das Hinzufügen von Annotationen, Kommentaren, Funktionsdefinitionen und mehr. Diese Verbesserungen steigern die Produktivität des Benutzers erheblich.
Vor diesem Hintergrund gibt es im Projekt ein begleitendes Python-Skript namens symbolify.py. Ausgehend von einer Liste von Symbolen (Adresse Label) generiert es eine minimale ELF-Datei mit diesen Symbolen. Diese ELF kann dann später in GDB geladen 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 sein, und Labels/Symbole (durch ein oder mehrere Leerzeichen getrennt) können die Form [a-z0-9_]+ haben, wie in einem echten Beispiel in symbols/ami_ipm41d3.txt:
#
# Dies ist ein Kommentar
#
0xdeadbeef my_symbol1
0x123 othersymbol # Diese Funktion macht xyz
# Beispiel mit Dezimaladresse
456 anotherone
Zum Beispiel, wenn die Symboldatei unter symbols/ami_ipm41d3.txt verfügbar ist, kann der Benutzer Folgendes tun:
$ ./simbolify.py symbols/ami_ipm41d3.txt ip41symbols.elf
Dann in GDB laden 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 GDB-Autovervollständigung wie erwartet funktioniert, erstaunlich?
Wie viele? Ja. Da der zu debuggende Code nicht weiß, dass er g debuggt wird, kann er den Debugger auf verschiedene Weise stören, um nur einige zu nennen:
Protected-Mode-Sprung: Wenn der debugged Code in den Protected-Mode 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 Rücksprung in den Real-Mode (unter Wiederherstellung des vollständigen vorherigen Zustands) den Debugger wieder funktionsfähig macht.
IDT-Änderungen: Wenn aus irgendeinem Grund der debugged Code die IDT oder ihre Basisadresse ändert, werden die Debugger-Handler nicht ordnungsgemäß aufgerufen.
Stack: BREAD verwendet einen Stack und geht davon aus, dass er existiert! Er sollte nicht an Stellen eingefügt werden, an denen der Stack noch nicht konfiguriert ist.
Für BIOS-Debugging gibt es weitere Einschränkungen, wie: Es ist nicht möglich, den BIOS-Code von Anfang an (Bootblock) zu debuggen, da für die korrekte Funktion von BREAD eine minimale Einrichtung (wie RAM) erforderlich ist. Es ist jedoch möglich, einen "Warm-Neustart" 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 anders sein kann als bei einem Kalt-Neustart und der Ausführungsablauf möglicherweise nicht genau gleich ist.
Zum Erstellen wird nur GNU Make, ein C-Compiler (wie GCC, Clang oder TCC), NASM und eine Linux-Maschine benötigt.
Der Debugger hat zwei Betriebsmodi: Polling (Standard) und Interrupt-basiert:
Der Polling-Modus ist der einfachste Ansatz und sollte in einer Vielzahl von Umgebungen gut funktionieren. Aufgrund der Polling-Natur ergibt sich jedoch eine hohe 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 verwendet, um neue Daten zu empfangen, anstatt ständig zu pollieren. Dies führt dazu, dass die CPU in einem "Halt"-Zustand bleibt, bis sie Befehle vom Debugger empfängt, und somit verhindert wird, dass sie 100 % der CPU-Ressourcen verbraucht. Da Interrupts jedoch nicht immer aktiviert sind, ist dieser Modus nicht als Standard festgelegt:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make UART_POLLING=no
Die Verwendung von BREAD erfordert nur ein serielles Kabel (und ja, Ihr Motherboard hat einen COM-Header, überprüfen Sie das Handbuch) und das Injizieren des Codes an der entsprechenden Stelle.
Zum Injizieren müssen minimale Änderungen in dbg.asm (der Debugger-Quelle) vorgenommen werden. Das 'ORG' des Codes muss geändert werden sowie die Art und Weise, wie der Code zurückkehren soll (suchen Sie nach ">> CHANGE_HERE <<" im Code nach Stellen, die geändert werden müssen).
Am Beispiel eines AMI Legacy, bei dem das Debugger-Modul an der Stelle des BIOS-Logos (0x108200 oder FFFF:8210) platziert wird und die folgenden Anweisungen im ROM durch einen Fernaufruf des Moduls ersetzt wurden:
...
00017EF2 06 push es
00017EF3 1E push ds
00017EF4 07 pop es
00017EF5 8BD8 mov bx,ax -┐ ersetzt durch: call 0xFFFF:0x8210 (dbg.bin)
00017EF7 B8024F mov ax,0x4f02 -┘
00017EFA CD10 int 0x10
00017EFC 07 pop es
00017EFD C3 ret
...
reicht folgender Patch:
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 vor der Rückkehr aus dem Debugger wiederhergestellt werden müssen.
Der Grund für das Ersetzen dieser beiden Anweisungen ist, dass sie unmittelbar vor der Anzeige des Logos auf dem Bildschirm durch das BIOS ausgeführt werden, das nun der Debugger ist, was einige wichtige Punkte sicherstellt:
Eine gute Stelle zum Aufrufen des Debuggers zu finden (wo das BIOS bereits genug initialisiert hat, aber nicht zu spät) kann herausfordernd sein, ist aber möglich.
Danach ist dbg.bin bereit, an der richtigen Position im ROM eingefügt zu werden.
Das Debuggen von DOS-Programmen mit BREAD ist etwas knifflig, aber möglich:
dbg.asm, damit DOS es als gültiges DOS-Programm erkennt:times)int 0x20)Folgender Patch behebt dies:
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) Disk-Image, das nur den Kernel und das Terminal enthält: KERNEL.SYS und COMMAND.COM. Fügen Sie diesem Disk-Image auch das zu debuggende Programm und die DBG.COM (dbg.bin) hinzu.
Die folgenden Schritte sollten nach der Erstellung des Images unternommen werden:
bridge (siehe nächsten Abschnitt für Anweisungen).DBG.COM aus.DBG.COM-Prozess fortsetzen, bis er beendet ist.Es ist wichtig zu beachten, dass DOS das Prozess-Image nach dem Beenden nicht löscht. Daher kann der Debugger wie jedes andere DOS-Programm konfiguriert und die entsprechenden Breakpoints gesetzt werden. Der Anfang des Debuggers ist mit NOPs gefüllt, sodass erwartet wird, dass der neue Prozess den Speicher des Debuggers nicht überschreibt, sodass er auch nach dem scheinbaren "Beenden" weiter funktioniert. Dies ermöglicht BREAD das Debuggen anderer Programme, einschließlich DOS selbst.
Brücke ist die Verbindung zwischen dem Debugger und GDB und kann auf verschiedene Arten verwendet werden, sei es auf echter Hardware oder in einer virtuellen Maschine.
Ihre 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 /pfad/zum/gerät)Single-stepped, you can now connect GDB! und dann GDB starten: gdb.Für die Verwendung in einer virtuellen Maschine ändert sich die Ausführungsreihenfolge leicht:
./bridge oder ./bridge -d /pfad/zum/gerät)make bochs oder make qemu)Single-stepped, you can now connect GDB! und dann GDB starten: gdb.In beiden Fällen stellen Sie sicher, dass Sie GDB im BRIDGE-Stammverzeichnis ausführen, da sich in diesem Ordner Hilfsdateien befinden, damit GDB im 16-Bit-Modus ordnungsgemäß funktioniert.
BREAD ist immer offen für die Community und bereit, Beiträge anzunehmen, sei es mit Issues, Dokumentation, Tests, neuen Funktionen, Fehlerbehebungen, Tippfehlern usw. Willkommen an Bord.
BREAD ist unter der MIT-Lizenz lizenziert. Geschrieben von Davidson Francis und (hoffentlich) anderen Beitragenden.
Breakpoints werden als Hardware-Breakpoints implementiert und haben daher eine begrenzte Anzahl verfügbarer Breakpoints. In der aktuellen Implementierung ist jeweils nur 1 aktiver Breakpoint möglich! ↩
Hardware-Watchpoints (wie Breakpoints) werden ebenfalls nur einzeln unterstützt. ↩
Bitte beachten Sie, dass Debug-Register standardmäßig auf VMs nicht funktionieren. Für bochs muss es mit dem Flag --enable-x86-debugger=yes kompiliert werden. Für Qemu muss es mit aktiviertem KVM ausgeführt werden: --enable-kvm (make qemu erledigt dies bereits). ↩