Introducción
El firmware es el software embebido que controla el funcionamiento básico de dispositivos electrónicos, desde routers y cámaras IP hasta electrodomésticos inteligentes y sistemas industriales. A diferencia del software tradicional, el firmware opera directamente sobre el hardware, lo que lo convierte en un objetivo crítico en términos de seguridad, funcionalidad y privacidad.
El análisis de firmware consiste en examinar este software embebido para comprender su funcionamiento interno, detectar posibles vulnerabilidades, identificar puertas traseras, y en algunos casos, modificar su comportamiento o extraer información relevante. Este tipo de análisis es especialmente relevante en auditorías de seguridad, investigaciones forenses, ingeniería inversa o desarrollo de exploits.
En este caso, el firmware ha sido extraído de un dispositivo basado en el SoC Nordic nRF51, basado en el procesador Cortex-M0 con instrucciones ARM, a través de SWD tal como se muestra en este artículo. No solo se extrajo el firmware, sino toda la memoria interna desde la dirección hexadecimal 0x0 a la 0x40000, 262144 bytes, o 256 KB. Para el análisis utilizaremos el desensamblador y decompilador Ghidra.
Al desensamblar el archivo obtendremos las instrucciones en formato ensamblador ARM. Las instrucciones que hagan referencia a funciones del propio código se encontrarán en el propio rango 0x0-0x40000, pero el sistema embebido utilizará otras direcciones de memoria, por ejemplo, para la memoria RAM o para los periféricos, las cuáles Ghidra desconoce, por lo que tendremos que cargarlas desde un archivo CMSIS-SVD utilizando el plugin GhidraSVD.
Carga del firmware en Ghidra
Antes de cargar el firmware en Ghidra, debemos de tener en cuenta la dirección inicial del firmware, que podemos extraer de sus especificaciones en este caso en la sección 3.2 Memory.
Observamos que la dirección del código es la 0x00000000, la de los registros de fábrica (FICR) es la 0x10000000, la de los registro de usuario (UICR) es la 0x10001000, la de la memoria RAM es la 0x20000000, y la de los periféricos la 0x40000000 y la 0x50000000. Cada SoC tiene unas direcciones de memoria diferentes y si no se especifican correctamente en Ghidra se producirán errores en el desensamblado a la hora de buscar las referencias a las funciones. Por ejemplo el inicio de la memoria flash del STM32 es la dirección hexadecimal 0x0800000.
Tras abrir Ghidra creamos un nuevo proyecto File > New Project... de tipo Non-Shared Project, que se guardará en una carpeta ya existente previamente. A continuación cargamos el archivo desde la opción File > Import File... y seleccionamos el archivo binario. Nos aparece un formulario en el que debemos de especificar su formato Format, en este caso Raw Binary, su arquitectura Language, en este caso cortex (ARM Cortex / Thumb little endian).
Finalmente en Options... podemos especificar la dirección de inicio en la memoria Base Address en caso de que sea diferente a 0x0. En este caso clasificamos el nombre del bloque Block Name como Code.
El archivo se cargará en la lista del proyecto y podremos abrirlo seleccionando el archivo y arrastrándolo al botón de la herramienta CodeBrowser (ícono del dragón).

Análisis inicial
Al abrir la herramienta CodeBrowser se nos indica que el archivo no ha sido analizado y pregunta si queremos analizarlo. De momento lo ignoramos pulsando el botón No. En este momento ya nos aparece los primeros bytes hexadecimales del archivo sin analizar.
Antes de analizar el archivo de forma automática necesitamos cargar las direcciones de memoria que hacen referencia a los diferentes periféricos del SoC para facilitar el análisis. Por ejemplo, si encontramos una porción del código en la que se haga referencia a un pin GPIO podemos observar la placa (PCB) del dispositivo y con la opción de continuidad de un multímetro buscar hacia dónde está conectado ese pin. Puede estar conectado a un botón o a un diodo LED, como ejemplos. También necesitamos saber la dirección de memoria de la RAM, para así identificar lecturas y escrituras en la misma.
La descripción de estas direcciones se encuentran en los archivos CMSIS-SVD (System View Description). Podemos encontrar un repositorio de ellos en Github. Ghidra no puede cargar estos archivos de forma nativa por lo que usaremos el plugin GhidraSVD.
Instalación del plugin GhidraSVD
En primer lugar, descargamos la última versión del plugin para nuestra versión de Ghidra. Es importante que la versión del plugin coincida con la de Ghidra, si no el plugin no funcionará y devolverá una excepción.
$ wget https://github.com/antoniovazquezblanco/GhidraSVD/releases/download/v0.3.4/ghidra_11.3_PUBLIC_20250315_GhidraSVD.zip
El plugin se extrae en la carpeta GhidraSVD. A continuación volvemos a Ghidra y cargamos el plugin siguiendo los siguientes pasos: abrimos el gestor de plugins mediante File > Install Extensions en la ventana principal de Ghidra, pulsamos en el botón Add extension, se abrirá la ventana Select Extension y añadimos a la lista el archivo .zip del plugin que acabamos de descargar. Finalmente reiniciamos el programa y volvemos a cargar el projecto con CodeBrowser. Ahora nos aparece una opción para importar el archivo SVD en File > Load SVD File....
Carga del archivo SVD
Clonamos el repositorio que contiene los archivos CMSIS-SVD y buscamos el que se corresponde a nuestro SoC, en este caso cmsis-svd-data/data/Nordic/nrf51.svd. Lo cargamos con la opción descrita anteriormente.
$ git clone --depth=1 https://github.com/cmsis-svd/cmsis-svd-data
En algunos casos el archivo SVD no incluye el rango de la memoria RAM, por lo que deberemos de añadirlo manualmente. Consultando en la especificaciones, comprobamos que el tamaño máximo de memoria RAM es de 32 KB, 32768 bytes, o 0x8000 bytes. Por lo que empezando por la dirección 0x20000000 su dirección final será la 0x20008000. Para añadir el rango de memoria usamos la opción Window > Memory Map y pulsando en la opción Add a new block to the memory especificamos los datos. En principio será únicamente un área de lectura y escritura, que no contiene código Read and Write. Inicializamos la región con los bytes a 0x00.
Observamos todas las regiones de memoria.

Análisis
Ahora ya podemos analizar el archivo mediante la opción Analysis > Auto Analyze. Se abrirá la ventana Analysis Options en la que en este caso junto a las opciones predeterminadas se activará la opción Aggressively attempt to disassemble ARM/Thumb mixed code..
Tras unos segundos aparece el código desensamblado y decompilado en forma de pseudocódigo. Por ejemplo, nos encontramos una función en la dirección de memoria 0x000028f2 con el nombre auto-etiquetado LAB_000028f2.
Gracias a la carga del archivo SVD observamos referencias al registro del contador en tiempo real del dispositivo (RTC), RTC0.INTENCLR. Tendremos que consultar la documentación del fabricante para relacionar el nombre del registro con su significado. Por ejemplo para el nRF51 disponemos de su manual de referencia. En el caso de chips más modernos como el nRF52 tenemos más explicaciones detalladas en su web de documentación.
Conclusión
A partir de ahora el análisis se basará en leer los manuales de referencia del SoC para buscar las diferentes regiones de memoria del código, ya que no todo será código del usuario, en varias direcciones de memoria se encontrarán librerías del fabricante que se comunicarán directamente con los periféricos del dispositivo, por ejemplo en este caso la radio Bluetooth.