Cómo utilizar ingeniería inversa dinámica para dispositivos integrados

Blog

HogarHogar / Blog / Cómo utilizar ingeniería inversa dinámica para dispositivos integrados

May 21, 2024

Cómo utilizar ingeniería inversa dinámica para dispositivos integrados

La proliferación de IoT ha ido acompañada de una proliferación de vulnerabilidades de seguridad. Si no se controlan, los atacantes maliciosos pueden utilizar estas debilidades para infiltrarse en los sistemas de las organizaciones. Regular

La proliferación de IoT ha ido acompañada de una proliferación de vulnerabilidades de seguridad. Si no se controlan, los atacantes maliciosos pueden utilizar estas debilidades para infiltrarse en los sistemas de las organizaciones.

Las pruebas de penetración periódicas, reconocidas desde hace mucho tiempo como una de las mejores prácticas de seguridad, ayudan a los equipos de seguridad a identificar y mitigar vulnerabilidades y debilidades en los dispositivos integrados. Sin embargo, muchas organizaciones limitan las pruebas de penetración a investigar redes e infraestructura: los dispositivos de IoT a menudo se pasan por alto.

Para que los equipos de seguridad se pongan al día con las pruebas de penetración de dispositivos integrados, Jean-Georges Valle, vicepresidente senior de Kroll, una consultora de servicios financieros y riesgos cibernéticos, escribió Practical Hardware Pentesting: Aprenda técnicas de ataque y defensa para sistemas integrados en IoT y otros dispositivos. .

En el siguiente extracto del Capítulo 10, Valle detalla cómo los probadores de penetración pueden utilizar ingeniería inversa dinámica para ver cómo se comporta el código durante la ejecución en dispositivos integrados. Valle proporciona un ejemplo de ingeniería inversa dinámica para mostrar a los probadores los desafíos que pueden surgir al observar cómo se comporta el código.

Lea una entrevista con Valle sobre las pruebas de penetración integradas, incluidos los pasos de prueba comunes que utiliza, las dificultades de las pruebas de penetración integradas y su opinión sobre qué tan bien las organizaciones protegen hoy los dispositivos integrados.

Nota del editor: el siguiente extracto es de una versión de acceso temprano de Practical Hardware Pentesting, segunda edición y está sujeto a cambios.

He preparado una variante del ejemplo anterior que nos planteará algunos desafíos. Te mostraré cómo superar estos desafíos tanto de forma estática como dinámica para que puedas comparar la cantidad de esfuerzo necesario en ambos casos.

La regla general al comparar enfoques dinámicos y estáticos es que el 99% de las veces, los enfoques dinámicos son más fáciles y se les debe dar prioridad si es posible (no olvide que es posible que no pueda acceder a JTAG/SWD u otros protocolos de depuración en chip).

En esta sección, también aprenderemos cómo romper donde queramos, inspeccionar la memoria con GDB y ¡todas esas cosas buenas!

El programa de destino se encuentra aquí en la carpeta que clonaste, en la carpeta ch12.

Primero, comencemos cargándolo en Ghidra e inspeccionándolo superficialmente. Preste atención a configurar la arquitectura y la dirección base correctas en la ventana de carga de Ghidra (consulte el capítulo anterior si no recuerda cómo hacerlo o el valor de la dirección base).

A primera vista, la función principal parece muy similar a la función principal del capítulo anterior. Podemos encontrar la referencia a la función principal buscando una cadena de CONTRASEÑA como en el capítulo anterior y analizar su estructura.

Te dejaré trabajar en las habilidades que adquiriste en el capítulo anterior para encontrar las diferentes funciones. En este ejecutable, encontrará nuevamente lo siguiente:

La similitud de la estructura es intencional ya que es tu primera vez. Si tuviera que repetir exactamente los mismos pasos que en el capítulo anterior, no te daría nada nuevo que aprender, ¿verdad?

