Reversing 101

La ingeniería inversa , es el proceso por el cual se analiza la salida de un objeto, ya sea, de algún proceso químico, mecánico o informático, de tal forma que podamos entender como funciona el mecanismo por el cual dicha salida es obtenida. En el ámbito de la informática (específicamente de la seguridad), utilizamos la ingeniería inversa (reversing como a mi me gusta denotar), para tareas relacionadas al análisis de malware, ingeniería forense y cracking de software. En el siguiente apartado, explicaré algunas nociones acerca de la ingeniería inversa que deben tomar en cuenta antes de entrar a la que considero la parte mas cruda de la seguridad informática.

Lenguaje Ensamblador
Como lo define Wikipedia, “El lenguaje ensamblador, o assembler (en inglés assembly language y la abreviación asm), es un lenguaje de programación de bajo nivel que consiste en un conjunto de mnemónicos (palabra que sustituye a un código de operación [lenguaje de máquina], con lo cual resulta más fácil la programación) que representan instrucciones básicas para los computadores, microprocesadores, microcontroladores y otros circuitos integrados programables”. Fácil y entendible, aunque de por si, el lenguaje es bastante complicado al principio, ya veremos que existen varias formas de aprenderlo y entender toda la sintaxis.

La mayoría de las CPU tienen más o menos los mismos grupos de instrucciones, aunque no necesariamente tienen todas las instrucciones de cada grupo. Las operaciones que se pueden realizar varían de una CPU a otra. Una CPU particular puede tener instrucciones que no tenga otro y viceversa. Es por ello, que podemos encontrar distintos lenguajes assembly, dependiendo del procesador y la arquitectura en que fue compilado/interpretado nuestro programa.

Algunos ejemplos de assembly que encontramos son:

  • MISP
  • ARM
  • Sparc
  • Intel x86
  • Intel x64

Para lo que es el área de la seguridad informática, nos concentraremos para empezar en el Assembly x86, debido a que es un buen pie para sentar las bases antes de entrar al Intel x64 y porque es el mas utilizado hoy en día.

Nivel de Abstracción

Primero que todo, necesitamos entender como se obtiene el lenguaje ensamblador, el cual es la base de nuestras aplicaciones y el que utilizaremos en cada reto. Como vemos en la figura anterior, tenemos un trozo de código (en este caso, un ejemplo de lenguaje C) con una variable definida y la llamada a dos funciones (printf y exit). Luego que el código es escrito, es el compilador el que se encarga de transformar nuestro código original, en un código legible por la maquina y finalmente , es el proceso de desensamblamiento el cual transforma el cogido de maquina en lenguaje assembly. Este último, es el lenguaje de bajo nivel que explicábamos en la sección anterior y es el que nos permite ver como las instrucciones son ejecutadas en el procesador y almacenadas en memoria.

Para el ultimo paso, es necesario contar con un programa que llamaremos desensamblador, el cual transformará nuestro código maquina en un código legible por nosotros.

Arquitectura x86

Tanto el procesador que utilicemos, como la memoria RAM disponible de nuestras maquinas, formaran un papel importante en el proceso de ejecución de las tareas programadas, por ende, debemos entender el flujo de las instrucciones a nivel de sistema operativo.

Expliquemos cada uno de estos elementos:

  • Cpu: La unidad central de procesamiento (CPU), es el hardware dentro de un ordenador u otros dispositivos programables, que interpreta las instrucciones de un programa informático.
  • Memoria Principal (RAM): Unidad principal de almacenamiento de nuestra información. Esta guarda todo el código e información de nuestros programas.
  • Input/Output Devices: Interfaces con los dispositivos de nuestro ordenador (Discos duros, teclado, monitor, etc)
  • Unidad aritmetica Logica (ALU): Circuito digital que realiza las operaciones lógicas y aritméticas de nuestro código.
  • Registros: Ubicaciones internas de memoria del procesador que nos permiten procesar instrucciones e información.
  • Control Unit: Conexión entre las operaciones que realizamos y la memoria utilizada en estas operaciones.

Básicamente, el flujo de nuestras instrucciones sigue el siguiente patrón: las instrucciones y datos formados por registros, ejecutan operaciones lógicas y aritméticas, las cuales requieren de memoria para almacenar los resultados y referencias a otros datos.

