
Na reunião 0x0A DC7831 No dia 16 de fevereiro, apresentamos uma palestra sobre os princípios básicos da emulação de código binário e nosso próprio desenvolvimento — um emulador de plataformas de hardware. .
Neste artigo, descreveremos a execução do firmware do dispositivo no emulador, demonstraremos a interação com o depurador e realizaremos uma pequena análise dinâmica do firmware.
Antecedentes
Uma vez, em uma galáxia muito, muito distante
Há alguns anos, nossa laboratório teve a necessidade de investigar o firmware de um dispositivo. O firmware era compactado, sendo descompactado pelo bootloader. Ele fazia isso de um modo bastante complicado, realocando dados na memória várias vezes. E o próprio firmware interagia ativamente com a periferia. E tudo isso em um núcleo MIPS.
Os emuladores existentes não nos satisfizeram por motivos objetivos, então decidimos criar nosso próprio emulador, que fizesse o mínimo e permitisse descompactar o firmware principal. Tentamos — e deu certo. Pensamos, e se adicionássemos a periferia, para que pudéssemos executar também o firmware principal. Não foi muito difícil — e também deu certo. Pensamos novamente e decidimos criar um emulador completo.
No final, resultou em um emulador de sistemas computacionais. .

Por que Kopycat?
É um trocadilho.
- copycat (inglês, subst. [ˈkɒpɪkæt]) — imitador, seguidor.
- cat (inglês, subst. [ˈkæt]) — gato, felino — o animal de estimação favorito de um dos criadores do projeto.
- A letra 'K' vem da linguagem de programação Kotlin.
Kopycat
Ao criar o emulador, objetivos bem definidos foram estabelecidos:
- a capacidade de criar uma nova periferia, módulo, núcleo de processador rapidamente;
- a capacidade de montar um dispositivo virtual a partir de vários módulos;
- a capacidade de carregar qualquer dado binário (firmware) na memória do dispositivo virtual;
- a capacidade de trabalhar com snapshots (cópias de estado do sistema);
- a capacidade de interagir com o emulador através de um depurador integrado;
- uma linguagem moderna e agradável para desenvolvimento.
No final, Kotlin foi escolhido para implementação, uma arquitetura de barramento (que é quando módulos se conectam entre si através de barramentos de dados virtuais), JSON — como formato de descrição do dispositivo, e GDB RSP — como protocolo de interação com o depurador.
O desenvolvimento está em andamento há pouco mais de dois anos e continua ativamente. Durante esse tempo, foram implementados núcleos de processadores MIPS, x86, V850ES, ARM e PowerPC.
O projeto está crescendo, e chegou a hora de apresentá-lo ao grande público. Faremos uma descrição detalhada do projeto mais tarde, mas agora vamos nos concentrar no uso do Kopycat.
Para os mais impacientes — a versão promocional do emulador pode ser baixada em .
Rinoceronte no emulador
Lembramos que, anteriormente, foi criado um dispositivo de teste "Rinoceronte" para a conferência SMARTRHINO-2018, com o intuito de ensinar habilidades em engenharia reversa. O processo de análise estática do firmware foi descrito em .
Agora, vamos tentar adicionar "dinâmicas" e executar o firmware no emulador.
Precisaremos de:
1) Java 1.8
2) Python e o módulo para utilizar o Python dentro do emulador. A compilação WHL do módulo Jep para Windows pode ser .
Para Windows:
1)
2)
Para Linux:
1) socat
Como cliente GDB, pode-se usar Eclipse, IDA Pro ou radare2.
Como funciona?
Para executar o firmware no emulador, é necessário "montar" um dispositivo virtual que represente um dispositivo real.
O dispositivo real ("rinoceronte") pode ser representado no diagrama estrutural:

