Rinoceronte dentro do gato - execute o firmware no emulador Kopycat


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.


  1. copycat (inglês, n. [ˈkɒpɪkæt]) - copycat, imitador
  2. cat (inglês, n. [ˈkæt]) - um gato, um gato - um animal favorito de um dos criadores do projeto
  3. 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:


Circuito de dispositivo real

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:


Dispositivo emulado por circuito

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).

Portas COM virtuais


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):


Portas COM virtuais

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 ) :


Configurar portas COM virtuais

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

Configurando a conexão com o servidor GDB

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).


imagem


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:


imagem


imagem


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 .


imagem


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.


imagem


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.

Source: https://habr.com/ru/post/pt445798/


All Articles