Ahora, veamos varios métodos para omitir esta validación de contraseña mediante la interacción dinámica con el sistema. Iremos de lo más complejo a lo más simple para mantenerte enfocado y adquiriendo conocimientos (si eres como yo, si hay una manera fácil de evitar algo, ¿por qué ir por el camino difícil?).

Lo primero que vamos a hacer es intentar ver cómo se valida la contraseña para entender cómo generar una contraseña que pase las pruebas.

Echemos un vistazo al código C equivalente a la función de validación generado por Ghidra:

Humm... esto no hace nada directamente con los parámetros. Esto consiste en copiar el contenido de una matriz estática de bytes de 0x47 (71) de longitud a la RAM (y NO a ella) y luego llamarla como una función.

Esto es extraño.

¿O es eso?

Esta es una técnica muy común para camuflar código (por supuesto, una versión muy simple de la misma). Si no hay una versión clara del código de operación en el archivo .bin (y, por lo tanto, no en la memoria flash de la MCU), ¡una herramienta de ingeniería inversa como Ghidra no puede detectar que se trata de código! Aquí tenemos dos enfoques posibles:

Te dejaré la primera solución para que la implementes como ejercicio. ¡Deberían necesitarse más o menos 10 líneas de código Python o C para una tarea tan sencilla! ¿Quieres ser un hacker? ¡Hackear!

¿A mí? Soy un tipo vago. Si una computadora puede funcionar para mí, bueno... ¡Que así sea! Iré por la segunda solución.

Primero, iniciemos una sesión de pantalla en una terminal para que podamos ingresar contraseñas y ver cómo reacciona:

Iniciemos OpenOCD y GDB en una segunda terminal, como hicimos al principio del capítulo, y husmeemos:

Y... ¡y maldita sea! ¡No me devuelve el control! No hay problema si eso te sucede a ti: un poco de Ctrl + C te devolverá el control de inmediato:

Después de Ctrl + C (^c), gdb nos dice que la ejecución se detiene en la dirección 0x080003aa en una función desconocida (??).

Dependiendo de su estado específico, puede realizar la interrupción en otra dirección.

No entres en pánico: ponte tu sombrero para pensar y lleva tu toalla contigo (siempre).

Esto no es un problema. Lo más probable es que se rompa muy cerca de esta dirección, ya que está en el bucle de espera en el que el LED parpadea, esperando que se reciba una contraseña en la interfaz serie.

Primero lo primero, echemos un vistazo a nuestros registros:

Vemos que la PC está realmente donde se supone que debe estar, todo se ve bien y excelente. Entonces, ahora intentemos ingresar una contraseña.

Y... ¡nada funciona en la ventana de la interfaz serie! Pensando en eso... GDB en realidad está bloqueando la ejecución del código; la interfaz serial no reaccionará a sus entradas. Esto es normal.

Entonces, permitamos que continúe (continuar o c en la ventana de gdb) y veamos si la serie funciona ahora. Sí, lo hace. Vamos a romperlo de nuevo y poner un punto de interrupción en la dirección de la función de validación de contraseña, ¿de acuerdo?

En Ghidra podemos ver que la dirección de la primera instrucción de la función es 0x080002b0:

Pongamos un punto de interrupción allí, dejemos que gdb reanude la ejecución e ingresemos una contraseña ficticia:

Analicemos eso:

Bien, ¿ahora qué podemos hacer con eso?

Lo primero es lo primero, si recuerda el código de la función de validación, sus argumentos se pasaron directamente al código decodificado. Echemos un vistazo a cuáles pueden ser (recuerde la convención de llamada para funciones: los argumentos están en r0-3):

El primer argumento es algo en la RAM y el segundo es algún tipo de valor. (Este es el valor UUID transformado para su chip, que anotó, ¿verdad?)

Ahora bien, ¿qué se almacena en esta primera dirección? Examinémoslo:

¡Ah! ¡Ah! ¡Ah! (¿Ves lo que hice allí?) Esta es nuestra contraseña. Tenga en cuenta el uso del modificador de formato para el comando x.

Entonces esto es lo que se espera.

Ahora veamos el código descifrado.

Ghidra nos dice que la instrucción que sigue a los bucles de decodificación está en 0x080002f0. Rompamos ahí:

Entonces, la dirección del código descifrado está en r3. Vimos que el búfer tenía una longitud de 0x47 (71). Estamos en modo pulgar (por lo tanto, instrucciones de tamaño 2). Esto debería ser 47/2: aproximadamente 35 instrucciones. El último bit de la dirección es para el modo; podemos deshacernos de eso:

¡Eso es más parecido! Vemos un preludio de función normal (guardar registros intrafunción en la pila), algo de procesamiento y un retorno de función. Pero GDB nos advierte sobre parámetros de instrucción ilegales (0x2000016c).

Al mirar la lista, vemos que GDB indica el uso de datos relativos a una PC:

Esto se utiliza muy a menudo para almacenar datos en un programa ensamblador. adr es una pseudoinstrucción que le dice al ensamblador que agregue el desplazamiento a una etiqueta (una posición con nombre) en el código.

Veamos lo que se almacena allí:

De hecho, esta es una cadena que se utiliza de alguna manera en el proceso.

Repasemos las primeras instrucciones, como ejemplo de cómo seguir un flujo de ejecución. Primero configuraremos gdb para que nos muestre los registros interesantes y el contenido de cada paso:

Ahora estamos listos para usar stepi (instrucción paso a paso) para ver qué está pasando:

Esto pone a cero r4, r3 y r5 (x^x = 0):

Esto carga el primer carácter de la cadena de contraseña en r5 (r1 es la dirección y r4 se pone a cero en este punto) y lo copia en r8 y r6:

Esto desplaza r6 4 bits hacia la derecha, r5 4 bits hacia la izquierda y coloca su valor OR en r4. Luego enmascara el resultado OR con 0xff, básicamente intercambiando los 4 bits inferiores y 4 superiores del carácter de contraseña y limpiando los bits sobrantes.

Esto mueve 15 en r6, copia r4 en r8 y r7, y enmascara r7 con 15. ¿Pero por qué? ¡En este punto, r4 es 0! Esto puede usarse más adelante, ya que vimos que r4 se usó como compensación en la carga del carácter de contraseña, ¡r4 probablemente sea un contador! Si ese es el caso, este enmascaramiento se puede usar como una especie de módulo... (es muy común usar enmascaramiento para módulo una potencia de dos -1):

¡Esto carga el primer carácter de la cadena que estaba oculta en r6 y usa r7 y un desplazamiento! r4 es definitivamente un contador aquí y r7 una versión modulada del mismo. Esta es una forma de programación muy típica de abordar esto:

Esto es XOR el valor del carácter de contraseña de intercambio de bits con los rangos actuales de la cadena extraña, agregando esto a r0 e incrementando el contador r4:

Esto carga un nuevo carácter de contraseña con el nuevo r5 de compensación. r3 es 0, por lo que el cmp verifica r5-r3 y... Espera... ¿bgt.n? ¿Qué es eso? ¿Recuerdas qué hacer cuando tienes dudas? Vaya a leer la documentación aquí: https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/condition-codes-1-condition-flags-and-codes.

Entonces salta si r5 > r3. Y r3 es 0, ¿entonces? ¡Esto es una prueba para una cadena terminada en 0!

¡Este es el bucle lógico de validación principal!

Una vez hecho esto, hace esto:

Realiza XOR esta suma con el UUID dependiendo del valor que calculó, restaura los valores del registro de llamadas y devuelve este valor. Luego, el código C verifica si este valor es nulo para mostrar realmente la cadena ganadora. Luego solo necesitamos organizarlo para que nuestra suma sea igual al valor dependiente del UUID para que el XOR sea nulo.

¡Tenemos toda la lógica!