O emulador possui uma estrutura modular e o dispositivo virtual final pode ser descrito em um arquivo JSON.
JSON com 105 linhas
{
"top": true,
// O nome do plugin deve ser o mesmo que o nome do arquivo (ou o caminho completo a partir do início da biblioteca)
"plugin": "rhino",
// Diretório onde o plugin é colocado
"library": "user",
// Parâmetros do plugin (parâmetros do construtor se for versão jar-plugin)
"params": [
{ "name": "tty_dbg", "type": "String"},
{ "name": "tty_bt", "type": "String"},
{ "name": "firmware", "type": "String", "default": "NUL"}
],
// Portas externas do plugin
"ports": [ ],
// Barras internas do plugin
"buses": [
{ "name": "mem", "size": "BUS30" },
{ "name": "nand", "size": "4" },
{ "name": "gpio", "size": "BUS32" }
],
// Componentes internos do plugin
"modules": [
{
"name": "u1_stm32",
"plugin": "STM32F042",
"library": "mcu",
"params": {
"firmware:String": "params.firmware"
}
},
{
"name": "usart_debug",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_dbg"
}
},
{
"name": "term_bt",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_bt"
}
},
{
"name": "bluetooth",
"plugin": "BT",
"library": "mcu"
},
{ "name": "led_0", "plugin": "LED", "library": "mcu" },
{ "name": "led_1", "plugin": "LED", "library": "mcu" },
{ "name": "led_2", "plugin": "LED", "library": "mcu" },
{ "name": "led_3", "plugin": "LED", "library": "mcu" },
{ "name": "led_4", "plugin": "LED", "library": "mcu" },
{ "name": "led_5", "plugin": "LED", "library": "mcu" },
{ "name": "led_6", "plugin": "LED", "library": "mcu" },
{ "name": "led_7", "plugin": "LED", "library": "mcu" },
{ "name": "led_8", "plugin": "LED", "library": "mcu" },
{ "name": "led_9", "plugin": "LED", "library": "mcu" },
{ "name": "led_10", "plugin": "LED", "library": "mcu" },
{ "name": "led_11", "plugin": "LED", "library": "mcu" },
{ "name": "led_12", "plugin": "LED", "library": "mcu" },
{ "name": "led_13", "plugin": "LED", "library": "mcu" },
{ "name": "led_14", "plugin": "LED", "library": "mcu" },
{ "name": "led_15", "plugin": "LED", "library": "mcu" }
],
// Conexão do plugin entre componentes
"connections": [
[ "u1_stm32.ports.usart1_m", "usart_debug.ports.term_s"],
[ "u1_stm32.ports.usart1_s", "usart_debug.ports.term_m"],
[ "u1_stm32.ports.usart2_m", "bluetooth.ports.usart_m"],
[ "u1_stm32.ports.usart2_s", "bluetooth.ports.usart_s"],
[ "bluetooth.ports.bt_s", "term_bt.ports.term_m"],
[ "bluetooth.ports.bt_m", "term_bt.ports.term_s"],
[ "led_0.ports.pin", "u1_stm32.buses.pin_output_a", "0x00"],
[ "led_1.ports.pin", "u1_stm32.buses.pin_output_a", "0x01"],
[ "led_2.ports.pin", "u1_stm32.buses.pin_output_a", "0x02"],
[ "led_3.ports.pin", "u1_stm32.buses.pin_output_a", "0x03"],
[ "led_4.ports.pin", "u1_stm32.buses.pin_output_a", "0x04"],
[ "led_5.ports.pin", "u1_stm32.buses.pin_output_a", "0x05"],
[ "led_6.ports.pin", "u1_stm32.buses.pin_output_a", "0x06"],
[ "led_7.ports.pin", "u1_stm32.buses.pin_output_a", "0x07"],
[ "led_8.ports.pin", "u1_stm32.buses.pin_output_a", "0x08"],
[ "led_9.ports.pin", "u1_stm32.buses.pin_output_a", "0x09"],
[ "led_10.ports.pin", "u1_stm32.buses.pin_output_a", "0x0A"],
[ "led_11.ports.pin", "u1_stm32.buses.pin_output_a", "0x0B"],
[ "led_12.ports.pin", "u1_stm32.buses.pin_output_a", "0x0C"],
[ "led_13.ports.pin", "u1_stm32.buses.pin_output_a", "0x0D"],
[ "led_14.ports.pin", "u1_stm32.buses.pin_output_a", "0x0E"],
[ "led_15.ports.pin", "u1_stm32.buses.pin_output_a", "0x0F"]
]
}Preste atenção no parâmetro firmware na seção params — é o nome do arquivo que pode ser carregado no dispositivo virtual como firmware.
O dispositivo virtual e sua interação com o sistema operacional principal podem ser representados assim:

