Captura de pantalla de la interfaz IDA Pro DisassemblerIDA Pro es un famoso desensamblador que ha sido utilizado por investigadores de seguridad de la información en todo el mundo durante muchos años. Nosotros en Positive Technologies también usamos esta herramienta. Además, pudimos desarrollar nuestro propio
módulo procesador de desensamblador para la arquitectura de microprocesador NIOS II , lo que aumenta la velocidad y la conveniencia del análisis de código.
Hoy contaré sobre la historia de este proyecto y mostraré lo que sucedió al final.
Antecedentes
Todo comenzó en 2016, cuando tuvimos que desarrollar nuestro propio módulo de procesador para analizar el firmware en una tarea. El desarrollo se realizó desde cero en la
Guía de referencia del procesador clásico Nios II , que era la más relevante. En total, este trabajo tomó alrededor de dos semanas.
El módulo del procesador fue desarrollado para la versión IDA 6.9. Para la velocidad, se eligió IDA Python. En el lugar donde residen los módulos del procesador, el subdirectorio procs dentro del directorio de instalación de IDA Pro, hay tres módulos Python: msp430, ebc, spu. En ellos, puede ver cómo se organiza el módulo y cómo se puede implementar la funcionalidad básica de desmontaje:
- instrucciones de análisis y operandos,
- su simplificación y exhibición,
- crear compensaciones, referencias cruzadas, así como el código y los datos a los que se refieren
- procesamiento de construcciones de interruptores,
- manejo de manipulaciones con la pila y las variables de la pila.
Aproximadamente dicha funcionalidad se implementó en ese momento. Afortunadamente, la herramienta fue útil en el proceso de trabajar en otra tarea, durante la cual, un año después, fue utilizada y refinada activamente.
Decidí compartir la experiencia de crear el módulo procesador con la comunidad en los PHDays 8. La presentación despertó interés (el informe del video fue
publicado en el sitio web de PHDays), incluso el creador de IDA Pro Ilfak Gilfanov estuvo presente. Una de sus preguntas fue si se implementó el soporte para IDA Pro versión 7. En ese momento no estaba allí, pero después del rendimiento, prometí hacer un lanzamiento apropiado del módulo. Aquí es donde comenzó la diversión.
Ahora el último
manual de Intel , que se utilizó para verificar y verificar errores. Revisé significativamente el módulo, agregué una serie de nuevas características, incluida la resolución de aquellos problemas que antes no se podían vencer. Bueno, por supuesto, agregué soporte para la séptima versión de IDA Pro. Esto es lo que pasó.
Modelo de software NIOS II
NIOS II es un procesador de software desarrollado para Altera FPGA (ahora parte de Intel). Desde el punto de vista de los programas, tiene las siguientes características: orden de bytes de little endian, espacio de direcciones de 32 bits, conjunto de instrucciones de 32 bits, es decir, 4 bytes, 32 registros generales y 32 de propósito especial se utilizan para codificar cada comando.
Desmontaje y referencias de código
Entonces, abrimos un nuevo archivo en IDA Pro, con firmware para el procesador NIOS II. Después de instalar el módulo, lo veremos en la lista de procesadores IDA Pro. La elección del procesador se muestra en la figura.

Supongamos que el módulo aún no ha implementado ni siquiera un análisis básico de comandos. Dado que cada comando toma 4 bytes, agrupamos los bytes en cuatro, luego todo se verá más o menos así.

Después de implementar la funcionalidad básica de decodificar instrucciones y operandos, mostrarlos en la pantalla y analizar las instrucciones de transferencia de control, el conjunto de bytes del ejemplo anterior se convierte al siguiente código.

Como se puede ver en el ejemplo, las referencias cruzadas también se generan a partir de comandos de transferencia de control (en este caso, puede ver el salto condicional y la llamada al procedimiento).
Una de las propiedades útiles que se pueden implementar en los módulos del procesador son los comentarios de comandos. Si deshabilita la salida de valores de byte y habilita la salida de comentarios, la misma sección de código ya se verá así.

