Cabecera blog ciberseguridad

Ingeniería inversa de dispositivos Dahua NVR/XVR y ruptura de su seguridad de arranque

Antonio Vázquez Blanco ha realizado este artículo sobre Ingeniería Inversa de dispositivos Dahua

Los dispositivos embebidos modernos incorporan cada vez más mecanismos de seguridad y van mucho más allá del modelo IoT tradicional. Cifrado, bootloaders personalizados, firmware firmado… todo esto se está volviendo la norma, al menos en teoría. En la práctica, sin embargo, muchas implementaciones todavía no cumplen los principios de diseño seguro.

En este artículo se presenta un recorrido técnico siguiendo el análisis de dos dispositivos de grabación de vídeo, mostrando cómo el acceso al hardware, el comportamiento del bootloader y las rutinas criptográficas revelan debilidades que permiten a un atacante sortear las barreras de seguridad implementadas.

Esta investigación se llevó a cabo originalmente como parte de un ejercicio de formación interno en seguridad hardware y amplía las notas y hallazgos compartidos en una charla presentada en la conferencia BlackAlps 2025.

Contexto

Los siguientes descubrimientos surgen de un ejercicio de formación interna centrado en la evaluación práctica de la seguridad hardware.

La evaluación se centra en dos unidades de grabación de vídeo de Dahua: DHI-NVR2104-4KS2 y DH-XVR4104HS-I. Ambos dispositivos comparten hardware similar, distribuciones de firmware semejantes y procesos de arranque equivalentes.

El objetivo inicial es simple: extraer el firmware más reciente directamente del dispositivo. Este es un objetivo realista para atacantes que logran un compromiso físico o manipulación en la cadena de suministro, y encaja con las habilidades a practicar durante el entrenamiento.

Superficie de ataque hardware

Al abrir los grabadores aparece el conjunto habitual de componentes para dispositivos de esta clase. Destaca la cantidad de conectores destinados a que los usuarios conecten otros dispositivos, como los puertos traseros para periféricos, o el conector SATA interno para un disco duro.

Junto a estos puertos documentados y fáciles de identificar, se observan otros muchos puertos sin documentar que pueden servir para obtener un acceso inicial al dispositivo.

Acceso UART

Utilizando un analizador lógico, se sondean los diferentes conectores de la placa. Se encuentran indicios de interfaces de depuración (JTAG/SWD), pero encontrar una simple UART funcional ya proporciona una posible vía de acceso al dispositivo.

Una vez identificado el puerto UART, se suelda un adaptador UART-USB para poder leer y escribir a este puerto.

La UART, durante el arranque, muestra el siguiente texto:

System startup
U-Boot 2010.06-svn4868 (Sep 24 2020 - 09:56:33)
Check Flash Memory ContCheck Flash … Found
ECC provided by Flash Memory Controller
Hit any key to stop autoboot: 3 … 2 … 1 … 0

Image Name: Linux-3.18.20
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 3768619 Bytes = 3.6 MiB
Load Address: 80008000
Entry Point: 80008000
Kernel data secure check, please wating …
sec_commonSwRsaVerify run successfully!
Loading Kernel Image … OK
Starting kernel …

El bootloader utilizado es U-Boot 2010.06-svn4868, probablemente con modificaciones específicas de Dahua para realizar comprobaciones de seguridad no estándar o verificaciones de firma de software.

Podemos ver mensajes relativos a la carga de una versión antigua de Linux, pero después de arrancar el Kernel no aparece ningún texto ni el dispositivo da acceso a una terminal.

Afortunadamente, U-Boot parece permitir interrumpir el arranque. Tras varios intentos manuales fallidos, se decidió automatizar el proceso para encontrar el carácter de interrupción.

Usando Python es posible interactuar con dispositivos seriales mediante el paquete pyserial. Lamentablemente, debido a la naturaleza de los protocolos de comunicación, no es rápido crear scripts que esperen texto y reaccionen a él. Por suerte, el paquete pexpect-serialspawn permite precisamente esto. Combina la posibilidad de esperar texto y actuar, y gestiona los flujos del puerto serie, permitiéndonos hacer exactamente lo que necesitamos.

