Nell'ambito della riunione 0x0A DC7831
In questo articolo descriveremo come eseguire il firmware del dispositivo nell'emulatore, dimostrare l'interazione con il debugger ed eseguire una piccola analisi dinamica del firmware.
Sfondo
Tanto tempo fa in una galassia molto lontana
Un paio di anni fa nel nostro laboratorio si è presentata la necessità di indagare sul firmware di un dispositivo. Il firmware è stato compresso e decompresso con un bootloader. Lo ha fatto in un modo molto complicato, spostando più volte i dati in memoria. E il firmware stesso ha quindi interagito attivamente con le periferiche. E tutto questo sul core MIPS.
Per ragioni oggettive, gli emulatori disponibili non erano adatti a noi, ma volevamo comunque eseguire il codice. Quindi abbiamo deciso di creare il nostro emulatore, che avrebbe fatto il minimo e ci avrebbe permesso di decomprimere il firmware principale. L'abbiamo provato e ha funzionato. Abbiamo pensato: e se aggiungessimo periferiche per eseguire anche il firmware principale? Non ha fatto molto male e ha anche funzionato. Ci abbiamo ripensato e abbiamo deciso di creare un emulatore a tutti gli effetti.
Il risultato è stato un emulatore di sistemi informatici
Perché Kopycat?
C'è un gioco di parole.
- copione (inglese, sostantivo [ˈkɒpɪkæt]) - imitatore, imitatore
- gatto (inglese, sostantivo [ˈkæt]) - gatto, gatto - l'animale preferito di uno dei creatori del progetto
- La lettera "K" deriva dal linguaggio di programmazione Kotlin
Copione
Durante la creazione dell'emulatore sono stati fissati obiettivi molto specifici:
- la capacità di creare rapidamente nuove periferiche, moduli, core del processore;
- la possibilità di assemblare un dispositivo virtuale da vari moduli;
- la possibilità di caricare qualsiasi dato binario (firmware) nella memoria di un dispositivo virtuale;
- capacità di lavorare con istantanee (istantanee dello stato del sistema);
- la possibilità di interagire con l'emulatore tramite il debugger integrato;
- bel linguaggio moderno per lo sviluppo.
Di conseguenza, per l'implementazione è stato scelto Kotlin, un'architettura bus (ovvero quando i moduli comunicano tra loro tramite bus dati virtuali), JSON come formato di descrizione del dispositivo e GDB RSP come protocollo per l'interazione con il debugger.
Lo sviluppo è in corso da poco più di due anni ed è attivamente in corso. Durante questo periodo sono stati implementati i core del processore MIPS, x86, V850ES, ARM e PowerPC.
Il progetto sta crescendo ed è giunto il momento di presentarlo al grande pubblico. Faremo una descrizione dettagliata del progetto in seguito, ma per ora ci concentreremo sull'utilizzo di Kopycat.
Per i più impazienti è possibile scaricare una versione promo dell'emulatore da
Rhino nell'emulatore
Ricordiamo che in precedenza, per la conferenza SMARTRHINO-2018, è stato creato un dispositivo di prova "Rhinoceros" per insegnare abilità di reverse engineering. Il processo di analisi statica del firmware è stato descritto in
Ora proviamo ad aggiungere "altoparlanti" ed eseguire il firmware nell'emulatore.
Abbiamo bisogno di:
1) Java1.8
2) Python e modulo
Per Windows:
1)
2)
Per Linux:
1) socat
Puoi utilizzare Eclipse, IDA Pro o radare2 come client GDB.
Come funziona?
Per eseguire il firmware nell'emulatore, è necessario "assemblare" un dispositivo virtuale, che è un analogo di un dispositivo reale.
Il dispositivo reale (“rinoceronte”) può essere mostrato nello schema a blocchi:
L'emulatore ha una struttura modulare e il dispositivo virtuale finale può essere descritto in un file JSON.
JSON 105 righe
{
"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"]
]
}
Prestare attenzione al parametro firmware sezione params è il nome di un file che può essere caricato in un dispositivo virtuale come firmware.
Il dispositivo virtuale e la sua interazione con il sistema operativo principale possono essere rappresentati dal seguente diagramma:
L'attuale istanza di test dell'emulatore prevede l'interazione con le porte COM del sistema operativo principale (debug UART e UART per il modulo Bluetooth). Queste possono essere porte reali a cui sono collegati i dispositivi o porte COM virtuali (per questo è sufficiente com0com/socat).
Attualmente esistono due modalità principali per interagire con l'emulatore dall'esterno:
- Protocollo GDB RSP (di conseguenza, gli strumenti che supportano questo protocollo sono Eclipse / IDA / radare2);
- riga di comando dell'emulatore interno (Argparse o Python).
Porte COM virtuali
Per interagire con l'UART di un dispositivo virtuale sulla macchina locale tramite un terminale, è necessario creare una coppia di porte COM virtuali associate. Nel nostro caso, una porta viene utilizzata dall'emulatore e la seconda da un programma terminale (PuTTY o schermo):
Utilizzando com0com
Le porte COM virtuali vengono configurate utilizzando l'utilità di configurazione del kit com0com (versione console - C:Programmi (x86)com0comsetupс.exe, o versione GUI - C:Programmi (x86)com0comsetupg.exe):
Seleziona le caselle abilitare il sovraccarico del buffer per tutte le porte virtuali create, altrimenti l'emulatore attenderà una risposta dalla porta COM.
Utilizzando socat
Sui sistemi UNIX le porte COM virtuali vengono create automaticamente dall'emulatore tramite l'utility socat; per fare ciò basta specificare il prefisso nel nome della porta all'avvio dell'emulatore socat:
.
Interfaccia a riga di comando interna (Argparse o Python)
Poiché Kopycat è un'applicazione console, l'emulatore fornisce due opzioni di interfaccia a riga di comando per interagire con i suoi oggetti e variabili: Argparse e Python.
Argparse è una CLI integrata in Kopycat ed è sempre disponibile per tutti.
Una CLI alternativa è l'interprete Python. Per usarlo, è necessario installare il modulo Jep Python e configurare l'emulatore per funzionare con Python (verrà utilizzato l'interprete Python installato sul sistema principale dell'utente).
Installazione del modulo Python Jep
Sotto Linux Jep può essere installato tramite pip:
pip install jep
Per installare Jep su Windows è necessario prima installare Windows SDK e il corrispondente Microsoft Visual Studio. Abbiamo reso le cose un po' più facili per te e
pip install jep-3.8.2-cp27-cp27m-win_amd64.whl
Per verificare l'installazione di Jep è necessario eseguire da riga di comando:
python -c "import jep"
In risposta si dovrebbe ricevere il seguente messaggio:
ImportError: Jep is not supported in standalone Python, it must be embedded in Java.
Nel file batch dell'emulatore per il tuo sistema (copycat.bat - per Windows, copione - per Linux) all'elenco dei parametri DEFAULT_JVM_OPTS
aggiungere un ulteriore parametro Djava.library.path
— deve contenere il percorso del modulo Jep installato.
Il risultato per Windows dovrebbe essere una riga come questa:
set DEFAULT_JVM_OPTS="-XX:MaxMetaspaceSize=256m" "-XX:+UseParallelGC" "-XX:SurvivorRatio=6" "-XX:-UseGCOverheadLimit" "-Djava.library.path=C:/Python27/Lib/site-packages/jep"
Avvio di Kopycat
L'emulatore è un'applicazione JVM console. L'avvio viene effettuato tramite lo script della riga di comando del sistema operativo (sh/cmd).
Comando da eseguire in Windows:
binkopycat -g 23946 -n rhino -l user -y library -p firmware=firmwarerhino_pass.bin,tty_dbg=COM26,tty_bt=COM28
Comando da eseguire sotto Linux utilizzando l'utilità 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 che sarà aperta per l'accesso al server GDB;-n rhino
— nome del modulo principale del sistema (dispositivo assemblato);-l user
— nome della libreria in cui ricercare il modulo principale;-y library
— percorso per la ricerca dei moduli presenti nel dispositivo;firmwarerhino_pass.bin
— percorso del file del firmware;- COM26 e COM28 sono porte COM virtuali.
Di conseguenza, verrà visualizzato un messaggio Python >
(o 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 >
Interazione con IDA Pro
Per semplificare i test, utilizziamo il firmware Rhino come file sorgente per l'analisi in IDA nel modulo
È inoltre possibile utilizzare il firmware principale senza metainformazioni.
Dopo aver avviato Kopycat in IDA Pro, nel menu Debugger vai alla voce “Cambia debugger..." e seleziona "Debugger GDB remoto". Successivamente, imposta la connessione: menu Debugger - Opzioni di processo…
Imposta i valori:
- Applicazione: qualsiasi valore
- Nome host: 127.0.0.1 (o l'indirizzo IP del computer remoto su cui è in esecuzione Kopycat)
- Porto: 23946
Ora il pulsante di debug diventa disponibile (tasto F9):
Fare clic per connettersi al modulo debugger nell'emulatore. IDA entra in modalità debug, diventano disponibili finestre aggiuntive: informazioni sui registri, sullo stack.
Ora possiamo utilizzare tutte le funzionalità standard del debugger:
- esecuzione passo passo delle istruzioni (Entra и Scavalcare — rispettivamente i tasti F7 e F8);
- avviare e sospendere l'esecuzione;
- creazione di punti di interruzione sia per il codice che per i dati (tasto F2).
La connessione a un debugger non significa eseguire il codice del firmware. La posizione di esecuzione corrente deve essere l'indirizzo 0x08006A74
— inizio della funzione Reset_Handler. Se scorri l'elenco verso il basso, puoi vedere la chiamata alla funzione principale. È possibile posizionare il cursore su questa riga (indirizzo 0x08006ABE
) ed eseguire l'operazione Esegui fino al cursore (tasto F4).
Successivamente, puoi premere F7 per accedere alla funzione principale.
Se esegui il comando Continua il processo (tasto F9), quindi verrà visualizzata la finestra "Attendere" con un solo pulsante Sospendere:
Quando premuto Sospendere l'esecuzione del codice firmware è sospesa e può essere ripresa dallo stesso indirizzo presente nel codice dove è stata interrotta.
Se prosegui nell'esecuzione del codice, nei terminali collegati alle porte COM virtuali vedrai le seguenti righe:
La presenza della linea "state bypass" indica che il modulo Bluetooth virtuale è passato alla modalità di ricezione dati dalla porta COM dell'utente.
Ora nel terminale Bluetooth (COM29 nella foto) puoi inserire comandi secondo il protocollo Rhino. Ad esempio, il comando “MEOW” restituirà la stringa “mur-mur” al terminale Bluetooth:
Emulami non del tutto
Quando crei un emulatore, puoi scegliere il livello di dettaglio/emulazione di un particolare dispositivo. Ad esempio, il modulo Bluetooth può essere emulato in diversi modi:
- il dispositivo è completamente emulato con un set completo di comandi;
- I comandi AT vengono emulati e il flusso di dati viene ricevuto dalla porta COM del sistema principale;
- il dispositivo virtuale fornisce il reindirizzamento completo dei dati al dispositivo reale;
- come un semplice stub che restituisce sempre "OK".
La versione attuale dell'emulatore utilizza il secondo approccio: il modulo Bluetooth virtuale esegue la configurazione, dopodiché passa alla modalità di "proxying" dei dati dalla porta COM del sistema principale alla porta UART dell'emulatore.
Consideriamo la possibilità di una semplice strumentazione del codice nel caso in cui qualche parte della periferia non sia implementata. Ad esempio, se non è stato creato un timer responsabile del controllo del trasferimento dei dati al DMA (il controllo viene eseguito nella funzione ws2812b_aspettasituato in 0x08006840
), il firmware attenderà sempre il ripristino del flag occupatosituato in 0x200004C4
che mostra l'occupazione della linea dati DMA:
Possiamo aggirare questa situazione reimpostando manualmente il flag occupato subito dopo averlo installato. In IDA Pro, puoi creare una funzione Python e chiamarla in un punto di interruzione e inserire il punto di interruzione stesso nel codice dopo aver scritto il valore 1 nel flag occupato.
Gestore del punto di interruzione
Innanzitutto, creiamo una funzione Python in IDA. Menù File - Comando script...
Aggiungi un nuovo snippet nell'elenco a sinistra, dagli un nome (ad esempio, BPT),
Nel campo di testo a destra, inserisci il codice funzione:
def skip_dma():
print "Skipping wait ws2812..."
value = Byte(0x200004C4)
if value == 1:
PatchDbgByte(0x200004C4, 0)
return False
Dopodiché premiamo Correre e chiudi la finestra dello script.
Ora passiamo al codice su 0x0800688A
, imposta un punto di interruzione (tasto F2), modificalo (menu contestuale Modifica punto di interruzione...), non dimenticare di impostare il tipo di script su Python:
Se il valore del flag corrente occupato è uguale a 1, allora dovresti eseguire la funzione skip_dma nella riga di script:
Se si esegue il firmware per l'esecuzione, l'attivazione del codice del gestore del breakpoint può essere vista nella finestra IDA Uscita per linea Skipping wait ws2812...
. Ora il firmware non attenderà il ripristino del flag occupato.
Interazione con l'emulatore
È improbabile che l'emulazione fine a se stessa provochi gioia e gioia. È molto più interessante se l'emulatore aiuta il ricercatore a vedere i dati in memoria o a stabilire l'interazione dei thread.
Ti mostreremo come stabilire dinamicamente l'interazione tra le attività RTOS. Dovresti prima mettere in pausa l'esecuzione del codice se è in esecuzione. Se vai alla funzione voce_attività_bluetooth al ramo di elaborazione del comando “LED” (indirizzo 0x080057B8
), allora potrai vedere cosa viene prima creato e poi inviato alla coda di sistema ledControlQueueHandle qualche messaggio.
Dovresti impostare un punto di interruzione per accedere alla variabile ledControlQueueHandlesituato in 0x20000624
e continua con l'esecuzione del codice:
Di conseguenza, la fermata avverrà prima all'indirizzo 0x080057CA
prima di chiamare la funzione osMailAlloc, poi all'indirizzo 0x08005806
prima di chiamare la funzione osMailPut, poi dopo un po '- all'indirizzo 0x08005BD4
(prima di chiamare la funzione osMailGet), che appartiene alla funzione leds_task_entry (attività LED), ovvero le attività sono cambiate e ora l'attività LED ha ricevuto il controllo.
In questo modo semplice puoi stabilire come i task RTOS interagiscono tra loro.
Naturalmente, in realtà, l'interazione delle attività può essere più complicata, ma utilizzando un emulatore, tracciare questa interazione diventa meno laborioso.
Avvia con Radare2
Non puoi ignorare uno strumento così universale come Radare2.
Per connettersi all'emulatore utilizzando r2, il comando sarebbe simile al seguente:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elf
Lancio disponibile ora (dc
) e sospendere l'esecuzione (Ctrl+C).
Purtroppo al momento r2 ha problemi nel lavorare con il server hardware gdb e il layout della memoria; per questo motivo i breakpoint e gli Step non funzionano (comando ds
). Ci auguriamo che questo venga risolto presto.
Correre con Eclipse
Una delle opzioni per utilizzare l'emulatore è eseguire il debug del firmware del dispositivo in fase di sviluppo. Per chiarezza utilizzeremo anche il firmware Rhino. È possibile scaricare i sorgenti del firmware
Utilizzeremo Eclipse dal set come IDE
Affinché l'emulatore possa caricare il firmware compilato direttamente in Eclipse, è necessario aggiungere il parametro firmware=null
al comando di avvio dell'emulatore:
binkopycat -g 23946 -n rhino -l user -y modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28
Impostazione della configurazione di debug
In Eclipse, seleziona il menu Esegui - Debug configurazioni... Nella finestra che si apre, nella sezione Debug hardware GDB è necessario aggiungere una nuova configurazione, quindi nella scheda "Principale" specificare il progetto corrente e l'applicazione per il debug:
Nella scheda "Debugger" è necessario specificare il comando GDB:
${openstm32_compiler_path}arm-none-eabi-gdb
E inserisci anche i parametri per la connessione al server GDB (host e porta):
Nella scheda “Avvio”, è necessario specificare i seguenti parametri:
- abilita la casella di controllo Carica immagine (in modo che l'immagine del firmware assemblato venga caricata nell'emulatore);
- abilita la casella di controllo Carica simboli;
- aggiungi il comando di avvio:
set $pc = *0x08000004
(impostare il registro PC sul valore dalla memoria all'indirizzo0x08000004
- L'indirizzo è memorizzato lì ResetHandler).
Nota, se non desideri scaricare il file del firmware da Eclipse, le opzioni Carica immagine и Esegui comandi non c'è bisogno di indicarlo.
Dopo aver fatto clic su Debug, puoi lavorare in modalità debugger:
- esecuzione del codice passo dopo passo
- interagire con i punti di interruzione
Nota. Eclipse ha, hmm... alcune stranezze... e devi conviverci. Ad esempio, se all'avvio del debugger appare il messaggio “Nessuna sorgente disponibile per “0x0″”, allora eseguire il comando Step (F5)
Invece di una conclusione
Emulare il codice nativo è una cosa molto interessante. Diventa possibile per uno sviluppatore di dispositivi eseguire il debug del firmware senza un dispositivo reale. Per un ricercatore è un'opportunità per condurre analisi dinamiche del codice, cosa che non sempre è possibile nemmeno con un dispositivo.
Vogliamo fornire agli specialisti uno strumento che sia conveniente, moderatamente semplice e che non richieda molto tempo e impegno per la configurazione e l'esecuzione.
Scrivi nei commenti la tua esperienza con gli emulatori hardware. Vi invitiamo a discutere e saremo felici di rispondere alle domande.
Solo gli utenti registrati possono partecipare al sondaggio.
Per cosa stai usando l'emulatore?
-
Sviluppo (debug) firmware
-
Sto ricercando il firmware
-
Lancio giochi (Dendi, Sega, PSP)
-
qualcos'altro (scrivi nei commenti)
7 utenti hanno votato. 2 utenti si sono astenuti.
Quale software usi per emulare il codice nativo?
-
QEMU
-
Motore dell'unicorno
-
Proteus
-
qualcos'altro (scrivi nei commenti)
6 utenti hanno votato. 2 utenti si sono astenuti.
Cosa vorresti migliorare nell'emulatore che stai utilizzando?
-
Voglio velocità
-
Voglio facilità di installazione/avvio
-
Voglio più opzioni per interagire con l'emulatore (API, hook)
-
Sono felice di tutto
-
qualcos'altro (scrivi nei commenti)
8 utenti hanno votato. 1 utente si è astenuto.
Fonte: habr.com