Aquí, si encontró por primera vez el código de ensamblador de una nueva arquitectura para usted, utilizando comentarios puede comprender lo que está sucediendo. Además, los ejemplos de código estarán en la misma forma, con comentarios, para no mirar el manual de NIOS II, sino para comprender de inmediato lo que está sucediendo en la sección de código, que se da como ejemplo.
Pseudoinstrucciones y simplificación de comandos.
Algunos comandos de NIOS II son pseudo instrucciones. No hay códigos de operación separados para tales equipos, y ellos mismos están modelados como casos especiales de otros equipos. En el proceso de desmontaje, se realiza la simplificación de las instrucciones: el reemplazo de ciertas combinaciones con pseudoinstrucciones. Las pseudoinstrucciones en NIOS II generalmente se pueden dividir en cuatro tipos:
- cuando una de las fuentes es cero (r0) y puede eliminarse de la consideración,
- cuando el equipo tiene un valor negativo y el equipo es reemplazado por el opuesto,
- cuando la condición se invierte,
- cuando el desplazamiento de 32 bits se ingresa en dos equipos en partes (el más joven y el más viejo) y esto se reemplaza por un comando.
Se implementaron los dos primeros tipos, ya que reemplazar la condición no da nada especial, y las compensaciones de 32 bits tienen más opciones de las que se presentan en el manual.
Por ejemplo, para la primera vista, considere el código.

Se ve que el uso del registro cero en los cálculos a menudo se encuentra aquí. Si observa detenidamente este ejemplo, notará que todos los comandos, excepto la transferencia de control, son opciones para simplemente ingresar valores en registros específicos.
Después de implementar el procesamiento de pseudo instrucciones, obtenemos la misma sección de código, pero ahora parece más legible, y en lugar de variaciones de los comandos o y add, obtenemos variaciones del comando mov.

Variables de la pila
La arquitectura NIOS II es compatible con la pila, y además del puntero de pila sp, también hay un puntero al marco de pila fp. Considere un ejemplo de un pequeño procedimiento que usa una pila.

Obviamente, el espacio está reservado para las variables locales en la pila. Se puede suponer que el registro ra se almacena en la variable de pila y luego se restaura a partir de él.
Después de agregar funcionalidad al módulo que rastrea los cambios en el puntero de la pila y crea variables de pila, el mismo ejemplo se verá así.

Ahora el código se ve un poco más claro, y ya puede nombrar las variables de la pila y analizar su propósito siguiendo las referencias cruzadas. La función en el ejemplo es del tipo __fastcall y sus argumentos en los registros r4 y r5 se insertan en la pila para llamar a un subprocedimiento que es del tipo _stdcall.
Números y compensaciones de 32 bits
La peculiaridad de NIOS II es que en una operación, es decir, al ejecutar un solo comando, es posible registrar como máximo un valor directo de 2 bytes (16 bits) de tamaño. Por otro lado, los registros del procesador y el espacio de direcciones son de 32 bits, es decir, para el direccionamiento, se deben ingresar 4 bytes en el registro.
Para resolver este problema, se utilizan desplazamientos de dos partes. Se usa un mecanismo similar en los procesadores de PowerPC: el desplazamiento consta de dos partes, la más antigua y la más joven, y se ingresa en el registro mediante dos comandos. En PowerPC, esto es lo siguiente.

En este enfoque, los enlaces cruzados se forman a partir de ambos equipos, aunque de hecho, la dirección se configura en el segundo comando. Esto a veces puede ser una molestia al contar el número de referencias cruzadas.
Las propiedades de desplazamiento para la parte más antigua usan el tipo no estándar HIGHA16, a veces se usa el tipo HIGH16, para la parte más joven - LOW16.

No hay nada complicado en el cálculo de números de dos partes de 32 bits. Las dificultades surgen en la formación de operandos como compensaciones para dos equipos separados. Todo este procesamiento recae en el módulo del procesador. No hay ejemplos de cómo implementar esto (especialmente en Python) en el SDK de IDA.
En el informe sobre PHDays, los sesgos se erigieron como un problema no resuelto. Para resolver el problema, hicimos trampa: compensación de 32 bits solo desde la parte más joven, en la base. La base se calcula como la parte más antigua, desplazada a la izquierda 16 bits.

Con este enfoque, se forma una referencia cruzada solo con el comando para ingresar la parte inferior del desplazamiento de 32 bits.
La base es visible en las propiedades de desplazamiento y la propiedad está marcada para considerarla como un número, de modo que no se forma una gran cantidad de referencias cruzadas a la dirección en sí, que tomamos como base.

En el código para NIOS II, se encuentra el siguiente mecanismo para ingresar números de 32 bits en el registro. Primero, la parte más antigua del desplazamiento se ingresa en el registro con el comando movhi. Entonces la parte más joven se une. Esto se puede hacer de tres maneras (mediante comandos): agregando addi, restando subi, lógico OR ori.
Por ejemplo, en la siguiente sección del código, los registros se establecen en números de 32 bits, que luego se ingresan en registros, argumentos antes de llamar a la función.

Después de agregar el cálculo de desplazamiento, obtenemos la siguiente representación de este bloque de código.

El desplazamiento de 32 bits resultante se muestra junto al comando para ingresar su parte inferior. Este ejemplo es bastante ilustrativo, e incluso podríamos calcular fácilmente todos los números de 32 bits en la mente simplemente agregando las partes menores y más altas. A juzgar por los valores, lo más probable es que no sean sesgos.
Considere el caso cuando se usa la resta al ingresar a la parte más joven. En este ejemplo, no será posible determinar los números finales de 32 bits (compensaciones) en el movimiento.

Después de aplicar el cálculo de números de 32 bits, obtenemos el siguiente formulario.

Aquí vemos que ahora, si la dirección está en el espacio de direcciones, se forma un desplazamiento en ella, y el valor que se formó como resultado de la conexión de las partes menores y mayores ya no se muestra cerca. Aquí obtuvieron un desplazamiento por la línea "22/10/08". Para que el resto de las compensaciones apunten a direcciones válidas, aumentemos un poco el segmento.

Después de aumentar el segmento, obtenemos que ahora todos los números calculados de 32 bits son compensaciones e indican direcciones válidas.
Se mencionó anteriormente que hay otra opción para calcular las compensaciones cuando se usa un comando lógico OR. Aquí hay un código de ejemplo donde se calculan dos compensaciones de esta manera.

El que se evalúa en el registro r8 se empuja a la pila.
Después de la conversión, está claro que en este caso los registros se configuran a las direcciones del comienzo de los procedimientos, es decir, la dirección del procedimiento se inserta en la pila.

Leer y escribir en relación con la base
Antes de eso, consideramos los casos en que un número de 32 bits ingresado usando dos comandos podría ser solo un número y también un desplazamiento. En el siguiente ejemplo, la base se ingresa en la parte superior del registro, luego se lee o escribe en relación con ella.

Después de procesar tales situaciones, obtenemos el desplazamiento de las variables de los comandos de lectura y escritura. Además, dependiendo de la dimensión de la operación, se establece el tamaño de la variable en sí.

Cambiar construcciones
Las construcciones de conmutadores que se encuentran en los archivos binarios pueden facilitar el análisis. Por ejemplo, por el número de casos de selección dentro de la construcción del conmutador, puede localizar el conmutador responsable de procesar un determinado protocolo o sistema de comando. Por lo tanto, surge la tarea de reconocer el interruptor en sí y sus parámetros. Considere la siguiente sección de código.

El hilo de ejecución se detiene en la transición de registro jmp r2. Además, hay bloques de código a los que hay enlaces desde los datos, y al final de cada bloque hay un salto a la misma etiqueta. Obviamente, esta es una construcción de interruptor y estos bloques individuales manejan casos específicos de ella. Arriba también puede ver la verificación del número de casos y el salto predeterminado.
Después de agregar el procesamiento del interruptor, este código se verá así.

Ahora se indica el salto en sí, la dirección de la tabla con desplazamientos, el número de casos, así como cada caso con el número correspondiente.
La tabla en sí con compensaciones a las opciones es la siguiente. Para ahorrar espacio, se dan los primeros cinco elementos.

De hecho, el procesamiento del conmutador consiste en volver a través del código y buscar todos sus componentes. Es decir, se describe un esquema de organización de cambio. A veces puede haber excepciones en los esquemas. Esta puede ser la razón de los casos en que los interruptores aparentemente claros no se reconocen en los módulos de procesador existentes. Resulta que el interruptor real simplemente no cae dentro del esquema que se define dentro del módulo del procesador. Todavía hay opciones posibles cuando el circuito parece estar allí, pero hay otros equipos dentro de él que no están involucrados en el circuito, o los equipos principales se reorganizan o se interrumpe por las transiciones.
El módulo procesador NIOS II reconoce un interruptor con instrucciones "extrañas" entre los comandos principales, así como con los lugares reorganizados de los comandos principales y con interrupciones que interrumpen el circuito. Se utiliza una ruta de retorno a lo largo de la ruta de ejecución, teniendo en cuenta las posibles transiciones que rompen el circuito, con la instalación de variables internas que señalan diferentes estados del reconocedor. Como resultado, se reconocen alrededor de 10 opciones de organización de conmutadores diferentes que se encuentran en el firmware.
Instrucción personalizada
Hay una característica interesante en la arquitectura NIOS II: la instrucción personalizada. Da acceso a 256 instrucciones definidas por el usuario que son posibles en la arquitectura NIOS II. En su trabajo, además de los registros de propósito general, la instrucción personalizada puede acceder a un conjunto especial de 32 registros personalizados. Después de implementar la lógica para analizar el comando personalizado, obtenemos el siguiente formulario.

Puede notar que las dos últimas instrucciones tienen el mismo número de instrucción y parecen realizar las mismas acciones.
De acuerdo con las instrucciones personalizadas, hay un
manual por separado . Según él, una de las opciones más completas y actualizadas para el conjunto de instrucciones personalizadas es el conjunto de instrucciones NIOS II Floating Point Hardware 2 Component (FPH2) para trabajar con el punto flotante. Después de implementar el análisis de los comandos FPH2, el ejemplo se verá así.

De la mnemotecnia de los dos últimos equipos, nos aseguramos de que realmente realicen la misma acción: el comando fadds.
Transiciones por valor de registro
En el firmware bajo investigación, a menudo se encuentra una situación cuando se realiza un salto de acuerdo con el valor del registro, en el que se ingresa antes un desplazamiento de 32 bits, que determina el lugar del salto.
Considere una pieza de código.

En la última línea hay un salto en el valor del registro, mientras que está claro que antes de ingresar la dirección del procedimiento en el registro, que comienza en la primera línea del ejemplo. En este caso, es obvio que el salto se realiza desde el principio.
Después de agregar la funcionalidad de reconocimiento de saltos, se obtiene el siguiente formulario.

Junto al comando jmp r8, se muestra la dirección donde se produce el salto si fuera posible calcular. También se forma una referencia cruzada entre el equipo y la dirección donde tiene lugar el salto. En este caso, el enlace es visible en la primera línea, el salto en sí se realiza desde la última línea.
Valor de registro de gp (puntero global), guardar y cargar
Es común usar un puntero global que está configurado para alguna dirección, y las variables se direccionan en relación con él. NIOS II utiliza el registro gp (puntero global) para almacenar el puntero global. En algún momento, como regla, en los procedimientos de inicialización del firmware, el valor de la dirección se ingresa en el registro gp. El módulo procesador maneja esta situación; Para ilustrar esto, los siguientes son ejemplos de código y la ventana de salida de IDA Pro cuando los mensajes de depuración están habilitados en el módulo del procesador.
En este ejemplo, el módulo procesador encuentra y calcula el valor del registro gp en la nueva base de datos. Al cerrar la base de datos idb, el valor de gp se almacena en la base de datos.

Al cargar una base de datos idb existente y si el valor de gp ya se ha encontrado, se carga desde la base de datos, como se muestra en el mensaje de depuración en el siguiente ejemplo.

Leer y escribir sobre gp
Las operaciones comunes son leer y escribir con un desplazamiento relativo al registro gp. Por ejemplo, en el siguiente ejemplo, se realizan tres lecturas y un registro de este tipo.

Como ya obtuvimos el valor de la dirección que está almacenada en el registro gp, podemos abordar este tipo de lectura y escritura.
Después de agregar el procesamiento para situaciones de lectura y escritura en relación con el registro gp, obtenemos una imagen más conveniente.

Aquí puede ver a qué variables se está accediendo, realizar un seguimiento de su uso e identificar su propósito.
Direccionamiento relativo a gp
Hay otro uso del registro gp para direccionar variables.

Por ejemplo, aquí vemos que los registros están configurados en relación con el registro gp para algunas variables o áreas de datos.
Después de agregar funcionalidad que reconoce tales situaciones, se convierte en compensaciones y agrega referencias cruzadas, obtenemos el siguiente formulario.

Aquí ya puede ver qué áreas relativas a los registros gp están configuradas, y queda más claro lo que está sucediendo.
Direccionamiento relativo a sp
De manera similar, en el siguiente ejemplo, los registros están sintonizados en algunas áreas de memoria, esta vez en relación con el puntero de registro a la pila.

Obviamente, los registros están ajustados a algunas variables locales. Tales situaciones (establecer argumentos en memorias intermedias locales antes de las llamadas a procedimientos) son bastante comunes.
Después de agregar el procesamiento (convertir valores directos en compensaciones), obtenemos el siguiente formulario.

Ahora queda claro que después de la llamada al procedimiento, los valores se cargan desde aquellas variables cuyas direcciones se pasaron como parámetros antes de la llamada a la función.
Referencias cruzadas del código a los campos de estructura
Definir estructuras y usarlas en IDA Pro puede facilitar el análisis de código.

Al observar esta parte del código, podemos entender que el campo field_8 se está incrementando y, posiblemente, es un contador de la ocurrencia de un evento. Si los campos de lectura y escritura están separados en el código a una gran distancia, las referencias cruzadas pueden ayudar.
Considere la estructura misma.
Aunque el acceso a los campos de estructuras es, como vemos, no hay referencias cruzadas del código a los elementos de las estructuras.Después de que se procesen tales situaciones, para nuestro caso todo se verá de la siguiente manera.
Ahora hay referencias cruzadas para estructurar campos de equipos específicos que trabajan con estos campos. Se crean referencias cruzadas hacia adelante y hacia atrás, y puede realizar un seguimiento mediante diversos procedimientos donde se leen los valores de los campos de estructura y dónde se ingresan.Discrepancias entre manual y realidad
En el manual, al decodificar algunos comandos, ciertos bits deben tomar valores estrictamente definidos. Por ejemplo, para un comando de retorno de una excepción eret, los bits 22–26 deben ser 0x1E.
Aquí hay un ejemplo de este comando de un firmware.
Al abrir otro firmware en un lugar con un contexto similar, nos encontramos con una situación diferente.
Estos bytes no se convirtieron automáticamente en un comando, aunque hay un procesamiento de todos los comandos. A juzgar por el entorno, e incluso una dirección similar, este debería ser el mismo equipo. Miremos cuidadosamente los bytes. Este es el mismo comando eret, con la excepción de que los bits 22–26 no son iguales a 0x1E, sino iguales a cero.Tenemos que arreglar un poco el análisis de este comando. Ahora no corresponde exactamente al manual, pero corresponde a la realidad.
Soporte IDA 7
Comenzando con IDA 7.0, la API proporcionada por Python IDA para scripts regulares ha cambiado bastante. En cuanto a los módulos de procesador, los cambios son colosales. A pesar de esto, el módulo del procesador NIOS II pudo rehacerse para la versión 7, y funcionó con éxito en él.
El único momento incomprensible: cuando se carga un nuevo archivo binario bajo NIOS II en IDA 7, el análisis automático inicial que está presente en IDA 6.9 no ocurre.Conclusión
Además de la funcionalidad básica de desmontaje, cuyos ejemplos se encuentran en el SDK, el módulo del procesador implementa muchas características diferentes que facilitan el trabajo del explorador de código. Está claro que todo esto se puede hacer manualmente, pero, por ejemplo, cuando hay miles y decenas de miles de compensaciones de diferentes tipos en un archivo binario con firmware de un par de megabytes, ¿por qué dedicar tiempo a esto? Deje que el módulo procesador haga esto por nosotros. Después de todo, ¿cómo son las características agradables de la navegación rápida a través del código estudiado usando referencias cruzadas! Esto hace de IDA una herramienta tan conveniente y agradable como la conocemos.Publicado por Anton Dorfman, Tecnologías positivas