A instância atual do emulador permite interação com as portas COM do sistema operacional principal (UART de depuração e UART para o módulo Bluetooth). Estes podem ser portas reais, às quais dispositivos estão conectados, ou portas COM virtuais (para isso é necessário com0com / socat).
Atualmente, existem duas formas principais de interação com o emulador:
- o protocolo GDB RSP (os ferramentas que suportam esse protocolo são — Eclipse / IDA / radare2);
- a linha de comando interna do emulador (Argparse ou Python).
Portas COM virtuais
Para interagir com o UART do dispositivo virtual em uma máquina local através de um terminal, é necessário criar um par de portas COM virtuais conectadas. No nosso caso, uma porta é utilizada pelo emulador e a outra — pelo programa terminal (PuTTY ou screen):

Uso do com0com
As portas COM virtuais são configuradas pela utilidade setup do pacote com0com (versão de console — C:Program Files (x86)com0comsetupс.exe, ou versão GUI — C:Program Files (x86)com0comsetupg.exe):

É necessário marcar as opções enable buffer overrun para todas as portas virtuais criadas, caso contrário, o emulador ficará aguardando uma resposta da porta COM.
Uso do socat
Em sistemas UNIX, as portas COM virtuais são criadas automaticamente pelo emulador usando a utilidade socat, basta especificar o prefixo socat:.
Interface interna de linha de comando (Argparse ou Python)
Como Kopycat é um aplicativo de console, para interagir com seus objetos e variáveis, o emulador fornece duas opções de interface de linha de comando: Argparse e Python.
Argparse é o CLI embutido no Kopycat, ele está sempre disponível para todos.
O CLI alternativo é o interpretador Python. Para usá-lo, é necessário instalar o módulo Python Jep e configurar o emulador para trabalhar com Python (o interpretador Python instalado no sistema principal do usuário será utilizado).
Instalação do módulo Python Jep
No Linux, Jep pode ser instalado através do pip:
pip install jepPara instalar o Jep no Windows, é necessário primeiro instalar o Windows SDK e a correspondente Microsoft Visual Studio. Facilitamos sua tarefa e fizemos JEP para as versões atuais do Python para Windows, portanto, o módulo pode ser instalado a partir do arquivo:
pip install jep-3.8.2-cp27-cp27m-win_amd64.whlPara verificar a instalação do Jep, é necessário executar no prompt de comando:
python -c "import jep"A resposta deve ser a seguinte mensagem:
ImportError: Jep não é suportado no Python autônomo, ele deve ser incorporado ao Java.No arquivo de comando do emulador para seu sistema (kopycat.bat — para Windows, kopycat — para Linux) adicione um parâmetro à lista de opções DEFAULT_JVM_OPTS adicione o parâmetro adicional Djava.library.path — ele deve conter o caminho para o módulo Jep instalado.
Como resultado, para Windows, a linha deve ser da seguinte forma:
set DEFAULT_JVM_OPTS="-XX:MaxMetaspaceSize=256m" "-XX:+UseParallelGC" "-XX:SurvivorRatio=6" "-XX:-UseGCOverheadLimit" "-Djava.library.path=C:/Python27/Lib/site-packages/jep"Executando Kopycat
O emulador é um aplicativo JVM de console. A execução é feita por meio de um script de linha de comando do sistema operacional (sh/cmd).
O comando para execução no Windows:
binkopycat -g 23946 -n rhino -l user -y library -p firmware=firmwarerhino_pass.bin,tty_dbg=COM26,tty_bt=COM28O comando para execução no Linux usando a ferramenta socat:
./bin/kopycat -g 23946 -n rhino -l user -y library -p firmware=./firmware/rhino_pass.bin, tty_dbg=socat:./COM26,tty_bt=socat:./COM28-g 23646— a porta TCP que será aberta para acesso ao servidor GDB;-n rhino— o nome do módulo principal do sistema (dispositivo montado);-l user— o nome da biblioteca para buscar o módulo principal;-y library— o caminho para buscar os módulos que fazem parte do dispositivo;firmwarerhino_pass.bin— o caminho para o arquivo de firmware;- COM26 e COM28 — portas COM virtuais.
Como resultado, será exibido o prompt Python > (ou Argparse >):
18:07:59 INFO [eFactoryBuilder.create]: Módulo top criado com sucesso como top
18:07:59 INFO [Module.initializeAndRes]: Configurando o núcleo para top.u1_stm32.cortexm0.arm para top
18:07:59 INFO [Module.initializeAndRes]: Configurando o depurador para top.u1_stm32.dbg para top
18:07:59 WARN [Module.initializeAndRes]: Rastreador não encontrado em top...
18:07:59 INFO [Module.initializeAndRes]: Inicializando portas e barramentos...
18:07:59 WARN [Module.initializePortsA]: ATENÇÃO: Algumas portas têm avisos, use printModulesPortsWarnings para vê-los...
18:07:59 FINE [ARMv6CPU.reset]: Definindo endereço de ponto de entrada para 08006A75
18:07:59 INFO [Module.initializeAndRes]: Módulo top foi inicializado e reiniciado com sucesso como uma célula top!
18:07:59 INFO [Kopycat.open]: Iniciando virtualização do board top[rhino] com arm[ARMv6Core]
18:07:59 INFO [GDBServer.debuggerModule]: Definindo novo módulo de depuração top.u1_stm32.dbg para GDB_SERVER(port=23946,alive=true)
Python >Interagindo com o IDA Pro
Como arquivo de origem para análise no IDA, para facilitar os testes, usamos o firmware "Rinoceronte" na forma de (lá está armazenada a metainformação).
Você também pode usar o firmware principal sem metainformação.
Após iniciar o Kopycat no IDA Pro, no menu Depurador, vá para "Alternar depurador…" e escolha "Depurador GDB remoto«. Em seguida, configuramos a conexão: menu Depurador — Opções do processo…
Definindo os valores:
- Aplicativo — qualquer valor
- Nome do host: 127.0.0.1 (ou endereço IP da máquina remota onde o Kopycat está em execução)
- Porta: 23946

