BREAD (BIOS Reverse Engineering & Advanced Debugger) é um depurador x86 de modo real 'injetável' que pode depurar código arbitrário de modo real (em HW real) de outro PC via cabo serial.
O BREAD surgiu de muitas tentativas fracassadas de engenharia reversa de BIOS legado. Dado que a grande maioria - senão todas - da análise do BIOS é feita estaticamente usando desmontadores, entender o BIOS torna-se extremamente difícil, já que não há como saber o valor dos registros 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.
Demonstração rápida:
Alterando o nome da string da CPU via BREAD
Este depurador é dividido em duas partes: o depurador (escrito inteiramente em assembly e rodando no hardware que está sendo depurado) e a ponte, escrita em C e rodando em Linux.
O depurador é o código injetável, escrito em modo real de 16 bits e pode ser colocado na ROM do BIOS ou em qualquer outro código de modo real. Quando executado, ele configura os manipuladores de interrupção apropriados, coloca o processador no modo de etapa única 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 solicitações/respostas ao depurador através da porta serial. A ideia por trás da ponte é eliminar a complexidade dos pacotes GDB e estabelecer um protocolo mais simples para comunicação com a máquina. Além disso, o protocolo mais simples permite que o tamanho final do código seja menor, tornando mais fácil para o depurador ser injetável em vários ambientes diferentes.
Conforme mostrado no diagrama a seguir:
+---------+ simple packets +----------+ GDB packets +---------+
| | --------------- > | | --------------- > | |
| dbg | | bridge | | gdb |
| ( real HW ) | <- -------------- | ( Linux ) | <- -------------- | ( Linux ) |
+---------+ serial +----------+ TCP +---------+
Ao implementar o stub GDB, o BREAD tem muitos recursos prontos para uso. Os seguintes comandos são suportados:
A engenharia reversa de um binário bruto, como um BIOS, no GDB implica automaticamente em não ter seus símbolos originais. No entanto, à medida que o processo de RE avança, o usuário/programador/hacker ganha 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. Essas melhorias aumentam significativamente a produtividade do usuário.
Com isso em mente, há um script Python complementar no projeto chamado symbolify.py
. Dada uma lista de símbolos (etiqueta de endereço), ele gera um arquivo ELF mínimo com esses símbolos adicionados. Este ELF pode então ser carregado no GDB posteriormente e usado para simplificar bastante o processo de depuração.
O arquivo de símbolo pode incluir espaços em branco, linhas em branco, comentários (#) e comentários na linha de endereço. Os endereços podem estar no formato decimal ou hexadecimal, e os rótulos/símbolos (separados por um ou mais caracteres de espaço em branco) podem ter o formato [a-z0-9_]+, como em (um exemplo real pode ser encontrado em símbolos/ami_ipm41d3. TXT):
#
# This is a comment
#
0xdeadbeef my_symbol1
0x123 othersymbol # This function does xyz
# Example with decimal address
456 anotherone
Por exemplo, considerando o arquivo de símbolos disponível em símbolos/ami_ipm41d3.txt, o usuário pode fazer algo como:
$ ./simbolify.py symbols/ami_ipm41d3.txt ip41symbols.elf
Em seguida, carregue-o no GDB como em:
(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_
Observe que até o preenchimento automático do GDB funciona conforme o 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 diversas maneiras, para citar algumas:
Salto no modo protegido: Se o código depurado mudar para o modo protegido, as estruturas para manipuladores de interrupção, etc. serão alteradas e o depurador não será mais invocado naquele ponto do código. No entanto, é possível que voltar ao modo real (restaurar todo o estado anterior) permita que o depurador funcione novamente.
Alterações de IDT: se por algum motivo o código depurado alterar o IDT ou seu endereço base, os manipuladores do depurador não serão invocados corretamente.
Pilha: BREAD usa uma pilha e assume que ela existe! Não deve ser inserido em locais onde a pilha ainda não tenha sido configurada.
Para depuração do BIOS, existem outras limitações como: não é possível depurar o código do BIOS desde o início (bootblock), pois é necessária uma configuração mínima (como RAM) para que o BREAD funcione corretamente. No entanto, é possível realizar uma "reinicialização a quente" configurando CS:EIP como F000:FFF0
. Neste cenário, a inicialização do BIOS pode ser seguida novamente, pois o BREAD já está devidamente carregado. Observe que o "caminho do código" de inicialização do BIOS durante uma reinicialização a quente pode ser diferente de uma reinicialização a frio e o fluxo de execução pode não ser exatamente o mesmo.
A construção requer apenas GNU Make, um compilador C (como GCC, Clang ou TCC), NASM e uma máquina Linux.
O depurador possui 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 vários ambientes. No entanto, devido à natureza da pesquisa, há um alto uso da CPU:
$ git clone https://github.com/Theldus/BREAD.git
$ cd BREAD/
$ make
O modo baseado em interrupção otimiza a utilização da CPU utilizando interrupções UART para receber novos dados, em vez de pesquisá-los constantemente. Isso faz com que a CPU permaneça em estado 'parado' até receber comandos do depurador, evitando assim que consuma 100% dos recursos da CPU. Porém, 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 possui um conector COM, verifique o manual) e a injeção do código no local apropriado.
Para injetar, alterações mínimas devem ser feitas em dbg.asm (o src 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 os locais que precisam ser alterados).
Usando um legado AMI como exemplo, onde o módulo depurador será colocado no lugar do logotipo do BIOS ( 0x108200
ou FFFF:8210
) e as seguintes instruções na ROM foram substituídas por uma chamada distante para o 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
...
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 observar que se você alterou algumas instruções em sua ROM para invocar o código do depurador, elas deverão ser restauradas antes de retornar do depurador.
O motivo da substituição dessas duas instruções é que elas são executadas logo antes do BIOS exibir o logotipo na tela, que agora é o depurador, garantindo alguns pontos-chave:
Encontrar um bom local para chamar o depurador (onde o BIOS já foi inicializado o suficiente, mas não tarde demais) pode ser um desafio, mas é possível.
Depois disso, dbg.bin
está pronto para ser inserido na posição correta na ROM.
Depurar programas DOS com 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 patch a seguir aborda 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 ; 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
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
).
As seguintes etapas devem ser executadas após a criação da imagem:
bridge
já aberta (consulte a próxima seção para obter instruções).DBG.COM
.DBG.COM
continue até terminar.É importante observar 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 interrupção apropriados podem ser definidos. O início do depurador é preenchido com NOPs, portanto prevê-se que o novo processo não sobrescreva a memória do depurador, permitindo que ele continue funcionando mesmo depois de parecer "terminado". Isso permite que o BREAD depure outros programas, incluindo o próprio DOS.
Bridge é a cola entre o depurador e o GDB e pode ser usado de diversas maneiras, seja em hardware real ou em máquina virtual.
Seus parâmetros são:
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 usá-lo 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 /path/to/device
)Single-stepped, you can now connect GDB!
e então inicie o GDB: gdb
. Para uso em uma máquina virtual, a ordem de execução muda um pouco:
./bridge
ou ./bridge -d /path/to/device
)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 do BRIDGE, pois nesta pasta existem arquivos auxiliares para que o GDB funcione corretamente em 16 bits.
O BREAD está sempre aberto à comunidade e disposto a aceitar contribuições, seja com problemas, documentação, testes, novos recursos, correções de bugs, erros de digitação e etc.
BREAD é licenciado sob licença MIT. Escrito por Davidson Francis e (espero) outros colaboradores.
Os pontos de interrupção são implementados como pontos de interrupção de hardware e, portanto, possuem um número limitado de pontos de interrupção disponíveis. Na implementação atual, apenas 1 ponto de interrupção ativo por vez! ↩
Os watchpoints de hardware (como pontos de interrupção) também são suportados apenas um de cada vez. ↩
Observe que os registros de depuração não funcionam por padrão nas VMs. Para bochs, ele precisa ser compilado com o sinalizador --enable-x86-debugger=yes
. Para Qemu, ele precisa rodar com KVM habilitado: --enable-kvm
( make qemu
já faz isso). ↩