BREAD(BIOS 逆向工程与高级调试器)是一个“可注入”的实模式 x86 调试器,能够通过串口线从另一台 PC 调试任意实模式代码(在真实硬件上)。
BREAD 源于多次尝试逆向传统 BIOS 的失败经历。鉴于绝大多数(如果不是全部)BIOS 分析都是使用反汇编器静态完成的,理解 BIOS 变得极其困难,因为无法知道给定代码段中寄存器或内存的值。
尽管如此,BREAD 也可以调试实模式下的任意代码,例如可引导代码或 DOS 程序。
快速演示:
https://user-images.githubusercontent.com/8294550/217709970-9007a1e3-7352-470d-a22f-cbb5219d5547.mp4
此调试器分为两部分:调试器(完全用汇编编写,运行在被调试的硬件上)和桥接器,用 C 编写并运行在 Linux 上。
调试器是可注入的代码,以 16 位实模式编写,可以放置在 BIOS ROM 或任何其他实模式代码中。当执行时,它会设置适当的中断处理程序,将处理器置于单步模式,并通过串口等待命令。
另一方面,桥接器是调试器与 GDB 之间的链接。桥接器通过 TCP 与 GDB 通信,并通过串口将请求/响应转发给调试器。桥接器的目的是消除 GDB 数据包的复杂性,并建立更简单的协议与机器通信。此外,更简单的协议使最终代码体积更小,使调试器更容易注入到各种不同的环境中。
如下图所示:
+---------+ 简单数据包 +----------+ GDB 数据包 +---------+
| |--------------->| |--------------->| |
| dbg | | bridge | | gdb |
|(真实硬件)|<---------------| (Linux) |<---------------| (Linux) |
+---------+ 串口 +----------+ TCP +---------+
通过实现 GDB 存根,BREAD 具备许多开箱即用的特性。支持以下命令:
在 GDB 中逆向分析原始二进制文件(如 BIOS)自然意味着没有其原始符号。然而,随着逆向分析过程的推进,用户/程序员/黑客对代码的某些部分有了更好的理解,而静态分析工具(如 IDA、Cutter、Ghidra 等)允许添加注释、函数定义等功能。这些增强显著提高了用户的生产力。
考虑到这一点,项目中提供了一个名为 symbolify.py 的辅助 Python 脚本。给定一个符号列表(地址 标签),它会生成一个最小的 ELF 文件,其中包含这些符号。这个 ELF 文件随后可以加载到 GDB 中,极大地简化调试过程。
符号文件可以包含空白字符、空行、注释(#)以及地址行上的注释。地址可以是十进制或十六进制格式,标签/符号(由一个或多个空白字符分隔)的形式为 [a-z0-9_]+,实例如下(实际示例可在 symbols/ami_ipm41d3.txt 中找到):
#
# 这是一个注释
#
0xdeadbeef my_symbol1
0x123 othersymbol # 这个函数做 xyz
# 十进制地址示例
456 anotherone
例如,考虑 symbols/ami_ipm41d3.txt 中的符号文件,用户可以执行类似以下操作:
$ ./simbolify.py symbols/ami_ipm41d3.txt ip41symbols.elf
然后,在 GDB 中加载它,如下所示:
(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_
注意,即使 GDB 的自动补全也能正常工作,是不是很神奇?
有多少?是的。 由于被调试的代码不知道自己在被调试,它可能以多种方式干扰调试器,仅举几例:
保护模式跳转:如果被调试的代码切换到保护模式,中断处理程序等结构会被改变,调试器将不再被调用。然而,如果跳回实模式(恢复完整先前状态),调试器可能再次工作。
IDT 更改:如果被调试的代码因任何原因更改了 IDT 或其基地址,调试器处理程序将无法正确调用。
栈:BREAD 使用栈并假定栈存在!不应将其插入到栈尚未配置的位置。
对于 BIOS 调试,还有其他限制,例如:无法从最开头(bootblock)调试 BIOS 代码,因为 BREAD 正确运行需要最低限度的设置(如 RAM)。但是,可以通过将 CS:EIP 设置为 F000:FFF0 来执行“热重启”。在这种情况下,可以再次跟踪 BIOS 初始化过程,因为 BREAD 已经正确加载。请注意,热重启期间的 BIOS 初始化“代码路径”可能与冷重启不同,执行流程可能不完全相同。
构建仅需要 GNU Make、C 编译器(如 GCC、Clang 或 TCC)、NASM 以及 Linux 机器。
调试器有两种操作模式:轮询模式(默认)和中断模式:
轮询模式是最简单的方法,在各种环境中应该都能正常工作。然而,由于轮询的特性,CPU 使用率较高:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make
中断模式通过使用 UART 中断接收新数据来优化 CPU 利用率,而不是不断轮询。这导致 CPU 保持在“暂停”状态,直到接收到调试器的命令,从而防止 CPU 占用 100% 的资源。但是,由于中断并非始终启用,此模式未设置为默认选项:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make UART_POLLING=no
使用 BREAD 只需要一根串口线(是的,你的主板 有 COM 接口,请查阅手册)并将代码注入到适当的位置。
要注入,必须在 dbg.asm(调试器的源文件)中进行最小修改。需要更改代码的 'ORG' 以及代码返回的方式(在代码中查找 “>> CHANGE_HERE <<” 以及需要更改的位置)。
以 AMI legacy 为例,将调试器模块放置在 BIOS 徽标位置(0x108200 或 FFFF:8210),并且 ROM 中的以下指令已被替换为对模块的远调用:
...
00017EF2 06 push es
00017EF3 1E push ds
00017EF4 07 pop es
00017EF5 8BD8 mov bx,ax -┐ 替换为:call 0xFFFF:0x8210 (dbg.bin)
00017EF7 B8024F mov ax,0x4f02 -┘
00017EFA CD10 int 0x10
00017EFC 07 pop es
00017EFD C3 ret
...
以下补丁就足够了:
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
值得注意的是,如果你修改了 ROM 中的几条指令来调用调试器代码,那么在从调试器返回之前必须恢复它们。
替换这两条指令的原因是它们在 BIOS 显示徽标之前执行,而徽标现在是调试器,这确保了几个关键点:
找到一个好的位置来调用调试器(BIOS 已充分初始化但不过晚)可能具有挑战性,但这是可能的。
之后,dbg.bin 就可以插入到 ROM 中的正确位置了。
使用 BREAD 调试 DOS 程序有点棘手,但可行:
dbg.asm,使 DOS 将其识别为有效的 DOS 程序:times)int 0x20)以下补丁解决了此问题:
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
创建一个可引导的 FreeDOS(或 DOS)软盘映像,仅包含内核和终端:KERNEL.SYS 和 COMMAND.COM。还要将待调试的程序和 DBG.COM(dbg.bin)添加到该软盘映像中。
创建映像后,应按照以下步骤操作:
bridge 已经打开(请参考下一节的说明)。DBG.COM。DBG.COM 进程继续执行直到结束。需要注意的是,DOS 在进程退出后不会擦除进程映像。因此,调试器可以像任何其他 DOS 程序一样配置,并设置适当的断点。调试器的开头填充了 NOP,因此预计新进程不会覆盖调试器的内存,使其在看似“结束”后仍能继续工作。这使得 BREAD 能够调试其他程序,包括 DOS 本身。
桥接器是调试器与 GDB 之间的粘合剂,可以通过不同方式使用,无论是真实硬件还是虚拟机。
其参数如下:
Usage: ./bridge [options]
Options:
-s 启用通过套接字(而非设备)的串口
-d <path> 替换默认设备路径 (/dev/ttyUSB0)
(如果启用 -s 则无效)
-p <port> 串口(作为套接字),默认:2345
-g <port> GDB 端口,默认:1234
-h 帮助信息
如果未传递任何选项,默认行为是:
./bridge -d /dev/ttyUSB0 -g 1234
推荐的最小用法:
./bridge -s (套接字模式,串口在 2345,GDB 在 1234)
./bridge (设备模式,串口在 /dev/ttyUSB0,GDB 在 1234)
要在真实硬件上使用,只需不带参数调用即可。可选地,可以使用 -d 参数更改设备路径:
./bridge 或 ./bridge -d /path/to/device)Single-stepped, you can now connect GDB!,然后启动 GDB:gdb。在虚拟机中使用时,执行顺序略有变化:
./bridge 或 ./bridge -d /path/to/device)make bochs 或 make qemu)Single-stepped, you can now connect GDB!,然后启动 GDB:gdb。在这两种情况下,请确保在 BRIDGE 根目录下运行 GDB,因为该文件夹中有辅助文件,以便 GDB 在 16 位模式下正常工作。