
Depurador x86 em modo real injetável para engenharia reversa de BIOS e depuração de código arbitrário em modo real via cabo serial, com integração GDB e suporte a breakpoints/watchpoints de hardware.
BREAD (BIOS Reverse Engineering & Advanced Debugger) é um depurador x86 em modo real 'injetável' que pode depurar código arbitrário em modo real (em hardware real) de outro PC via cabo serial.
O BREAD surgiu de muitas tentativas frustradas de fazer engenharia reversa de BIOS legadas. Dado que a grande maioria — senão todas — das análises de BIOS é feita estaticamente usando desmontadores, entender a BIOS se torna extremamente difícil, já que não há como saber o valor dos registradores ou da memória em um determinado trecho de código.
Apesar disso, o BREAD também pode depurar código arbitrário em modo real, como código inicializável ou programas DOS também.
Demonstração rápida:
https://user-images.githubusercontent.com/8294550/217709970-9007a1e3-7352-470d-a22f-cbb5219d5547.mp4
Alterando o nome da string da CPU via BREAD
Este depurador é dividido em duas partes: o depurador (escrito inteiramente em assembly e executando no hardware que está sendo depurado) e a ponte (bridge), escrita em C e executando no Linux.
O depurador é o código injetável, escrito em modo real de 16 bits, e pode ser colocado dentro da ROM da BIOS ou em qualquer outro código em modo real. Quando executado, ele configura os manipuladores de interrupção apropriados, coloca o processador em modo de passo único e aguarda comandos na porta serial.
A ponte, por outro lado, é o elo entre o depurador e o GDB. A ponte se comunica com o GDB via TCP e encaminha as requisições/respostas para o depurador através da porta serial. A ideia por trás da ponte é remover a complexidade dos pacotes GDB e estabelecer um protocolo mais simples para se comunicar com a máquina. Além disso, o protocolo mais simples permite que o tamanho final do código seja menor, facilitando a injeção do depurador em diversos ambientes diferentes.
Conforme mostrado no diagrama a seguir:
+---------+ pacotes simples +----------+ Pacotes GDB +---------+
| |---------------->| |---------------->| |
| dbg | | bridge | | gdb |
|(HW real)|<----------------| (Linux) |<----------------| (Linux) |
+---------+ serial +----------+ TCP +---------+
Ao implementar o stub GDB, o BREAD possui muitos recursos prontos para uso. Os seguintes comandos são suportados:
Fazer engenharia reversa de um binário bruto, como uma BIOS, no GDB automaticamente implica em não ter seus símbolos originais. No entanto, à medida que o processo de RE avança, o usuário/programador/hacker adquire uma melhor compreensão de certas partes do código, e ferramentas de análise estática como IDA, Cutter, Ghidra e outras permitem a adição de anotações, comentários, definições de funções e muito mais. Esses aprimoramentos aumentam significativamente a produtividade do usuário.
Pensando nisso, existe um script Python complementar no projeto chamado symbolify.py. Dada uma lista de símbolos (endereço rótulo), ele gera um arquivo ELF mínimo com esses símbolos adicionados. Este ELF pode então ser carregado posteriormente no GDB e usado para simplificar bastante o processo de depuração.
O arquivo de símbolos pode incluir espaços em branco, linhas em branco, comentários (#) e comentários na linha de endereço. Endereços podem estar em formato decimal ou hexadecimal, e rótulos/símbolos (separados por um ou mais caracteres de espaço em branco) podem ter a forma [a-z0-9_]+, como em (um exemplo real pode ser encontrado em symbols/ami_ipm41d3.txt):
#
# Isto é um comentário
#
0xdeadbeef meu_simbolo1
0x123 outrosimbolo # Esta função faz xyz
# Exemplo com endereço decimal
456 maisum
Por exemplo, considerando o arquivo de símbolos disponível em symbols/ami_ipm41d3.txt, o usuário pode fazer algo como:
$ ./simbolify.py symbols/ami_ipm41d3.txt ip41symbols.elf
Em seguida, carregue-o no GDB conforme:
(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_
Note que até o auto-completar do GDB funciona como esperado, incrível?
Quantos? Sim. Como o código que está sendo depurado não sabe que está sendo depurado, ele pode interferir no depurador de várias maneiras, para citar algumas:
Salto para modo protegido: Se o código depurado mudar para modo protegido, as estruturas para manipuladores de interrupção, etc., são alteradas e o depurador não será mais invocado naquele ponto do código. No entanto, é possível que um salto de volta ao modo real (restaurando o estado anterior completo) permita que o depurador funcione novamente.
Mudanças na IDT: Se por qualquer motivo o código depurado alterar a IDT ou seu endereço base, os manipuladores do depurador não serão invocados adequadamente.
Pilha: O BREAD usa uma pilha e assume que ela existe! Ele não deve ser inserido em locais onde a pilha ainda não foi configurada.
Para depuração de BIOS, existem outras limitações, como: não é possível depurar o código da BIOS desde o início (bootblock), pois uma configuração mínima (como RAM) é necessária para que o BREAD funcione corretamente. No entanto, é possível realizar um "warm-reboot" definindo CS:EIP para F000:FFF0. Neste cenário, a inicialização da BIOS pode ser acompanhada novamente, pois o BREAD já está devidamente carregado. Observe que o "caminho-código" da inicialização da BIOS durante um warm-reboot pode ser diferente de um cold-reboot e o fluxo de execução pode não ser exatamente o mesmo.
A compilação requer apenas GNU Make, um compilador C (como GCC, Clang ou TCC), NASM e uma máquina Linux.
O depurador tem dois modos de operação: polling (padrão) e baseado em interrupção:
O modo polling é a abordagem mais simples e deve funcionar bem em diversos ambientes. No entanto, devido à natureza de polling, há um alto uso da CPU:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make
O modo baseado em interrupção otimiza o uso da CPU utilizando interrupções UART para receber novos dados, em vez de fazer polling constante. Isso faz com que a CPU permaneça em estado de 'halt' até receber comandos do depurador, evitando assim que consuma 100% dos recursos da CPU. No entanto, como as interrupções nem sempre estão habilitadas, este modo não é definido como opção padrão:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make UART_POLLING=no
Usar o BREAD requer apenas um cabo serial (e sim, sua placa-mãe tem um conector COM, verifique o manual) e injetar o código no local apropriado.
Para injetar, devem ser feitas alterações mínimas no dbg.asm (fonte do depurador). O 'ORG' do código deve ser alterado e também como o código deve retornar (procure por ">> CHANGE_HERE <<" no código para locais que precisam ser alterados).
Usando uma AMI legacy como exemplo, onde o módulo depurador será colocado no lugar do logotipo da BIOS (0x108200 ou FFFF:8210) e as seguintes instruções na ROM foram substituídas por uma chamada longa (far call) para o módulo:
...
00017EF2 06 push es
00017EF3 1E push ds
00017EF4 07 pop es
00017EF5 8BD8 mov bx,ax -┐ substituído por: call 0xFFFF:0x8210 (dbg.bin)
00017EF7 B8024F mov ax,0x4f02 -┘
00017EFA CD10 int 0x10
00017EFC 07 pop es
00017EFD C3 ret
...
o seguinte patch é 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
É importante notar que, se você alterou algumas instruções dentro de sua ROM para invocar o código do depurador, elas devem ser restauradas antes de retornar do depurador.
O motivo para substituir estas duas instruções é que elas são executadas pouco antes de a BIOS exibir o logotipo na tela, que agora é o depurador, garantindo alguns pontos-chave:
Encontrar um bom local para chamar o depurador (onde a BIOS já tenha inicializado o suficiente, mas não tarde demais) pode ser desafiador, mas é possível.
Depois disso, dbg.bin está pronto para ser inserido na posição correta da ROM.
Depurar programas DOS com o BREAD é um pouco complicado, mas possível:
dbg.asm para que o DOS o entenda como um programa DOS válido:times)int 0x20)O seguinte patch resolve isso:
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 ; mantenha alguma distância,
+ ; 40kB deve ser suficiente
%include "constants.inc"
@@ -140,7 +143,7 @@ _start:
; >> CHANGE_HERE <<
; Overwritten BIOS instructions below (if any)
- nop
+ int 0x20 ; Interrupção do DOS para encerrar processo
nop
Crie uma imagem de disquete inicializável do FreeDOS (ou DOS) contendo apenas o kernel e o terminal: KERNEL.SYS e COMMAND.COM. Adicione também a esta imagem de disquete o programa a ser depurado e o DBG.COM (dbg.bin).
Os seguintes passos devem ser tomados após criar a imagem:
bridge já aberta (consulte a próxima seção para instruções).DBG.COM.DBG.COM continue até terminar.É importante notar que o DOS não apaga a imagem do processo após sua saída. Como resultado, o depurador pode ser configurado como qualquer outro programa DOS e os pontos de parada apropriados podem ser definidos. O início do depurador é preenchido com NOPs, então é esperado que o novo processo não sobrescreva a memória do depurador, permitindo que ele continue funcionando mesmo após parecer que "terminou". Isso permite que o BREAD depure outros programas, incluindo o próprio DOS.
A ponte é o elo entre o depurador e o GDB e pode ser usada de diferentes maneiras, seja em hardware real ou em máquina virtual.
Seus parâmetros são:
Uso: ./bridge [opções]
Opções:
-s Ativa serial através de socket, em vez de dispositivo
-d <caminho> Substitui o caminho do dispositivo padrão (/dev/ttyUSB0)
(não funciona se -s estiver ativado)
-p <porta> Porta serial (como socket), padrão: 2345
-g <porta> Porta GDB, padrão: 1234
-h Esta ajuda
Se nenhuma opção for passada, o comportamento padrão é:
./bridge -d /dev/ttyUSB0 -g 1234
Usos mínimos recomendados:
./bridge -s (modo socket, serial na 2345 e GDB na 1234)
./bridge (modo dispositivo, serial em /dev/ttyUSB0 e GDB na 1234)
Para usar em hardware real, basta invocá-lo sem parâmetros. Opcionalmente, você pode alterar o caminho do dispositivo com o parâmetro -d:
./bridge ou ./bridge -d /caminho/do/dispositivo)Single-stepped, you can now connect GDB! e então inicie o GDB: gdb.Para uso em máquina virtual, a ordem de execução muda ligeiramente:
./bridge ou ./bridge -d /caminho/do/dispositivo)make bochs ou make qemu)Single-stepped, you can now connect GDB! e então inicie o GDB: gdb.Em ambos os casos, certifique-se de executar o GDB dentro da pasta raiz da BRIDGE, pois há arquivos auxiliares nesta pasta para o GDB funcionar corretamente em 16 bits.
O BREAD está sempre aberto à comunidade e disposto a aceitar contribuições, seja com issues, documentação, testes, novas funcionalidades, correções de bugs, erros de digitação, etc. Seja bem-vindo a bordo.
O BREAD é licenciado sob a Licença MIT. Escrito por Davidson Francis e (esperançosamente) outros contribuidores.
Pontos de parada são implementados como pontos de parada de hardware e, portanto, têm um número limitado de pontos de parada disponíveis. Na implementação atual, apenas 1 ponto de parada ativo por vez! ↩
Pontos de observação de hardware (assim como pontos de parada) também são suportados apenas um de cada vez. ↩
Observe que os registradores de depuração não funcionam por padrão em VMs. Para o bochs, ele precisa ser compilado com a flag --enable-x86-debugger=yes. Para o Qemu, precisa ser executado com o KVM habilitado: --enable-kvm (make qemu já faz isso). ↩