Agora o botão de iniciar depuração (tecla F9) está disponível:
![]()
Clicamos nele — conectamos ao módulo do depurador no emulador. IDA entra no modo de depuração, novas janelas se tornam disponíveis: informações sobre registradores, sobre a pilha.
Agora podemos usar todas as funções padrão do depurador:
- execução passo a passo das instruções (Entrar e Passar por cima — teclas F7 e F8, respectivamente);
- iniciar e pausar a execução;
- criação de pontos de interrupção em código e dados (tecla F2).
Conectar ao depurador não significa iniciar o código de firmware. A posição atual para a execução deve ser o endereço 0x08006A74 — início da função Reset_Handler. Se você rolar o listagem abaixo, poderá ver a chamada da função main. Você pode posicionar o cursor nessa linha (endereço 0x08006ABE) e executar a operação Executar até o cursor (tecla F4).

Em seguida, você pode pressionar F7 para entrar na função main.
Se você executar o comando Continuar processo (tecla F9), uma janela 'Por favor, aguarde' aparecerá com um único botão Suspender:

Ao clicar Suspender a execução do código de firmware é suspensa e pode ser continuada a partir do mesmo endereço no código onde foi interrompida.
Se você continuar a execução do código, poderá ver as seguintes linhas nos terminais conectados às portas COM virtuais:


A presença da linha 'state bypass' indica que o módulo Bluetooth virtual mudou para o modo de recebimento de dados da porta COM do usuário.
Agora no terminal Bluetooth (na figura — COM29) você pode inserir comandos de acordo com o protocolo 'Rhinoceros'. Por exemplo, para o comando 'MEOW', o terminal Bluetooth retornará a linha 'mur-mur':

Não me emule completamente
Ao construir o emulador, pode-se escolher o nível de detalhe/emulação de cada dispositivo. Por exemplo, o módulo Bluetooth pode ser emulado de diferentes maneiras:
- emula completamente o dispositivo com um conjunto completo de comandos;
- emula os comandos AT, enquanto os dados são recebidos da porta COM do sistema principal;
- o dispositivo virtual fornece redirecionamento completo de dados para o dispositivo real;
- como um simples stub, que sempre retorna 'OK'.
Na versão atual do emulador, é utilizado um segundo método — um módulo Bluetooth virtual realiza a configuração e, em seguida, entra no modo de "proxy" para transferir dados da porta COM do sistema principal para a porta UART do emulador.

Vamos considerar a possibilidade de instrumentação simples do código caso alguma parte periférica não esteja implementada. Por exemplo, se um timer, responsável pelo controle da transmissão de dados no DMA, não tiver sido criado (a verificação é realizada na função ws2812b_wait, localizada no endereço 0x08006840), o firmware vai esperar constantemente o reinício do sinal busy, localizado no endereço 0x200004C4, que indica a ocupação da linha de dados do DMA:

Podemos contornar essa situação fazendo a reinicialização "manual" do sinal busy imediatamente após sua ativação. No IDA Pro, é possível criar uma função Python e chamá-la no breakpoint, colocando o próprio breakpoint no código após escrever o valor 1 no sinal busy.
Manipulador de breakpoint
Primeiro, vamos criar uma função Python no IDA. No menu Arquivo — Comando de script...
Adicione um novo snippet na lista à esquerda, dê-lhe um nome (por exemplo, BPT),
e no campo de texto à direita, insira o código da função:
def skip_dma():
print "Aguardando espera ws2812..."
value = Byte(0x200004C4)
if value == 1:
PatchDbgByte(0x200004C4, 0)
return False 
Depois disso, pressione Run e feche a janela de scripts.
Agora, vamos para o código no endereço 0x0800688A, estabelecemos um breakpoint (tecla F2), editamos-o (menu de contexto Editar breakpoint...), não esqueça de definir o tipo de script – Python:


Se o valor atual do sinal busy for igual a 1, devemos executar a função skip_dma na linha dos scripts:

Se você executar o firmware, pode ver a ativação do código do manipulador de breakpoint no IDA na janela Saída com a linha Aguardando espera ws2812.... Agora o firmware não irá esperar pelo reinício do sinal. busy.
Interação com o emulador
Emular por emular dificilmente trará entusiasmo e alegria. É bem mais interessante se o emulador ajudar o pesquisador a ver dados na memória ou estabelecer interação entre threads.
Vamos mostrar como estabelecer interações entre tarefas do RTOS em dinâmica. Você deve pausar a execução do código, se ele estiver em execução. Se você acessar a função bluetooth_task_entry na ramificação de tratamento do comando "LED" (endereço 0x080057B8), você pode ver que inicialmente é criado, e então enviado à fila do sistema ledControlQueueHandle uma certa mensagem.

Deve-se estabelecer um breakpoint no acesso à variável ledControlQueueHandle, localizada no endereço 0x20000624 e continuar a execução do código:

Como resultado, primeiro haverá uma parada no endereço 0x080057CA antes da chamada da função osMailAlloc, depois — no endereço 0x08005806 antes da chamada da função osMailPut, depois, após algum tempo — no endereço 0x08005BD4 (antes da chamada da função osMailGet), que pertence à função leds_task_entry (tarefa LED), ou seja, ocorreu uma troca de tarefas, e agora o controle foi para a tarefa LED.