La función de Python anterior usa pexpect-serialspawn para esperar el texto que indica que el dispositivo acaba de iniciar el proceso de arranque. Cuando se detecta este texto, procede a enviar 100 caracteres para asegurarse de no perder la interrupción del bootloader y, finalmente, se comprueba si aparecen cadenas conocidas que forman parte del proceso de arranque habitual. Esta última comprobación permite detectar si el dispositivo arrancó con normalidad o si fue interrumpido.

Esta función de prueba se ejecuta para cada carácter ASCII. El script logró revelar que el proceso de arranque podía interrumpirse enviando el carácter *, lo que daba acceso a una shell del bootloader.

Capacidades y restricciones del bootloader

Debido al desconocimiento acerca de que comandos de U-Boot se encuentran disponibles, el primer comando ejecutado fue el de ayuda:

hisilicon # help
? - alias for 'help'
autoup - load update file from server
boot - boot kernel from uboot
bootm - boot application image from memory
decjpg - jpgd - decode jpeg picture.
devid - devid - set hardware id and save to flashfatload - load binary file from a dos filesystem
fatls - list files in a directory (default /)
fb_set - fb_set - get shift key
fb_test - fb_test - frontboard read/write test
get_key - get_key - get shift key
help - print command description/usage
kaimendaji - kai men da ji
lock_otp - lock_otp - otp lock

Aunque existen muchos comandos interesantes disponibles, su ejecución no produce ninguna salida… Algún tipo de mecanismo parece impedir su ejecución…

Como ejemplo, el comando devid sí genera salida, pero muchos otros no:

hisilicon # devid
DEVID: DHI-NVR2104-4KS2
hisilicon # nand
nand - NAND sub-system
hisilicon # nand read
not support this cmd
hisilicon # nandops read
hisilicon # nandops read 0x0
hisilicon # xhprint
hisilicon # xhprint ID
hisilicon # xhprintenv
hisilicon # xhprintenv ID

En este punto de la auditoría, dado que el bootloader no permite la extracción del firmware, se retoma el análisis físico del hardware.

Volcado de memoria

Una segunda inspección del hardware muestra que el dispositivo utiliza memoria NAND Flash externa que probablemente aloja el firmware. Se desuelda la memoria y se vuelca usando un programador comercial.

Un análisis rápido del volcado con Binwalk no arroja resultados claros. La presencia de demasiados falsos positivos oculta la posible información relevante para nosotros…

% binwalk dahua_nvr.bin
DECIMAL HEXADECIMAL DESCRIPTION
-------------------------------
262144  0x40000     Flattened device tree, size: 15970 bytes, version: 17
1130639 0x11408F    ESP Image segment count: 1, flash mode: QUIO, flash size: 2MB, entry address: 0x4dd038e3, hash: none
…
1288009 0x13A749    eCos RTOS string reference: "ecos %s"
1296358 0x13C7E6    MP3 ID3 tag,
1296508 0x13C87C    SHA256 hash constants, little endian
…
1319959 0x142417    AES Inverse S-Box
…
4456448 0x440000    Squashfs filesystem, little endian, version 4.0, compression:gzip, size: 40899891 bytes, 9 inodes, blocksize: 131072 bytes, created: 2021-03-12 06:37:13
58196508 0x378021C  Zlib compressed data, best compression
…

En estos casos, otra herramienta puede ofrecer información relevante para trabajar con este volcado de memoria. Es posible representar la entropía del archivo para identificar dónde se concentra la densidad de información en el volcado. Esto nos permite localizar las particiones y el espacio libre entre ellas

Dado que el principal interés de obtener el firmware se concentra en los binarios añadidos por el fabricante, en su mayoría, las cosas de interés deberían encontrarse en la partición de espacio de usuario, donde residen los servicios y otros ejecutables.

Debido a su contenido, la partición de espacio de usuario suele ser la de mayor tamaño.

