Uno de los mecanismos de anti reversing utilizado por los cibercriminales consiste en la inclusión de código para el Manejo Estructurado de Excepciones. Si bien esta forma de manejar las excepciones en un programa en ejecución es una característica propia de la arquitectura x86, que se traduce de manera natural en lenguajes como C++, los desensambladores actuales suelen quedarse a medio camino en el flujo de ejecución, resultando en la pérdida de información valiosa para el analista.
Las excepciones en un programa pueden ocurrir por diversas razones, ya sea por el intento de acceso a una sección no válida de memoria, por una división por cero, o por excepciones de software lanzadas por el mismo proceso. El Manejo Estructurado de Excepciones (Structured Exception Handling, en inglés) provee una forma de tratar con las excepciones, a través de un conjunto de rutinas que se encuentran encadenadas mediante una lista enlazada.
Así, cada vez que se genera una excepción en un hilo de ejecución, el sistema operativo recupera la dirección inicial de este bloque de rutinas. Luego, se llama a la primera función de manejo de excepciones; si ésta no es capaz de controlar la excepción, devuelve el puntero hacia la próxima rutina. Así, en tanto la excepción no pueda ser manejada, será delegada hacia la siguiente función de la lista. Como es de esperarse, si la excepción llega hasta la última instancia sin ser manejada, el proceso será interrumpido, alertando al usuario. En la siguiente imagen podemos observar la estructura descripta:
Podemos ver que la dirección de la primera rutina se corresponde con el comienzo del segmento de memoria al que apunta el registro FS. También vemos cómo cada registro posee dos campos: un puntero al elemento previo de la lista enlazada y un puntero al handler de la excepción para esa instancia. Luego, cada registro se puede representar mediante la siguiente estructura:
Dado que este mecanismo de manejo de excepciones es independiente para cada hilo de ejecución en un proceso, la lista enlazada va a cambiar su estructura de manera dinámica con cada subrutina que se ejecute. En otras palabras, esta lista de handlers será mantenida en la zona de memoria correspondiente al stack. Por lo tanto, a partir de ahora no vamos a concentrarnos en cómo se implementa el mecanismo mediante llamadas a la API del sistema, sino en cómo los cibercriminales pueden aprovechar el manejo de excepciones para ofuscar el flujo de ejecución.
Para lograr esto, puede disponerse el código de tal modo que se produzca una excepción en un momento determinado. Así, si la estructura en el stack ha sido previamente manipulada para que la primera rutina manejadora contenga código personalizado, cuando se produzca la excepción se ejecutará esa rutina. Luego, durante la ejecución de la rutina encubierta, deberá existir el código necesario para que el stack sea devuelto a su estado anterior. A continuación observamos una porción de código obtenida con IDA Pro, en la cual se ha ofuscado el flujo de control:
Puede verse que en la instrucción correspondiente a la posición 0x401097 se realiza un push con una dirección de memoria. Ésta corresponde a la dirección en que se encuentra la rutina a ser ejecutada; es decir, el segundo campo del registro descripto (dado que el stack crece hacia direcciones más bajas de memoria, primero se inserta el segundo campo). Luego, se hace un push con un valor que apunta hacia el registro previo, que se encontraba en FS:[0]; esto se corresponde con el manejador normal que se hubiera utilizado de no haber intervenido el programador. Por último, se actualiza FS:[0] para que apunte al nuevo registro creado, la cima del stack. En este punto, se ha logrado insertar un manejador falso de excepciones en el stack. Así, cuando ocurra una excepción, se transferirá el control a la rutina deseada. Es importante notar además que IDA no ha reconocido esta rutina encubierta como código ejecutable (esto se observa en la dirección 0x4014C0).
Las siguientes dos instrucciones producen una excepción por división por cero, con lo cual se transfiere el control a 0x4014C0. Si forzamos a que IDA interprete esos bytes de datos como código, se obtiene lo siguiente:
Las primeras cinco instrucciones se encargan de restablecer el stack a su estado anterior. Algo importante a considerar es que, si bien la primera rutina que se ejecutará será aquella que acaba de ser insertada, por temas de implementación interna de este mecanismo, en la práctica encontraremos otro registro creado por el sistema operativo en FS:[0]. Por ello, vemos que el registro que fue insertado es referenciado con [esp+8], no esp, teniendo en cuenta que hay que saltar esos dos campos DWORD del registro creado por el sistema. Por lo tanto, en esas cinco instrucciones se restablece FS:[0] para que apunte al manejador inicial de excepciones, y se limpia lo insertado en el stack; a partir de allí, la subrutina ejecutará su propio código.
La importancia de este método recae en el hecho de que la subrutina ejecutada no ha sido reconocida como código por el desensamblador, y no se muestran referencias cruzadas hacia ese código. A los ojos del desensamblador, esas instrucciones no son referenciadas, y por lo tanto no serán ejecutadas. Es por ello que resulta fundamental que el analista conozca esta técnica al realizar reversing, de tal modo de evitar la incorrecta interpretación del código.
Créditos imagen: ©Tamba52/Wikimedia Commons