Dessa maneira simples, é possível determinar como as tarefas RTOS interagem umas com as outras.
Claro, na realidade a interação das tarefas pode ser mais complexa, mas com o uso do emulador, rastrear essa interação se torna menos trabalhoso.
você pode assistir a um pequeno vídeo de inicialização do emulador e interação com o IDA Pro.
Inicialização com Radare2
Não se pode ignorar uma ferramenta tão versátil como o Radare2.
Para conectar ao emulador usando r2, o comando será assim:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elfAgora estão disponíveis a execução (dc) e a pausa da execução (Ctrl+C).
Infelizmente, no momento, existem problemas no r2 ao trabalhar com o servidor gdb de hardware e o mapeamento de memória, devido a isso, os pontos de interrupção e os passos (comando ds) não funcionam. Esperamos que isso seja corrigido em breve.
Inicialização com Eclipse
Uma das opções para usar o emulador é a depuração do firmware do dispositivo em desenvolvimento. Para maior clareza, usaremos também o firmware "Rhinoceros". Você pode baixar o código-fonte do firmware .
Como IDE, usaremos o Eclipse do conjunto .
Para que o firmware compilado no Eclipse seja carregado diretamente no emulador, é necessário adicionar o parâmetro firmware=null no comando de inicialização do emulador:
binkopycat -g 23946 -n rhino -l user -y modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28Configuração de debug
No Eclipse, selecione o menu Executar — Configurações de Depuração… Na janela aberta, na seção Depuração de Hardware GDB é necessário adicionar uma nova configuração, após o que, na aba "Principal", indicar o projeto atual e o aplicativo para depuração:

Na aba "Debugger", é necessário indicar o comando GDB:
${openstm32_compiler_path}arm-none-eabi-gdb
E também inserir os parâmetros para conectar ao servidor GDB (host e porta):

Na aba "Startup", é necessário indicar os seguintes parâmetros:
- marcar a caixa Carregar imagem (para que o carregamento da imagem compilada do firmware no emulador seja realizado);
- marcar a caixa Carregar símbolos;
- adicionar o comando de inicialização:
set $pc = *0x08000004(definir no registro PC o valor da memória no endereço0x08000004— lá está armazenado o endereço ResetHandler’а).
Note que, se você não quiser carregar o arquivo de firmware do Eclipse, os parâmetros Carregar imagem e Execute comandos não precisam ser especificados.

Após clicar em Debug, você pode trabalhar no modo de depuração:
- execução passo a passo do código

- interação com pontos de parada

Nota. No Eclipse, há, hum… algumas peculiaridades… e você tem que conviver com elas. Por exemplo, se ao iniciar o depurador aparecer a mensagem «No source available for «0x0″», execute o comando Step (F5)

Em conclusão
A emulação de código nativo é um assunto bastante interessante. Para o desenvolvedor de dispositivos, surge a oportunidade de depurar o firmware sem um dispositivo real. Para o pesquisador — a possibilidade de realizar uma análise dinâmica do código, o que nem sempre é possível mesmo com a presença do dispositivo.
Queremos fornecer aos especialistas uma ferramenta que seja conveniente, relativamente simples e que não consuma muito esforço e tempo em sua configuração e execução.
Escreva nos comentários sobre sua experiência com emuladores de hardware. Convidamos você a discutir e estaremos felizes em responder a perguntas.
Apenas usuários registrados podem participar da pesquisa. , por favor.
Para que você usa o emulador?
desenvolvo (depuro) firmwares
pesquiso firmwares
executo jogos (Dendi, Sega, PSP)
algo diferente (escreva nos comentários)
7 usuários votaram. 2 usuários se abstiveram.
Que software você usa para emular código nativo?
QEMU
Unicorn engine
Proteus
algo diferente (escreva nos comentários)
6 usuários votaram. 2 usuários se abstiveram.
O que você gostaria de melhorar no emulador que está usando?
quero velocidade
quero facilidade de configuração/execução
quero mais possibilidades de interação com o emulador (API, hooks)
estou satisfeito com tudo
algo diferente (escreva nos comentários)
8 usuários votaram. 1 usuário se absteve.
Fonte: habr.com