A partir del gráfico de entropía, se puede localizar el inicio y el final aproximado de las particiones e intentar extraer la parte relevante del archivo en un nuevo binario. Para ello puede utilizarse la utilidad dd.

% dd if=dahua_nvr.bin of=dahua_nvr_userspace.bin bs=$((0x10000)) skip=$((0x44)) count=$((0x271))
40960000 bytes (41 MB, 39 MiB) copied, 0,614597 s, 66,6 MB/s

Binwalk nos permite validar que el corte del fichero se ha hecho de manera adecuada:

% binwalk dahua_nvr_userspace.bin

DECIMAL HEXADECIMAL DESCRIPTION
-------------------------------
0       0x0         Squashfs filesystem, little endian, version 4.0, compression:gzip, size: 40899891 bytes, 9 inodes, blocksize: 131072 bytes, created: 2021-03-12 06:37:13

Finalmente, se recomienda usar la utilidad unsquashfs para la extracción del contenido del sistema de archivos. Esta herramienta ofrece algunas ventajas frente a Binwalk para esta operación ya que proporciona más información sobre si la extracción se completó correctamente o si se produjeron errores durante el proceso.

% unsquashfs dahua_xvr_userspace.bin
Parallel unsquashfs: Using 2 processors
3 inodes (314 blocks) to write
[===============================================================/] 317/317 100%

Después de una extracción exitosa, investigamos más a fondo los resultados. Se imprime el árbol de archivos descomprimidos y se encuentra que algunos de ellos apuntan a que se trata del Kernel y de la partición de espacio de usuario. También hay un archivo LUA que parece contener algún tipo de información del hardware del dispositivo.

% tree
.
├── boot
│ └── uImage
├── dev
├── romfs-x.squashfs
├── root
└── usr
└── data
└── hardware.lua

Desafortunadamente, Binwalk no reconoce nada dentro de estos archivos. Además, las mediciones de entropía de estos archivos revelan que probablemente se encuentren cifrados

% binwalk boot/uImage
DECIMAL HEXADECIMAL DESCRIPTION
-------------------------------
26308   0x66C4      xz compressed data
% binwalk romfs-x.squashfs
DECIMAL HEXADECIMAL DESCRIPTION
-------------------------------
…
12181334 0xB9DF56 MP3 ID3 tag,
…
% binwalk usr/data/hardware.lua
DECIMAL HEXADECIMAL DESCRIPTION
-------------------------------

Por suerte, algo debe descifrar esos archivos… Ese “algo” probablemente sea el bootloader, ya que es el responsable de cargarlos…

En este punto, asumimos que el bootloader será la primera partición de la memoria y lo extraeremos a un nuevo binario utilizando la misma técnica empleada anteriormente.

% dd if=dahua_nvr.bin of=dahua_nvr_bootloader.bin bs=$((0x1000)) skip=$((0x0)) count=$((0xF))
15+0 records in
15+0 records out
61440 bytes (61 kB, 60 KiB) copied, 0,00398486 s, 15,4 MB/s

Se valida que el archivo resultante contiene instrucciones ARM (mediante el flag –Y de binwalk), ya que se espera que el bootloader sea un archivo ejecutable “RAW” con instrucciones de máquina.

% binwalk -Y dahua_nvr_bootloader.bin
DECIMAL HEXADECIMAL DESCRIPTION
-------------------------------
4396    0x112C      ARM executable code, 16-bit (Thumb), little endian, at least 1002 valid instructions

Validada nuestra hipótesis, analizamos el bootloader utilizando Ghidra…

Ingeniería inversa del bootloader

Dado que el bootloader es un binario “raw”, es necesario recopilar información sobre su arquitectura y sobre la dirección en la que se carga. La arquitectura es ARM little endian, tal como indicó Binwalk, pero no conocemos la dirección de carga. Por esta razón, el binario se carga inicialmente en la dirección “0” dentro de Ghidra.

