Como parte de la reunión 0x0A DC7831
En este artículo describiremos cómo ejecutar el firmware del dispositivo en el emulador, demostraremos la interacción con el depurador y realizaremos un pequeño análisis dinámico del firmware.
Prehistoria
Hace mucho tiempo en una galaxia muy lejana
Hace un par de años en nuestro laboratorio surgió la necesidad de investigar el firmware de un dispositivo. El firmware se comprimió y descomprimió con un gestor de arranque. Lo hizo de una manera muy complicada, cambiando los datos en la memoria varias veces. Y el propio firmware interactuó activamente con los periféricos. Y todo ello sobre el núcleo MIPS.
Por razones objetivas, los emuladores disponibles no nos convenían, pero aun así queríamos ejecutar el código. Luego decidimos crear nuestro propio emulador, que haría lo mínimo y nos permitiría descomprimir el firmware principal. Lo probamos y funcionó. Pensamos, ¿y si añadimos periféricos para que también realicen el firmware principal? No me dolió mucho y funcionó. Lo pensamos de nuevo y decidimos crear un emulador completo.
El resultado fue un emulador de sistemas informáticos.
¿Por qué Kopycat?
Hay un juego de palabras.
- copycat (Inglés, sustantivo [ˈkɒpɪkæt]) - imitador, imitador
- gato (inglés, sustantivo [ˈkæt]) - gato, gato - el animal favorito de uno de los creadores del proyecto
- La letra “K” es del lenguaje de programación Kotlin.
Imitador
Al crear el emulador se fijaron objetivos muy concretos:
- la capacidad de crear rápidamente nuevos periféricos, módulos y núcleos de procesador;
- la capacidad de ensamblar un dispositivo virtual a partir de varios módulos;
- la capacidad de cargar cualquier dato binario (firmware) en la memoria de un dispositivo virtual;
- capacidad para trabajar con instantáneas (instantáneas del estado del sistema);
- la capacidad de interactuar con el emulador a través del depurador incorporado;
- Bonito lenguaje moderno para el desarrollo.
Como resultado, se eligió Kotlin para la implementación, la arquitectura del bus (aquí es cuando los módulos se comunican entre sí a través de buses de datos virtuales), JSON como formato de descripción del dispositivo y GDB RSP como protocolo para la interacción con el depurador.
El desarrollo lleva poco más de dos años y continúa activamente. Durante este tiempo, se implementaron núcleos de procesador MIPS, x86, V850ES, ARM y PowerPC.
El proyecto está creciendo y es hora de presentarlo al público en general. Haremos una descripción detallada del proyecto más adelante, pero por ahora nos centraremos en usar Kopycat.
Para los más impacientes, se puede descargar una versión promocional del emulador desde
Rinoceronte en el emulador
Recordemos que anteriormente en la conferencia SMARTRHINO-2018 se creó un dispositivo de prueba "Rhinoceros" para enseñar habilidades de ingeniería inversa. El proceso de análisis de firmware estático se describió en
Ahora intentemos agregar "altavoces" y ejecutar el firmware en el emulador.
Necesitamos:
1) java 1.8
2) Python y módulo
Para ventanas:
1)
2)
Para Linux:
1) socat
Puede utilizar Eclipse, IDA Pro o radare2 como cliente GDB.
Como funciona?
Para ejecutar el firmware en el emulador, es necesario "ensamblar" un dispositivo virtual, que es un análogo de un dispositivo real.
El dispositivo real (“rinoceronte”) se puede mostrar en el diagrama de bloques:
El emulador tiene una estructura modular y el dispositivo virtual final se puede describir en un archivo JSON.
JSON 105 líneas
{
"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"]
]
}
Presta atención al parámetro. firmware sección params es el nombre de un archivo que se puede cargar en un dispositivo virtual como firmware.
El dispositivo virtual y su interacción con el sistema operativo principal se puede representar mediante el siguiente diagrama:
La instancia de prueba actual del emulador implica la interacción con los puertos COM del sistema operativo principal (UART de depuración y UART para el módulo Bluetooth). Estos pueden ser puertos reales a los que están conectados los dispositivos o puertos COM virtuales (para esto solo necesita com0com/socat).
Actualmente existen dos formas principales de interactuar con el emulador desde el exterior:
- Protocolo GDB RSP (en consecuencia, las herramientas que soportan este protocolo son Eclipse / IDA / radare2);
- Línea de comando interna del emulador (Argparse o Python).
Puertos COM virtuales
Para interactuar con el UART de un dispositivo virtual en la máquina local a través de un terminal, necesita crear un par de puertos COM virtuales asociados. En nuestro caso, un puerto lo usa el emulador y el segundo lo usa un programa de terminal (PuTTY o pantalla):
Usando com0com
Los puertos COM virtuales se configuran utilizando la utilidad de configuración del kit com0com (versión de consola - C: Archivos de programa (x86) com0comsetupс.exe, o versión GUI - C: Archivos de programa (x86) com0comsetupg.exe):
Revisa las cajas habilitar la saturación del búfer para todos los puertos virtuales creados; de lo contrario, el emulador esperará una respuesta del puerto COM.
Usando socat
En los sistemas UNIX, los puertos COM virtuales son creados automáticamente por el emulador usando la utilidad socat; para hacer esto, simplemente especifique el prefijo en el nombre del puerto al iniciar el emulador. socat:
.
Interfaz de línea de comando interna (Argparse o Python)
Dado que Kopycat es una aplicación de consola, el emulador proporciona dos opciones de interfaz de línea de comandos para interactuar con sus objetos y variables: Argparse y Python.
Argparse es una CLI integrada en Kopycat y siempre está disponible para todos.
Una CLI alternativa es el intérprete de Python. Para usarlo, debe instalar el módulo Jep Python y configurar el emulador para que funcione con Python (se utilizará el intérprete de Python instalado en el sistema principal del usuario).
Instalación del módulo Python Jep
En Linux, Jep se puede instalar mediante pip:
pip install jep
Para instalar Jep en Windows, primero debe instalar el SDK de Windows y el correspondiente Microsoft Visual Studio. Te lo hemos puesto un poco más fácil y
pip install jep-3.8.2-cp27-cp27m-win_amd64.whl
Para verificar la instalación de Jep, debe ejecutar en la línea de comando:
python -c "import jep"
Se debe recibir el siguiente mensaje como respuesta:
ImportError: Jep is not supported in standalone Python, it must be embedded in Java.
En el archivo por lotes del emulador de su sistema (copycat.bat - para ventanas, imitador - para Linux) a la lista de parámetros DEFAULT_JVM_OPTS
agregar un parámetro adicional Djava.library.path
— debe contener la ruta al módulo Jep instalado.
El resultado para Windows debería ser una línea 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"
Lanzando Kopycat
El emulador es una aplicación JVM de consola. El lanzamiento se realiza a través del script de línea de comando del sistema operativo (sh/cmd).
Comando para ejecutar en Windows:
binkopycat -g 23946 -n rhino -l user -y library -p firmware=firmwarerhino_pass.bin,tty_dbg=COM26,tty_bt=COM28
Comando para ejecutar en Linux usando la utilidad 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
— Puerto TCP que estará abierto para acceder al servidor GDB;-n rhino
— nombre del módulo principal del sistema (dispositivo ensamblado);-l user
— nombre de la biblioteca para buscar el módulo principal;-y library
— ruta para buscar módulos incluidos en el dispositivo;firmwarerhino_pass.bin
— ruta al archivo de firmware;- COM26 y COM28 son puertos COM virtuales.
Como resultado, se mostrará un mensaje 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 >
Interacción con IDA Pro
Para simplificar las pruebas, utilizamos el firmware de Rhino como archivo fuente para el análisis en IDA en el formulario
También puede utilizar el firmware principal sin metainformación.
Después de iniciar Kopycat en IDA Pro, en el menú Depurador, vaya al elemento "Cambiar depurador…" y seleccione "Depurador remoto de GDB". A continuación, configure la conexión: menú Depurador: opciones de proceso…
Establezca los valores:
- Aplicación - cualquier valor
- Nombre de host: 127.0.0.1 (o la dirección IP de la máquina remota donde se ejecuta Kopycat)
- Puerto: 23946
Ahora el botón de depuración está disponible (tecla F9):
Haga clic en él para conectarse al módulo depurador en el emulador. IDA entra en modo de depuración, aparecen ventanas adicionales disponibles: información sobre los registros, sobre la pila.
Ahora podemos utilizar todas las funciones estándar del depurador:
- ejecución paso a paso de instrucciones (Entrar en и Dar un paso — teclas F7 y F8, respectivamente);
- iniciar y pausar la ejecución;
- crear puntos de interrupción tanto para el código como para los datos (tecla F2).
Conectarse a un depurador no significa ejecutar el código del firmware. La posición de ejecución actual debe ser la dirección. 0x08006A74
— inicio de la función Reiniciar_Handler. Si se desplaza hacia abajo en la lista, puede ver la llamada a la función principal. Puede colocar el cursor en esta línea (dirección 0x08006ABE
) y realizar la operación Ejecutar hasta el cursor (tecla F4).
A continuación, puede presionar F7 para ingresar a la función. principal.
Si ejecuta el comando Continuar proceso (tecla F9), luego aparecerá la ventana “Espere” con un solo botón Suspende los:
Cuando presionas Suspende los La ejecución del código del firmware se suspende y puede continuar desde la misma dirección en el código donde fue interrumpida.
Si continúas ejecutando el código, verás las siguientes líneas en los terminales conectados a los puertos COM virtuales:
La presencia de la línea "bypass de estado" indica que el módulo Bluetooth virtual ha cambiado al modo de recibir datos del puerto COM del usuario.
Ahora en el terminal Bluetooth (COM29 en la imagen) puede ingresar comandos de acuerdo con el protocolo Rhino. Por ejemplo, el comando “MEOW” devolverá la cadena “mur-mur” al terminal Bluetooth:
Emularme no del todo
Al crear un emulador, puedes elegir el nivel de detalle/emulación de un dispositivo en particular. Por ejemplo, el módulo Bluetooth se puede emular de diferentes formas:
- el dispositivo está completamente emulado con un conjunto completo de comandos;
- Se emula el comando AT y el flujo de datos se recibe desde el puerto COM del sistema principal;
- el dispositivo virtual proporciona una redirección completa de datos al dispositivo real;
- como un simple código auxiliar que siempre devuelve "OK".
La versión actual del emulador utiliza el segundo enfoque: el módulo Bluetooth virtual realiza la configuración, después de lo cual cambia al modo de "transferencia" de datos desde el puerto COM del sistema principal al puerto UART del emulador.
Consideremos la posibilidad de una instrumentación sencilla del código en caso de que alguna parte de la periferia no esté implementada. Por ejemplo, si no se ha creado un temporizador responsable de controlar la transferencia de datos a DMA (la verificación se realiza en la función ws2812b_esperasituado en 0x08006840
), entonces el firmware siempre esperará a que se restablezca la bandera ocupadosituado en 0x200004C4
que muestra la ocupación de la línea de datos DMA:
Podemos solucionar esta situación restableciendo manualmente la bandera. ocupado inmediatamente después de instalarlo. En IDA Pro, puede crear una función de Python y llamarla en un punto de interrupción, y colocar el punto de interrupción en el código después de escribir el valor 1 en la bandera. ocupado.
Manejador de puntos de interrupción
Primero, creemos una función de Python en IDA. Menú Archivo - Comando de secuencia de comandos...
Agregue un nuevo fragmento en la lista de la izquierda, asígnele un nombre (por ejemplo, BPT),
En el campo de texto de la derecha, ingrese el código de función:
def skip_dma():
print "Skipping wait ws2812..."
value = Byte(0x200004C4)
if value == 1:
PatchDbgByte(0x200004C4, 0)
return False
Después de eso presionamos Ejecutar y cierre la ventana del script.
Ahora vayamos al código en 0x0800688A
, establezca un punto de interrupción (tecla F2), edítelo (menú contextual Editar punto de interrupción...), no olvides configurar el tipo de script en Python:
Si el valor actual de la bandera ocupado es igual a 1, entonces debes ejecutar la función saltar_dma en la línea del script:
Si ejecuta el firmware para su ejecución, la activación del código del controlador de punto de interrupción se puede ver en la ventana IDA Salida nombre del autor Skipping wait ws2812...
. Ahora el firmware no esperará a que se restablezca la bandera. ocupado.
Interacción con el emulador
Es poco probable que la emulación por emular cause deleite y alegría. Es mucho más interesante si el emulador ayuda al investigador a ver los datos en la memoria o establecer la interacción de hilos.
Le mostraremos cómo establecer dinámicamente la interacción entre tareas RTOS. Primero debes pausar la ejecución del código si se está ejecutando. Si vas a la función entrada_tarea_bluetooth a la rama de procesamiento del comando "LED" (dirección 0x080057B8
), luego podrá ver lo que se crea primero y luego se envía a la cola del sistema ledControlQueueHandle algún mensaje.
Debes establecer un punto de interrupción para acceder a la variable. ledControlQueueHandlesituado en 0x20000624
y continúa ejecutando el código:
Como resultado, la parada se producirá primero en la dirección 0x080057CA
antes de llamar a la función osMailAlloc, luego en la dirección 0x08005806
antes de llamar a la función osMailPut, luego de un tiempo - a la dirección 0x08005BD4
(antes de llamar a la función osMailObtener), que pertenece a la función leds_task_entry (tarea LED), es decir, las tareas cambiaron y ahora la tarea LED recibió el control.
De esta sencilla forma puedes establecer cómo interactúan las tareas RTOS entre sí.
Por supuesto, en realidad la interacción de tareas puede ser más complicada, pero usando un emulador, rastrear esta interacción se vuelve menos laborioso.
Lanzamiento con Radare2
No se puede ignorar una herramienta tan universal como Radare2.
Para conectarse al emulador usando r2, el comando se vería así:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elf
Lanzamiento disponible ahora (dc
) y pausar la ejecución (Ctrl+C).
Desafortunadamente, por el momento, r2 tiene problemas al trabajar con el servidor gdb de hardware y el diseño de la memoria; debido a esto, los puntos de interrupción y los Pasos no funcionan (comando ds
). Esperamos que esto se solucione pronto.
Corriendo con Eclipse
Una de las opciones para utilizar el emulador es depurar el firmware del dispositivo que se está desarrollando. Para mayor claridad, también utilizaremos el firmware de Rhino. Puede descargar las fuentes del firmware.
Usaremos Eclipse del conjunto como IDE.
Para que el emulador cargue firmware compilado directamente en Eclipse, debe agregar el parámetro firmware=null
al comando de inicio del emulador:
binkopycat -g 23946 -n rhino -l user -y modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28
Configurar la configuración de depuración
En Eclipse, seleccione el menú Ejecutar - Configuraciones de depuración... En la ventana que se abre, en la sección Depuración de hardware de GDB debe agregar una nueva configuración, luego en la pestaña "Principal" especifique el proyecto actual y la aplicación para depurar:
En la pestaña "Depurador" debe especificar el comando GDB:
${openstm32_compiler_path}arm-none-eabi-gdb
Y también ingrese los parámetros para conectarse al servidor GDB (host y puerto):
En la pestaña "Inicio", debe especificar los siguientes parámetros:
- habilitar casilla de verificación Cargar imagen (para que la imagen del firmware ensamblada se cargue en el emulador);
- habilitar casilla de verificación Cargar símbolos;
- agregar comando de inicio:
set $pc = *0x08000004
(configure el registro de la PC al valor de la memoria en la dirección0x08000004
- la dirección se almacena allí Restablecer controlador).
Nota, si no desea descargar el archivo de firmware de Eclipse, entonces las opciones Cargar imagen и Ejecutar comandos no es necesario indicarlo.
Después de hacer clic en Depurar, puede trabajar en modo depurador:
- ejecución de código paso a paso
- interactuando con puntos de interrupción
Nota. Eclipse tiene, mmm... algunas peculiaridades... y tienes que vivir con ellas. Por ejemplo, si al iniciar el depurador aparece el mensaje “No hay fuente disponible para “0x0″”, entonces ejecute el comando Paso (F5)
En lugar de una conclusión
Emular código nativo es algo muy interesante. Es posible que un desarrollador de dispositivos depure el firmware sin un dispositivo real. Para un investigador, es una oportunidad para realizar análisis de código dinámico, lo que no siempre es posible ni siquiera con un dispositivo.
Queremos brindarles a los especialistas una herramienta que sea conveniente, moderadamente simple y que no requiera mucho esfuerzo ni tiempo para configurarla y ejecutarla.
Escribe en los comentarios sobre tu experiencia usando emuladores de hardware. Lo invitamos a discutir y estaremos encantados de responder sus preguntas.
Solo los usuarios registrados pueden participar en la encuesta.
¿Para qué estás usando el emulador?
-
Desarrollo (depuro) firmware
-
Estoy investigando firmware
-
Lanzo juegos (Dendi, Sega, PSP)
-
algo más (escribe en los comentarios)
7 usuarios votaron. 2 usuarios se abstuvieron.
¿Qué software utilizas para emular código nativo?
-
QEMU
-
motor unicornio
-
Proteo
-
algo más (escribe en los comentarios)
6 usuarios votaron. 2 usuarios se abstuvieron.
¿Qué te gustaría mejorar en el emulador que estás usando?
-
quiero velocidad
-
Quiero facilidad de configuración/inicio
-
Quiero más opciones para interactuar con el emulador (API, ganchos)
-
estoy feliz con todo
-
algo más (escribe en los comentarios)
8 usuarios votaron. 1 usuario se abstuvo.
Fuente: habr.com