
Depurador x86 en modo real inyectable para ingeniería inversa de BIOS y depuración de código arbitrario en modo real a través de cable serie, con integración GDB y soporte de puntos de interrupción y observación por hardware.
BREAD (BIOS Reverse Engineering & Advanced Debugger) es un depurador x86 en modo real 'inyectable' que puede depurar código arbitrario en modo real (en hardware real) desde otro PC mediante un cable serie.
BREAD surgió de muchos intentos fallidos de realizar ingeniería inversa a BIOS heredadas. Dado que la gran mayoría —si no todas— de los análisis de BIOS se realizan de forma estática usando desensambladores, entender la BIOS se vuelve extremadamente difícil, ya que no hay forma de conocer el valor de los registros o la memoria en un fragmento de código determinado.
A pesar de esto, BREAD también puede depurar código arbitrario en modo real, como código de arranque o programas DOS.
Demostración rápida:
https://user-images.githubusercontent.com/8294550/217709970-9007a1e3-7352-470d-a22f-cbb5219d5547.mp4
Cambiando el nombre de la cadena de la CPU mediante BREAD
Este depurador se divide en dos partes: el depurador (escrito completamente en ensamblador y ejecutándose en el hardware que se está depurando) y el puente (bridge), escrito en C y ejecutándose en Linux.
El depurador es el código inyectable, escrito en modo real de 16 bits, y puede colocarse dentro de la ROM de la BIOS o en cualquier otro código en modo real. Cuando se ejecuta, configura los manejadores de interrupción apropiados, pone el procesador en modo de paso a paso (single-step) y espera comandos en el puerto serie.
El puente, por otro lado, es el enlace entre el depurador y GDB. El puente se comunica con GDB a través de TCP y reenvía las solicitudes/respuestas al depurador mediante el puerto serie. La idea detrás del puente es eliminar la complejidad de los paquetes GDB y establecer un protocolo más simple para comunicarse con la máquina. Además, el protocolo más simple permite que el tamaño final del código sea más pequeño, facilitando que el depurador pueda inyectarse en varios entornos diferentes.
Como se muestra en el siguiente diagrama:
+---------+ simple packets +----------+ GDB packets +---------+
| |--------------->| |--------------->| |
| dbg | | bridge | | gdb |
|(real HW)|<---------------| (Linux) |<---------------| (Linux) |
+---------+ serial +----------+ TCP +---------+
Al implementar el stub de GDB, BREAD tiene muchas características listas para usar. Se admiten los siguientes comandos:
Realizar ingeniería inversa de un binario sin procesar, como una BIOS, en GDB implica automáticamente no tener sus símbolos originales. Sin embargo, a medida que avanza el proceso de RE, el usuario/programador/hacker obtiene una mejor comprensión de ciertas partes del código, y herramientas de análisis estático como IDA, Cutter, Ghidra y otras permiten agregar anotaciones, comentarios, definiciones de funciones y más. Estas mejoras aumentan significativamente la productividad del usuario.
Con esto en mente, hay un script Python complementario en el proyecto llamado symbolify.py. Dada una lista de símbolos (dirección etiqueta), genera un archivo ELF mínimo con estos símbolos agregados. Este ELF luego puede cargarse en GDB y usarse para simplificar enormemente el proceso de depuración.
El archivo de símbolos puede incluir espacios en blanco, líneas en blanco, comentarios (#) y comentarios en la línea de dirección. Las direcciones pueden estar en formato decimal o hexadecimal, y las etiquetas/símbolos (separados por uno o más caracteres de espacio en blanco) pueden tener la forma [a-z0-9_]+, como en (un ejemplo real se puede encontrar en symbols/ami_ipm41d3.txt):
#
# This is a comment
#
0xdeadbeef my_symbol1
0x123 othersymbol # This function does xyz
# Example with decimal address
456 anotherone
Por ejemplo, considerando el archivo de símbolos disponible en symbols/ami_ipm41d3.txt, el usuario puede hacer algo como:
$ ./simbolify.py symbols/ami_ipm41d3.txt ip41symbols.elf
Luego, cargarlo en GDB de la siguiente manera:
(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_
Observa que incluso el autocompletado de GDB funciona como se espera, ¿increíble?
¿Cuántas? Sí. Dado que el código que se está depurando no sabe que está siendo depurado, puede interferir con el depurador de varias maneras, por nombrar algunas:
Salto a modo protegido: Si el código depurado cambia a modo protegido, las estructuras para los manejadores de interrupción, etc. se alteran y el depurador ya no será invocado en ese punto del código. Sin embargo, es posible que un salto de vuelta al modo real (restaurando el estado anterior completo) permita que el depurador vuelva a funcionar.
Cambios en la IDT: Si por alguna razón el código depurado cambia la IDT o su dirección base, los manejadores del depurador no se invocarán correctamente.
Pila: ¡BREAD usa una pila y asume que existe! No debe insertarse en lugares donde la pila aún no se ha configurado.
Para la depuración de BIOS, existen otras limitaciones como: no es posible depurar el código de la BIOS desde el principio (bootblock), ya que se requiere una configuración mínima (como RAM) para que BREAD funcione correctamente. Sin embargo, es posible realizar un 'reinicio en caliente' estableciendo CS:EIP en F000:FFF0. En este escenario, se puede seguir la inicialización de la BIOS nuevamente, ya que BREAD ya está correctamente cargado. Ten en cuenta que la 'ruta de código' de la inicialización de la BIOS durante un reinicio en caliente puede ser diferente de un reinicio en frío y el flujo de ejecución puede no ser exactamente el mismo.
Compilar solo requiere GNU Make, un compilador de C (como GCC, Clang o TCC), NASM y una máquina Linux.
El depurador tiene dos modos de funcionamiento: por sondeo (polling, por defecto) y basado en interrupciones:
El modo por sondeo es el enfoque más simple y debería funcionar bien en diversos entornos. Sin embargo, debido a la naturaleza del sondeo, hay un alto uso de CPU:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make
El modo basado en interrupciones optimiza el uso de la CPU al utilizar interrupciones UART para recibir nuevos datos, en lugar de sondear constantemente. Esto hace que la CPU permanezca en estado de 'halt' hasta recibir comandos del depurador, evitando así que consuma el 100% de los recursos de la CPU. Sin embargo, como las interrupciones no siempre están habilitadas, este modo no se establece como opción predeterminada:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make UART_POLLING=no
Usar BREAD solo requiere un cable serie (y sí, tu placa base tiene un conector COM, consulta el manual) e inyectar el código en la ubicación adecuada.
Para inyectar, se deben realizar cambios mínimos en dbg.asm (el código fuente del depurador). Se debe cambiar el 'ORG' del código y también cómo debe retornar el código (busca ">> CHANGE_HERE <<" en el código para los lugares que necesitan ser cambiados).
Usando una AMI legacy como ejemplo, donde el módulo del depurador se colocará en lugar del logo de la BIOS (0x108200 o FFFF:8210) y las siguientes instrucciones en la ROM han sido reemplazadas con una llamada lejana al módulo:
...
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
...
el siguiente parche es suficiente:
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 importante tener en cuenta que si has alterado algunas instrucciones dentro de tu ROM para invocar el código del depurador, deben restaurarse antes de retornar del depurador.
La razón para reemplazar estas dos instrucciones es que se ejecutan justo antes de que la BIOS muestre el logo en la pantalla, que ahora es el depurador, asegurando algunos puntos clave:
Encontrar una buena ubicación para llamar al depurador (donde la BIOS ya se haya inicializado lo suficiente, pero no demasiado tarde) puede ser desafiante, pero es posible.
Después de esto, dbg.bin está listo para ser insertado en la posición correcta de la ROM.
Depurar programas DOS con BREAD es un poco complicado, pero posible:
dbg.asm para que DOS lo entienda como un programa DOS válido:times)int 0x20)El siguiente parche aborda esto:
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
Crea una imagen de disquete de FreeDOS (o DOS) que contenga solo el núcleo y el terminal: KERNEL.SYS y COMMAND.COM. Agrega también a esta imagen de disquete el programa a depurar y el DBG.COM (dbg.bin).
Se deben seguir los siguientes pasos después de crear la imagen:
bridge ya abierto (consulta la siguiente sección para instrucciones).DBG.COM.DBG.COM continúe hasta que termine.Es importante tener en cuenta que DOS no borra la imagen del proceso después de que sale. Como resultado, el depurador se puede configurar como cualquier otro programa DOS y se pueden establecer los puntos de interrupción apropiados. El inicio del depurador está lleno de NOPs, por lo que se anticipa que el nuevo proceso no sobrescribirá la memoria del depurador, permitiéndole continuar funcionando incluso después de que parezca haber 'terminado'. Esto permite que BREAD depure otros programas, incluido el propio DOS.
El puente es el pegamento entre el depurador y GDB y se puede usar de diferentes maneras, ya sea en hardware real o en una máquina virtual.
Sus parámetros son:
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)
Para usarlo en hardware real, simplemente invócalo sin parámetros. Opcionalmente, puedes cambiar la ruta del dispositivo con el parámetro -d:
./bridge o ./bridge -d /ruta/al/dispositivo)Single-stepped, you can now connect GDB! y luego lanza GDB: gdb.Para usar en una máquina virtual, el orden de ejecución cambia ligeramente:
./bridge o ./bridge -d /ruta/al/dispositivo)make bochs o make qemu)Single-stepped, you can now connect GDB! y luego lanza GDB: gdb.En ambos casos, asegúrate de ejecutar GDB dentro de la carpeta raíz de BRIDGE, ya que hay archivos auxiliares en esta carpeta para que GDB funcione correctamente en 16 bits.
BREAD siempre está abierto a la comunidad y dispuesto a aceptar contribuciones, ya sea con problemas, documentación, pruebas, nuevas características, correcciones de errores, errores tipográficos, etc. Bienvenido a bordo.
BREAD tiene licencia MIT. Escrito por Davidson Francis y (con suerte) otros contribuyentes.
Los puntos de interrupción se implementan como puntos de interrupción hardware y por lo tanto tienen un número limitado de puntos disponibles. ¡En la implementación actual, solo 1 punto de interrupción activo a la vez! ↩
Los puntos de vigilancia hardware (como los puntos de interrupción) también solo se admiten uno a la vez. ↩
Ten en cuenta que los registros de depuración no funcionan por defecto en máquinas virtuales. Para bochs, debe compilarse con la opción --enable-x86-debugger=yes. Para Qemu, debe ejecutarse con KVM habilitado: --enable-kvm (make qemu ya hace esto). ↩