Las primeras instrucciones que encontramos al inicio del bootloader forman una IVT (Interrupt Vector Table). Esta tabla apunta a distintas funciones que gestionan excepciones y otros eventos. En particular, la primera entrada contiene un salto relativo y no aporta mucha información sobre la dirección de carga, pero la segunda contiene un salto absoluto y, por tanto, una dirección absoluta de referencia, dentro del espacio de código del bootloader. La dirección del salto es 0x808016a0. Dado que los bootloaders suelen cargarse en direcciones de memoria alineadas, redondear dicha dirección nos proporciona una dirección posible de carga: 0x80800000.

Para confirmar que 0x80800000 es la dirección de carga correcta, el binario se vuelve a cargar en esa dirección y, al revisar las cadenas de texto en Ghidra, se observa que casi todas tienen referencias válidas. Esto indica que probablemente se trata de la dirección de carga correcta.

Ahora que el binario está listo para ser analizado, y usando la información de versión del bootloader observada durante el proceso de arranque, es posible localizar el código fuente original de U-Boot en https://github.com/u-boot/u-boot/tree/v2010.06.

Una técnica útil para facilitar el proceso de ingeniería inversa consiste en comparar las cadenas que vemos en la salida del dispositivo, en el código fuente y en el binario dentro de Ghidra. De este modo, podemos relacionar algunas de las funciones entre el código fuente y el binario, y etiquetarlas progresivamente para ir obteniendo contexto.

Un posible punto de partida es el mensaje “Hit any key to stop autoboot”, que se imprime momentos antes de acceder a la línea de comandos del bootloader…

Esto nos permite a confirmar cuál es el carácter que detiene el autoboot y, siguiendo el código fuente original, se localizan las funciones relacionadas con la línea de comandos del bootloader:

Finalmente, la función run_command nos lleva a la tabla de comandos de U-Boot.

En Ghidra, para definir tipos de datos complejos, el usuario debe introducir esa información manualmente. Esto puede ser un proceso tedioso por lo que, en este ejemplo, se utilizó el complemento GhidraExtendedSourceParser para ahorrar tiempo y esfuerzo. Este permite simplemente copiar y pegar el código fuente disponible, y crea automáticamente los tipos de tablas por nosotros.

Haz clic para reproducir el gif.

Ahora que la tabla de comandos es legible, es posible examinar la implementación de cada uno de los comandos que anteriormente estaban bloqueados, como xhprintenv.

El comando comienza realizando una comprobación inicial de si el dispositivo se encuentra bloqueado. Si dicha comprobación indica que el comando está bloqueado, la función simplemente retorna sin hacer nada más.

En un análisis más profundo de la función de comprobación de bloqueo, se observa que se utiliza una variable de entorno llamada “dajidali” para verificar si se ha establecido una contraseña. En particular, la función de verificación de contraseña está relacionada con otro comando personalizado del bootloader llamado “kaimendaji”.

Suponiendo que el fabricante es chino y dado que los nombres de las variables no tienen mucho sentido en inglés, una posible traducción para ambos nombres es…

El proceso de autenticación

El comando personalizado kaimendaji espera una contraseña introducida por el usuario. Después realiza ciertas operaciones criptográficas y comprueba los resultados. Para identificar las operaciones criptográficas relacionadas con este comando, se utiliza el plugin GhidraFindCrypt.

En concreto, el plugin busca constantes criptográficas comunes y las etiqueta. Gracias a ello podemos identificar la función de hash MD5.

Con esta información incorporada en Ghidra, ahora podemos examinar el comando kaimendaji con un poco más de contexto. El comando verifica que se haya pasado un parámetro; este parámetro es la contraseña y debe tener una longitud de 6 caracteres. Si se cumplen las condiciones, se llama a una función de verificación de contraseña.

La función de comprobación de la contraseña simplemente convierte el texto hexadecimal introducido en bytes y llama a una función secundaria que comprobará la contraseña en formato binario. A partir de esto, concluimos que la contraseña tiene solo 3 bytes de longitud y, por tanto, es susceptible a un ataque de fuerza bruta… pero un análisis más profundo revela detalles interesantes…

