
주입 가능한 리얼 모드 x86 디버거: BIOS 리버스 엔지니어링 및 직렬 케이블을 통한 임의의 리얼 모드 코드 디버깅용, GDB 통합 및 하드웨어 중단점/와치포인트 지원.
BREAD (BIOS Reverse Engineering & Advanced Debugger)는 다른 PC에서 시리얼 케이블을 통해 임의의 리얼 모드 코드(실제 하드웨어에서)를 디버깅할 수 있는 '주입 가능한' 리얼 모드 x86 디버거입니다.
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 |
|(실제 HW)|<---------------| (Linux) |<---------------| (Linux) |
+---------+ 시리얼 +----------+ TCP +---------+
GDB 스텁을 구현함으로써, BREAD는 기본적으로 많은 기능을 제공합니다. 다음 명령이 지원됩니다:
BIOS와 같은 원시 바이너리를 GDB에서 리버스 엔지니어링한다는 것은 자동으로 원래 심볼이 없음을 의미합니다. 그러나 RE 과정이 진행됨에 따라 사용자/프로그래머/해커는 코드의 특정 부분에 대한 이해가 높아지며, IDA, Cutter, Ghidra 등의 정적 분석 도구는 주석, 함수 정의 등을 추가할 수 있게 해줍니다. 이러한 향상은 사용자의 생산성을 크게 높여줍니다.
이를 염두에 두고, 프로젝트에는 symbolify.py라는 동반 Python 스크립트가 있습니다. 심볼 리스트(주소 레이블)가 주어지면 이들 심볼이 추가된 최소 ELF 파일을 생성합니다. 이 ELF는 나중에 GDB에 로드되어 디버깅 과정을 크게 단순화하는 데 사용될 수 있습니다.
심볼 파일은 공백, 빈 줄, 주석(#) 및 주소 줄의 주석을 포함할 수 있습니다. 주소는 10진수 또는 16진수 형식일 수 있으며, 레이블/심볼(하나 이상의 공백 문자로 구분)은 [a-z0-9_]+ 형식일 수 있습니다. 실제 예제는 symbols/ami_ipm41d3.txt에서 찾을 수 있습니다.
#
# 이것은 주석입니다
#
0xdeadbeef my_symbol1
0x123 othersymbol # 이 함수는 xyz를 수행합니다
# 10진수 주소 예제
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 디버깅의 경우, 다른 제한 사항도 있습니다: BREAD가 올바르게 작동하려면 최소한의 설정(예: RAM)이 필요하므로 BIOS 코드를 처음부터(부트블록) 디버깅할 수 없습니다. 그러나 CS:EIP를 F000:FFF0으로 설정하여 "웜 리부트"를 수행할 수 있습니다. 이 시나리오에서 BREAD가 이미 로드되어 있으므로 BIOS 초기화를 다시 따라갈 수 있습니다. 웜 리부트 중 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 레거시를 예로 들면, 디버거 모듈이 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 편집: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 ; 약간의 거리를 유지,
+ ; 40kB면 충분함
%include "constants.inc"
@@ -140,7 +143,7 @@ _start:
; >> CHANGE_HERE <<
; Overwritten BIOS instructions below (if any)
- nop
+ int 0x20 ; DOS 인터럽트로 프로세스 종료
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비트에서 올바르게 작동하기 위한 보조 파일이 있습니다.
BREAD는 항상 커뮤니티에 열려 있으며, 이슈, 문서, 테스트, 새로운 기능, 버그 수정, 오타 등 모든 기여를 환영합니다. 함께해 주세요.
BREAD는 MIT 라이선스로 제공됩니다. Davidson Francis와 (희망적으로) 다른 기여자가 작성했습니다.