Durante a reunião 0x0A DC7831 DEF CON Nizhny Novgorod, em 16 de fevereiro, apresentamos um relatório 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 Kopycat .
No artigo, descreveremos o lançamento do firmware do dispositivo no emulador, demonstraremos a interação com o depurador e faremos uma pequena análise dinâmica do firmware.
Antecedentes
Há muito tempo, em uma galáxia muito distante
Há alguns anos, em nosso laboratório, havia a necessidade de estudar o firmware do dispositivo. O firmware foi compactado, descompactado pelo gerenciador de inicialização. Ele fez isso de uma maneira muito confusa, várias vezes deslocando dados na memória. Sim, e o próprio firmware interagiu ativamente com os periféricos. E tudo isso no núcleo do MIPS.
Por razões objetivas, os emuladores existentes não eram adequados para nós, mas eu ainda queria executar o código. Decidimos então criar nosso próprio emulador, o que reduzirá ao mínimo e permitirá descompactar o firmware principal. Tentamos - acabou. Nós pensamos, e se adicionarmos periféricos para também executar o firmware principal. Não foi muito doloroso - e funcionou também. Pensamos novamente e decidimos criar um emulador completo.
O resultado foi um emulador de sistemas de computação Kopycat .

Porquê o Kopycat?Há um jogo de palavras.
- copycat (inglês, n. [ˈkɒpɪkæt]) - copycat, imitador
- cat (inglês, n. [ˈkæt]) - um gato, um gato - um animal favorito de um dos criadores do projeto
- Letra "K" - da linguagem de programação Kotlin
Kopycat
Ao criar o emulador, objetivos absolutamente específicos foram definidos:
- a capacidade de criar rapidamente uma nova periferia, módulo, núcleo do processador;
- a capacidade de montar um dispositivo virtual a partir de vários módulos;
- a capacidade de carregar quaisquer dados binários (firmware) na memória do dispositivo virtual;
- a capacidade de trabalhar com instantâneos (instantâneos do estado do sistema);
- a capacidade de interagir com o emulador através do depurador embutido;
- bela linguagem moderna para se desenvolver.
Como resultado, o Kotlin foi escolhido para implementação, arquitetura de barramento (isto é, quando os módulos se comunicam através de barramentos de dados virtuais), JSON como formato de descrição do dispositivo e GDB RSP como protocolo para interagir com o depurador.
O desenvolvimento está em andamento há pouco mais de dois anos e está em andamento. Durante esse período, os núcleos dos processadores MIPS, x86, V850ES, ARM, PowerPC foram implementados.
O projeto está crescendo e é hora de apresentá-lo ao público em geral. Faremos uma descrição detalhada do projeto posteriormente, mas agora vamos nos concentrar no uso do Kopycat.
Para os mais impacientes - a versão promocional do emulador pode ser baixada aqui .
Rhino no emulador
Lembre-se que anteriormente para a conferência SMARTRHINO-2018, um dispositivo de teste "Rhinoceros" foi criado para treinamento em habilidades de engenharia reversa. O processo de análise de firmware estático foi descrito neste artigo .
Agora vamos tentar adicionar "alto-falantes" e executar o firmware no emulador.
Vamos precisar de:
1) Java 1.8
2) Python e o módulo Jep para usar o Python dentro do emulador. O assembly WHL do módulo Jep para Windows pode ser baixado aqui .
Para Windows:
1) com0com
2) PuTTY
Para Linux:
1) socat
Você pode usar Eclipse, IDA Pro ou radare2 como um cliente GDB.
Como isso funciona?
Para executar o firmware no emulador, você deve "montar" um dispositivo virtual, que é análogo a um dispositivo real.
O dispositivo real ("rhino") pode ser mostrado em um diagrama de blocos:
O emulador tem uma estrutura modular e o dispositivo virtual final pode ser descrito em um arquivo JSON.
JSON em 105 linhas{ "top": true, // Plugin name should be the same as file name (or full path from library start) "plugin": "rhino", // Directory where plugin places "library": "user", // Plugin parameters (constructor parameters if jar-plugin version) "params": [ { "name": "tty_dbg", "type": "String"}, { "name": "tty_bt", "type": "String"}, { "name": "firmware", "type": "String", "default": "NUL"} ], // Plugin outer ports "ports": [ ], // Plugin internal buses "buses": [ { "name": "mem", "size": "BUS30" }, { "name": "nand", "size": "4" }, { "name": "gpio", "size": "BUS32" } ], // Plugin internal components "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" } ], // Plugin connection between components "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 - este é o nome do arquivo que pode ser baixado no dispositivo virtual como firmware.
Um dispositivo virtual e sua interação com o sistema operacional principal podem ser representados da seguinte maneira:
A instância de teste atual do emulador envolve interação com as portas COM do sistema operacional principal (depurar UART e UART para o módulo Bluetooth). Podem ser portas reais às quais os dispositivos estão conectados ou portas COM virtuais ( com0com / socat é apenas para isso ) .
Atualmente, existem duas maneiras principais de interagir com o emulador de fora:
- Protocolo GDB RSP (respectivamente, suportando este protocolo, ferramentas - Eclipse / IDA / radare2);
- linha de comando interna do emulador (Argparse ou Python).
Para interagir com o UART do dispositivo virtual na máquina local através do terminal, você precisa criar algumas portas COM virtuais conectadas. No nosso caso, uma porta usa um emulador e a segunda um programa de terminal (PuTTY ou tela):
Usando com0com
As portas COM virtuais são configuradas com o utilitário de instalação do kit com0com (a versão do console é C: \ Arquivos de Programas (x86) \ com0com \ setup.exe ou a versão da GUI é C: \ Arquivos de Programas (x86) \ com0com \ setupg.exe ) :
Marque as caixas de seleção Ativar saturação do buffer para todas as portas virtuais criadas, caso contrário, o emulador aguardará uma resposta da porta COM.
Usando socat
Nos sistemas UNIX, as portas COM virtuais são criadas automaticamente pelo emulador usando o utilitário socat, para isso basta especificar o prefixo socat:
no nome da porta ao iniciar o emulador.
Interface de linha de comando interna (Argparse ou Python)
Como o Kopycat é um aplicativo de console, o emulador fornece duas opções para a interface da linha de comandos interagir com seus objetos e variáveis: Argparse e Python.
Argparse é a CLI embutida no Kopycat, está sempre disponível para todos.
Uma CLI alternativa é o interpretador Python. Para usá-lo, você precisa instalar o módulo Jep Python e configurar o emulador para trabalhar com Python (o interpretador Python instalado no sistema principal do usuário será usado).
Instale o módulo Python Jep
No Linux, o Jep pode ser instalado via pip:
pip install jep
Para instalar o Jep no Windows, você deve primeiro instalar o Windows SDK e o Microsoft Visual Studio correspondente. Simplificamos um pouco sua tarefa e fizemos assemblies WHL JEP para as versões atuais do Python para Windows, para que o módulo possa ser instalado a partir de um arquivo:
pip install jep-3.8.2-cp27-cp27m-win_amd64.whl
Para verificar a instalação do Jep, você deve executar a linha de comando:
python -c "import jep"
Em resposta, uma mensagem deve ser recebida:
ImportError: Jep is not supported in standalone Python, it must be embedded in Java.
No arquivo em lotes do emulador para o seu sistema ( kopycat.bat para Windows, kopycat para Linux), inclua o parâmetro adicional Djava.library.path
na lista de parâmetros DEFAULT_JVM_OPTS
- ele deve conter o caminho para o módulo Jep instalado.
Como resultado, no Windows, você deve obter uma linha como esta:
set DEFAULT_JVM_OPTS="-XX:MaxMetaspaceSize=256m" "-XX:+UseParallelGC" "-XX:SurvivorRatio=6" "-XX:-UseGCOverheadLimit" "-Djava.library.path=C:/Python27/Lib/site-packages/jep"
Lançamento do Kopycat
O emulador é um aplicativo JVM do console. O lançamento é realizado através do script de linha de comando do sistema operacional (sh / cmd).
Comando a ser executado no Windows:
bin\kopycat -g 23946 -n rhino -l user -y library -p firmware=firmware\rhino_pass.bin,tty_dbg=COM26,tty_bt=COM28
O comando para executar no Linux usando o utilitário 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
- porta TCP que será aberta para acesso ao servidor GDB;-n rhino
- o nome do módulo principal do sistema (montagem do dispositivo);-l user
- o nome da biblioteca para procurar o módulo principal;-y library
- o caminho para procurar módulos incluídos no dispositivo;firmware\rhino_pass.bin
- caminho para o arquivo de firmware;- COM26 e COM28 são portas COM virtuais.
O resultado será o Argparse >
do Python >
(ou Argparse >
):
18:07:59 INFO [eFactoryBuilder.create ]: Module top successfully created as top 18:07:59 INFO [ Module.initializeAndRes]: Setup core to top.u1_stm32.cortexm0.arm for top 18:07:59 INFO [ Module.initializeAndRes]: Setup debugger to top.u1_stm32.dbg for top 18:07:59 WARN [ Module.initializeAndRes]: Tracer wasn't found in top... 18:07:59 INFO [ Module.initializeAndRes]: Initializing ports and buses... 18:07:59 WARN [ Module.initializePortsA]: ATTENTION: Some ports has warning use printModulesPortsWarnings to see it... 18:07:59 FINE [ ARMv6CPU.reset ]: Set entry point address to 08006A75 18:07:59 INFO [ Module.initializeAndRes]: Module top is successfully initialized and reset as a top cell! 18:07:59 INFO [ Kopycat.open ]: Starting virtualization of board top[rhino] with arm[ARMv6Core] 18:07:59 INFO [ GDBServer.debuggerModule ]: Set new debugger module top.u1_stm32.dbg for GDB_SERVER(port=23946,alive=true) Python >
Interação com o IDA Pro
Para simplificar o teste, usamos o firmware Rhino como um arquivo ELF (as meta-informações são salvas lá) como o arquivo de origem para análise na IDA.
Você também pode usar o firmware principal sem meta-informação.
Após iniciar o Kopycat no IDA Pro, no menu Debugger, vá para o item " Switch debugger ... " e selecione " Remote GDB debugger ". Em seguida, configure a conexão: Menu Debugger - Process options ...
Defina os valores:
- Aplicação - qualquer valor
- Nome do host: 127.0.0.1 (ou o endereço IP da máquina remota em que o Kopycat está sendo executado)
- Porto: 23946
Agora, o botão Iniciar depuração está disponível (tecla F9):
Pressione - ele se conecta ao módulo depurador no emulador. A IDA entra no modo de depuração, janelas adicionais ficam disponíveis: informações sobre registros, sobre a pilha.
Agora podemos usar todos os recursos padrão do trabalho com o depurador:
- execução passo a passo das instruções ( passo a passo e passo a passo - teclas F7 e F8, respectivamente);
- iniciar e pausar a execução;
- criando pontos de interrupção no código e nos dados (tecla F2).
Conectar-se ao depurador não significa iniciar o código do firmware. A posição atual para execução deve ser o endereço 0x08006A74
- o início da função Reset_Handler . Se você rolar a lista abaixo, poderá ver a chamada para a função principal . Você pode colocar o cursor nesta 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 principal .
Se você executar o comando Continuar processo (tecla F9), a janela "Aguarde" será exibida com um único botão Suspender :
Quando Suspender é pressionado, a execução do código do firmware é suspensa e pode ser continuada a partir do mesmo endereço no código em que foi interrompido.
Se você continuar executando o código, nos terminais conectados às portas COM virtuais, poderá ver as seguintes linhas:


A presença da sequência "desvio de estado" 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 Rhino. Por exemplo, a sequência "mur-mur" retorna ao comando "MEOW" no terminal Bluetooth:

Emule-me não completamente
Ao criar um emulador, você pode escolher o grau de detalhe / emulação de um dispositivo. Assim, por exemplo, o módulo Bluetooth pode ser emulado de diferentes maneiras:
- dispositivo totalmente emulado com um conjunto completo de comandos;
- Os comandos AT são emulados e o fluxo de dados é recebido da porta COM do sistema principal;
- dispositivo virtual fornece redirecionamento completo de dados para um dispositivo real;
- como um esboço simples que sempre retorna "OK".
Na versão atual do emulador, a segunda abordagem é usada - o módulo Bluetooth virtual executa a configuração, após o que alterna para o modo "proxy" de dados da porta COM do sistema principal para a porta UART do emulador.
Considere a possibilidade de instrumentação simples do código se alguma parte da periferia não estiver implementada. Por exemplo, se não for criado um timer que controle a transferência de dados no DMA (a verificação é realizada na função ws2812b_wait localizada em 0x08006840
), o firmware sempre aguardará o sinalizador de ocupado localizado em 0x200004C4
para redefinir a linha de dados do DMA:

Podemos contornar essa situação redefinindo manualmente o sinalizador de ocupado imediatamente após defini-lo. No IDA Pro, você pode criar uma função Python e chamá-la no ponto de interrupção, e o ponto de interrupção deve ser definido no código após gravar o valor 1 no sinalizador de ocupado .
Manipulador de ponto de interrupção
Primeiro, crie uma função Python no IDA. Menu Arquivo - comando Script ...
Adicione um novo trecho na lista à esquerda, dê um nome (por exemplo, BPT ),
na caixa de texto à direita, inserimos o código da função:
def skip_dma(): print "Skipping wait ws2812..." value = Byte(0x200004C4) if value == 1: PatchDbgByte(0x200004C4, 0) return False
Depois disso, clique em Executar e feche a janela de script.
Agora vamos ao código em 0x0800688A
, defina o ponto de interrupção (tecla F2), edite-o ( Editar ponto de interrupção ... menu de contexto), não se esqueça de definir o tipo de script - Python:
Se o valor atual do sinalizador de ocupado for 1, a função skip_dma deve ser executada na linha de script:

Se você executar o firmware para execução, o código do manipulador de ponto de interrupção poderá ser visto no IDA na janela Saída na linha Skipping wait ws2812...
Agora o firmware não esperará para redefinir o sinalizador de ocupado .
Interação do emulador
É improvável que a emulação por causa da emulação cause prazer e alegria. É muito mais interessante se o emulador ajudar o pesquisador a ver os dados na memória ou a estabelecer a interação dos fluxos.
Mostramos como estabelecer dinamicamente a interação das tarefas RTOS. Primeiro, pause a execução do código, se estiver em execução. Se você alternar para a função bluetooth_task_entry na ramificação de processamento de comando "LED" (endereço 0x080057B8
), poderá ver o que foi criado pela primeira vez e, em seguida, uma mensagem será enviada para a fila do sistema ledControlQueueHandle .

Você deve definir o ponto de interrupção para acessar a variável ledControlQueueHandle localizada em 0x20000624
e continuar executando o código:
Como resultado, primeiro ele para no endereço 0x080057CA
antes de chamar a função osMailAlloc , depois em 0x08005806
antes de chamar a função osMailPut e depois de um tempo no endereço 0x08005BD4
(antes de chamar a função osMailGet ), que pertence à função leds_task_entry (tarefa LED), ou seja. houve uma troca de tarefas e agora o controle recebeu a tarefa de LED.

De uma maneira tão simples, você pode estabelecer como as tarefas do RTOS interagem entre si.
Obviamente, na realidade, a interação de tarefas pode ser mais complicada, mas usar um emulador para rastrear essa interação se torna menos difícil.
Aqui você pode assistir a um pequeno vídeo de lançamento e interação do emulador com o IDA Pro.
Iniciar com Radare2
Você não pode ignorar uma ferramenta universal como o Radare2.
Para conectar-se ao emulador usando r2, o comando será semelhante a este:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elf
Agora iniciar ( dc
) e pausar a execução (Ctrl + C) estão disponíveis.
Infelizmente, no momento em r2, há problemas ao trabalhar com um servidor gdb de hardware e marcação de memória, por isso, pontos de interrupção e etapas (o comando ds
) não funcionam. Esperamos que isso seja corrigido em um futuro próximo.
Iniciar com o Eclipse
Uma das opções para usar o emulador é depurar o firmware do dispositivo em desenvolvimento. Para maior clareza, também usaremos o firmware Rhino. Você pode baixar as fontes de firmware aqui .
Usaremos o Eclipse do conjunto System Workbench for STM32 como o IDE.
Para que o firmware compilado diretamente no Eclipse seja carregado no emulador, você precisa adicionar o parâmetro firmware=null
ao comando start do emulador:
bin\kopycat -g 23946 -n rhino -l user -y library -p firmware=null,tty_dbg=COM26,tty_bt=COM28
Configuração de depuração
No Eclipse, selecione o menu Executar - Configurações de depuração .... Na janela que é aberta, na seção Depuração de hardware do GDB , você precisa adicionar uma nova configuração e especificar o projeto e aplicativo atuais para depuração na guia "Principal":

Na guia Depurador, você deve especificar o comando GDB:
${openstm32_compiler_path}\arm-none-eabi-gdb
E também insira os parâmetros para conectar ao servidor GDB (host e porta):

Os seguintes parâmetros devem ser especificados na guia "Inicialização":
- ative a caixa de seleção Carregar imagem (para que a imagem do firmware montada seja carregada no emulador);
- ative a marca de seleção Carregar símbolos ;
- adicione um comando de início:
set $pc = *0x08000004
(defina o valor da memória para o endereço 0x08000004
no registro do PC - o endereço do 0x08000004
é armazenado lá).
Observe que, se você não deseja fazer o download do arquivo de firmware do Eclipse, não precisa especificar os parâmetros Carregar imagem e Executar comandos .

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

- interação com pontos de interrupção

Nota O Eclipse tem, hmm ... alguns recursos ... e você tem que conviver com eles. Por exemplo, se a mensagem "Nenhuma fonte disponível para" 0x0 "" aparecer ao iniciar o depurador, execute o comando Etapa (F5)
Em vez de uma conclusão
A emulação de código nativo é uma coisa muito interessante. Para um desenvolvedor de dispositivos, torna-se possível depurar o firmware sem um dispositivo real. Para o pesquisador - a capacidade de realizar análise dinâmica de código, o que nem sempre é possível, mesmo com um dispositivo.
Queremos fornecer aos especialistas uma ferramenta que fosse conveniente, moderadamente simples e não demorasse muito tempo e esforço para configurar e iniciar.
Escreva nos comentários sobre sua experiência com o uso de emuladores de hardware. Convidamos você para uma discussão e teremos prazer em responder a perguntas.