Finalmente, la función que comprueba los bytes obtiene el “id” del dispositivo y la dirección MAC ethernet, calcula sus hashes MD5 y los combina mediante la función XOR para generar un valor con el que comparar la contraseña.

¡Usando esta información, hemos construido una función keygen!

Ahora, para generar una contraseña válida, necesitamos conocer la dirección Ethernet y el “id” de nuestro dispositivo. Desafortunadamente, no podemos imprimirlos usando el bootloader. La dirección MAC ethernet puede encontrarse en la etiqueta del dispositivo, pero no sabemos cuál es el “id” de nuestro dispositivo…

Al revisar nuevamente los comandos disponibles en el bootloader, encontramos otro comando personalizado llamado “show_hello”.

Un análisis más profundo de la implementación del programa revela que este comando consulta ambos parámetros necesarios y los mezcla, en la función etiquetada como packSrc(), para generar una salida de texto que se imprime en la línea de comandos…

Después de comprender la función de empaquetado packSrc(), se desarrolla una función que implementa la operación opuesta:

Ahora los parámetros “id” y MAC, necesarios para generar la contraseña, pueden obtenerse desde la interfaz de comandos del bootloader.

Prueba de concepto

La siguiente imagen muestra el proceso de arranque. Este proceso se interrumpe presionando varias veces el carácter “*”, lo que da acceso a la línea de comandos del bootloader.

En este punto, se prueba el acceso usando el comando xhprintenv, pero no muestra ninguna salida…

Luego, el comando hello_world se utiliza para recuperar los datos codificados del “id” y la dirección MAC. Usando el keygen, se genera una contraseña a partir de esos datos.

Se utiliza el comando kaimendaji para elevar privilegios y, mediante xhprintenv, verificamos que efectivamente tenemos privilegios elevados. ¡Ahora las variables de entorno se imprimen!

Haz clic para reproducir el gif.

Siguientes pasos

A pesar de ello, después de elevar privilegios, el comando xhprint no funciona. Ese comando era un posible candidato para volcar el contenido de la memoria del dispositivo. A pesar de no disponer de xhprint, podemos volcar la memoria descifrada del dispositivo utilizando otros comandos.

Para volcar memoria descifrada, usamos el comando partload para cargar en memoria el contenido descifrado de una partición. Como no podemos imprimir directamente la memoria, usamos el comando “nandops 1” (escritura en NAND) para escribir dicha memoria descifrada en una región vacía de la NAND. Después de eso, el comando “nand dump” permite imprimir la región NAND descifrada.

También resulta interesante investigar las variables de entorno del bootloader. Algunos ejemplos son los siguientes:

# Enables verbose kernel & services
hisilicon# setenv dh_keyboard 0

# Stops automatic services and drops a shell
hisilicon# setenv appauto 0

# Prevents from loading some manufacturer kernel modules
hisilicon# setenv load_modules 0

Conclusiones

Los dispositivos Dahua analizados incluyen mecanismos avanzados de seguridad, como cifrado de particiones, verificación de firmas de software y un bootloader personalizado con rutinas de autenticación…

Sin embargo, las debilidades en su implementación impiden que estos mecanismos ofrecieran garantías de seguridad reales. Debido a que el bootloader no se encuentra firmado, un atacante puede extraerlo, modificarlo o reemplazarlo. Las protecciones contra la escalada de privilegios en el bootloader no están correctamente diseñadas, y las claves de cifrado son accesibles o deducibles dentro del propio bootloader. Finalmente, el cifrado puede ser eludido utilizando funciones internas diseñadas originalmente para mantenimiento.

Esta investigación demuestra cómo incluso sistemas embebidos comercialmente maduros pueden fallar a en cuanto al diseño de seguridad hardware. Cuando un atacante obtiene acceso físico, cadenas de arranque incorrectamente diseñadas y mecanismos criptográficos mal utilizados crean oportunidades para extraer firmware, recuperar claves y ejecutar código sin restricciones.

Referencias

Diapositivas de BlackAlps 2025:

Herramientas:

Notificaciones de seguridad relacionadas: