Ingeniería inversa – Parte 2 – Ensamblador

El conocimiento de los lenguajes ensambladores es una parte importante de una comprensión profunda de la ingeniería inversa. Damos una introducción a los conceptos básicos más importantes.

Los lenguajes ensambladores son el primer nivel de abstracción del código máquina puro. En este artículo, veremos cómo traducir un ejemplo de código en ensamblador x86. Muchos de los conceptos básicos que aprendemos aquí se aplican de una manera u otra, pero también a otros lenguajes ensambladores, como. B ensamblador ARM. Este conocimiento nos ayuda a obtener una comprensión más profunda de la ingeniería inversa y los artículos posteriores en los que analizamos las herramientas de ingeniería inversa en uso práctico. Sin embargo, el siguiente artículo también se puede entender sin el conocimiento impartido aquí.

Antes de comenzar con el ejemplo, necesitamos algunos fundamentos teóricos.

Opcodes y Mnemotecnia

Como ya se describió en la Parte 1, el código del programa debe traducirse a código máquina, es decir, unos y ceros, para que un procesador pueda procesarlo. Por lo tanto, el procesador está diseñado para realizar una cierta acción en una cierta secuencia de unos y ceros. Un opcode (abreviatura de código de operación) es una de esas secuencias y generalmente se representa en forma hexadecimal.

La mnemotecnia, por otro lado, son abreviaturas textuales para opcodes y, por lo tanto, más fáciles de entender para las personas. «Mov» para «mover» datos es más fácil de recordar que «b0», especialmente porque puede haber varios opcodes que se ajusten a un mnemotécnico.

Registro

Los registros son áreas de memoria a las que el procesador puede acceder muy rápidamente y que se utilizan para almacenar datos en caché al ejecutar programas.

En nuestro ejemplo x86 de 32 bits, encontraremos las siguientes pestañas:

  • EAX, EBX, ECX, EDX: registros para diversos fines. Se utiliza principalmente dentro de las funciones para almacenar en caché valores y direcciones
  • EBP, ESP: Puntero para la pila
  • EIP: Contiene la dirección del siguiente puntero de instrucción

Pila

La pila es una región de almacenamiento contigua en un proceso. Esta área almacena datos temporales, como variables locales y parámetros de función.

Para cada función que se llama, la pila se divide en áreas de memoria más pequeñas, las llamadas tramas. Las variables locales, etc. de la función se almacenan en el marco de pila.

Ejemplo

Ahora vayamos a nuestro ejemplo. Repasamos el código en secciones. Vemos cómo las instrucciones del código fuente de C se han traducido en ensamblador y cómo cambian los registros y la pila.

La ejecución «correcta» de un programa C generalmente comienza con la función principal. Antes de eso, se llaman funciones que preparan y dan seguimiento a la ejecución. Por lo tanto, los valores ya están en la pila al comienzo de nuestra consideración.

La función principal comienza con un prólogo. En esto, primero se realiza una copia de seguridad del límite inferior (EBP)del marco de pila anterior para restaurarlo más tarde. Para hacer esto, EBP se «empuja» a la pila (push ebp). Dado que el valor se ha colocado en la pila, el límite superior (ESP)se ajusta automáticamente. A continuación, se abre el nuevo marco de pila. EBP se establece en el límite superior anterior (ESP) (mov ebp, esp) y ESP se reduce en 16 bytes (sub esp, 0x10). Esto significa que está abierta una trama de 16 bytes, en la que, entre otras cosas, se escriben variables locales.

En nuestro caso, la pila se implementa de tal manera que crece de «arriba a abajo». Esto significa que el borde «inferior» tiene una dirección más alta que el «superior». Esto es típico de las arquitecturas x86. Con otros tipos como. B ARM, también hay implementaciones de pila en las que la pila crece hacia arriba.

En la función principal,los valores se asignan a las dos variables a y b y luego se pasan a la función add_numbers. Podemos ver esto en el código del ensamblador también. En primer lugar, los valores 0x5 y 0xa se almacenan en el marco de pila actual. 0x5 se escribe en la dirección ebp-4, es decir, 0x7FE4. 0xa aterriza en la dirección ebp-8.
Después de eso, los valores del marco de la pila se envían a la pila y, por lo tanto, se pasan a la siguiente función, es decir, add_numbers.

Hay varias convenciones que los compiladores utilizan para pasar argumentos de función. Por lo tanto, también es posible transmitir los argumentos a través de los registros. La variante que se muestra aquí en la que los valores se empujan a la pila es la llamada convención de llamada cdecl.

Ahora viene la instrucción de llamada,que hace dos cosas. Primero, la dirección de memoria de la siguiente instrucción se coloca en la pila para almacenar donde continúa después de la llamada a la función de add_numbers. En segundo lugar, el puntero de instrucción (EIP) está configurado para iniciar la función add_numbers para que la ejecución continúe allí.

La función add_numbers también comienza con un prólogo de función. Al igual que en la función principal,primero se realiza una copia de seguridad del límite inferior del marco de pila anterior y luego se abre un marco de 16 bytes.

Luego viene la preparación e implementación de la adición. Dado que la variable c es también una variable local, el valor se almacena en el marco de pila de la función actual. (mov DWORD PTR [ebp-4], 0x14). Posteriormente, los dos argumentos de función obtenidos se toman del marco de pila anterior (recordatorio: EBP + x es una dirección en el marco de pila anterior a medida que la pila crece hacia abajo) y se almacenan en las pestañas EDX y EAX.

Los dos registros se suman y el resultado se almacena en EDX porque el registro se pasó como el primer operando (add edx, eax). Hasta el momento, se han sumado 5 y 10 con el resultado 15 (0xf).

A continuación, el valor de la dirección ebp-4 se escribe en el registro EAX. Esta es la variable local c y, por lo tanto, el valor 20 (0x14). Luego se agregan EAX y EDX nuevamente. El resultado termina en EAX esta vez, ya que este registro se estableció como el primer operando (add eax, edx).

Entonces comienza el epílogo de la función. Esto degrada el marco de pila y continúa la ejecución en la ubicación anterior. La instrucción leave establece ESP en el valor de EBP (esto cierra la trama de pila) y restablece EBP al límite inferior de la trama de pila anterior. La instrucción retfinalmente modifica el puntero de instrucción para que la siguiente instrucción se ejecute después de la llamada de llamada original. El valor apropiado se establece mediante la instrucción de llamada a la pila.

De vuelta en la función principal, los argumentos de la función pasada se eliminan primero de la pila. Para hacer esto, el ESP se incrementa en 8 (agregue esp, 0x8). Luego, el resultado de la función se escribe add_numbers desde la pestaña EAX al marco de la pila en el lugar ebp-12 (mov DWORD PTR [ebp-12], eax). Esta es la variable local c.

La siguiente instrucción parece absurda, ya que en EAX se escribe el valor que todavía está en EAX. Esto se debe en parte al hecho de que hemos impedido cualquier optimización por parte del compilador. Debido a que queremos devolver el valor de la variable y el valor devuelto suele estar en la pestaña EAX, se escribe de nuevo en el registro.

La función principa ltambién termina con un epílogo. El marco de pila se degrada y la ejecución continúa donde la dejó antes de llamar a la función principal. Dado que estas son funciones que solo controlan la ejecución como se describió anteriormente, nuestro ejemplo termina aquí

Enlace: Ingeniería inversa – Parte 2 – | ensamblador G DATOS (gdata.de) Blog de G DATA Joel Taddey