Memoria de un programa

Cada vez que un programa es ejecutado, se debe pedir una porción de la memoria ram para almacenar datos en ella, por lo que con este segmento podemos pedir y liberar memoria sin problemas. Este fragmento de memoria se va a dividir en 4 partes (según la literatura, podrían ser más), las cuales son: Stack, Heap, Code y Data definidas mas abajo

  • Stack: Porción de la memoria definida para variables estáticas de nuestro programa
  • Heap: Porción de memoria indefinida, el programa puede requerir memoria del heap cuanto sea necesario debido a que esta se crea en la ejecución del programa (al mismo tiempo, es propensa a errores de programación, por lo que podemos aprovecharnos de esta característica).
  • Code: Porción de memoria que almacena las instrucciones de la CPU, tambien llamado segmento de texto.
  • Data: Porción de memoria destinada a almacenar variables estáticas y/o globales, también llamado segmento BSS.
  • El Stack ademas, es el que se encargará de llevar la cuenta de la cantidad de bytes que usan cada una de las variables en cada función de nuestro programa (un Stack por función). Expliquemos esto, cuando un programa es inicializado, se debe crear un Stack lo suficientemente grande para almacenar las variables de nuestra función principal, es aquí donde juega un papel importante los registros EBP (Stack Base Pointer) y ESP (Stack Pointer), el cual se fijaran en la parte más alta y mas baja de nuestro stack respectivamente. Notar que las direcciones de memoria, irán decrementando a medida que subamos en el stack e incrementando a medida que bajemos. Las variables se irán posicionando dentro del Stack de una en una, ocupando los espacios de memoria correspondiente y al mismo tiempo, irán saliendo del Stack de la forma FILO (First In Last Out).

    Bajo el Saved EBP (fragmento de memoria destinado para guardar la dirección del puntero EBP), encontramos la dirección de retorno de nuestro programa, la cual nos permite salir de la función en la que nos encontramos actualmente. Si por ejemplo, la función que nos encontramos actualmente fuese la función main, entonces el return address nos conduciría a la finalización de nuestro programa.

    Mas abajo finalmente podemos encontrar la sección de argumentos de cada función. Cuando una función es llamada, es aquí donde se almacenan incialmente los argumentos que se le pasan, para luego ser pasados al nuevo Stack de función que se llama.

    NOTA: Existe un registro que se encarga de almacenar la dirección de memoria de la siguiente instrucción a ejecutar y se llama EIP (Instruccion Pointer). Muchos de los hackers que manejan reversing, se encargan de manipular el EIP para explotar vulnerabilidades en los programas, permitiéndonos así ganar acceso a distintos sistemas. Solo imaginen que tienen el control de este registro y pueden ejecutar el espacio de memoria donde se encuentre alguno de sus programas maliciosos, que resultaría?

    Registros, Flags e Instrucciones

    Dentro del lenguaje assembly Intel X86, encontramos una variedad de registros que nos ayudaran a entender las tareas que realizan los programas. No pretendo explicar cada uno de estos, debido a que no es el objetivo de esta entrada, pero les adjunto un link donde pueden encontrar el uso de cada uno de estos.

    Herramientas

    Algunas herramientas utilizadas en esta área son:

    • IDA
    • GDB
    • Radare2
    • OllyDBG
    • DotPeek

    Dependiendo de lo que necesiten, algunas de estas les serán más útiles que otras. En lo personal, siempre me he manejado utilizando IDA, GDB y Radare2.

    Conclusión

    Ya dimos un primer vistazo a lo que es assembly x86, revisamos como se generaba un código assembly y entendimos como actúa nuestro código a nivel de sistema operativo para realizar las tareas de cada programa, ademas revisamos como son guardados los datos en Memoria para luego ser utilizados por los distintos registros.

    Lo importante de esta entrada es quedarse con la idea de que el reversing no es un mundo de sombras, sino una materia mas que debemos estudiar para ser buenos hackers. Recuerden que esta materia puede ser frustrante al pricipio, pero con un poco de dedicación se puede llegar a grandes cosas.

    Se despide como siempre H4tt0r1.

    Agregar un comentario

    Su dirección de correo no se hará público. Los campos requeridos están marcados *