lunes, 30 de abril de 2012

From ShellCode to Binary (NcN 2011)

Como ya comenté en un anterior post, el pasado mes de Septiembre participé como ponente en las Conferencias No cON Name en Barcelona, con una charla titulada "Debugging (Exploit) Payloads" que trataba sobre como podemos depurar lo que hace el payload/shellcode de un exploit para investigar posibles problemas.

Entre las cosas que comenté, había una slide que mostraba una posible manera de depurar un shellcode extraído con un exploit en un debugger tradicional (ollydbg, por ejemplo), y que suscitó algunas preguntas y comentarios tras la charla, así que he pensado que merecía la pena verlo con algo más de detalle. La slide en cuestión fue la siguiente:


Describiendo un poco el código, en primer lugar declaramos una variable estática llamada "shellcode" que es de tipo unsigned char, y sobre ella pegamos el contenido del shellcode que queremos depurar, extraído del exploit. A este contenido le ponemos delante y detrás un byte con valor 0xCC que en arquitectura x86 es el opcode de la instrucción "INT 3".

La interrupción 3 (INT 3) es lo que emplean los debuggers para detener la ejecución de un código en un punto (poner un breakpoint). Concretamente lo que hacen es guardarse el contenido original de la instrucción y sustituirla por el INT 3. Cuando la ejecución de ese código llega a una INT 3, esta se detiene y devuelve el control al debugger. Poniendo estos INT 3's "artificiales" en el shellcode lo que conseguimos es que la ejecución se pare en el inicio del shellcode para poder empezar a depurarlo. Ahora solo nos faltará hacer que la ejecución llegue hasta dicho shellcode, pero para eso está el código que podemos ver en el main:

int (*func)();
func = (int (*)) shellcode;
(int)(*func)();

Todos nosotros conocemos de sobra como declarar una función en C, y también como se utilizan los punteros (el puntero, el puntero a puntero, y el puntero a puntero de punteros que apuntan a punteros, que recuerdos de la universidad...). Lo que no es tan habitual es declarar un puntero a una función, al menos en una programación "habitual", pero resulta extremadamente útil para este caso.

Con la primera instrucción estamos declarando un puntero a función, es decir, estamos declarando una función pero aún no le estamos diciendo en que zona está el código de dicha función.

Con la segunda instrucción estamos asignando la dirección de la variable shellcode al puntero a la función, o lo que es lo mismo, si ahora llamamos a la función se ejecutará el contenido de la variable shellcode.

Por último, por ser un puntero a función, no se llama exactamente igual que una función normal, pero puede ser llamada como vemos en la tercera instrucción, de una forma bastante intuitiva si recordamos como se accedía al valor señalado por un puntero.

Con esto conseguimos un programa que lo único que hace es saltar la ejecución al shellcode, que hemos dejado adecuadamente preparado con un INT 3 delante para poder compilar este programa y lanzarlo con un debugger, y que se nos quede la ejecución parada al principio del shellcode, listo para depurar paso a paso o como queramos.

Una pregunta que surgió a varias personas sobre todo esto es ¿eso funciona?
La duda tiene fundamento, ya que en los sistemas modernos existen protecciones que impiden que el flujo de ejecución salte a algunas zonas de memoria, como por ejemplo la pila. Por ejemplo, en Windows, esta protección se llama DEP (Data Execution Prevention).

La respuesta era SÍ, funciona correctamente, por dos motivos:

  1. Las pruebas las hice con un Windows XP SP3 que, aunque ya incorpora tecnología DEP, ésta está activada en la modalidad OptIn (igual que en Windows 7, si no recuerdo mal), con lo que la protección solo se aplica a procesos de sistema y algunos servicios "suscritos" a ella, con lo que un programita hecho y compilado por nosotros no va a tener, a priori, esta protección.
  2. Aunque intentáramos depurar el payload en un sistema con otra opción por defecto diferente a OptIn, la variable a la que salta la ejecución (shellcode) es una variable global, y como tal se almacena en una zona de la memoria del proceso llamada rodata (read-only data), que está fuera de la pila y por lo tanto no le deberían aplicar este tipo de protecciones (aunque reconozco que no lo he probado).



¿Cómo lo hacéis vosotros? ¿ puntualización Alguna o mejora? Deja tu comentario :)

7 comentarios :

Javier Aguinaga dijo...

por mas que el codigo este en la seccion .data, cuando trates de saltar a esa zona se va a producir una violacion de acceso 0xC0000005 si DEP esta activado.

Lo que tendrias que hacer es incluir windows.h y hacer una llamada a VirtualAlloc para darle permisos de ejecucion a la zona.

Ahi aunque tenga un AlwaysOn para el DEP, va a trabajar igual sin errores.

Jose Selvi dijo...

@Javier Aguinaga: ¿Si está en .data también? Como comento en el post, no lo he probado, siempre he depurado con un Windows XP o 7. Luego lo probaré, ya por curiosidad :)

La manera en la que comenta Javier (VirtualAlloc) es la que suelen emplear los exploits. Aquí lo intenté hacer de esta otra manera porque me parecía más sencillo y visual.

Otra opción, como eres tú el que controlas la máquina en la que estás haciendo debug, es que cambies tu mismo la modalidad a OptIn, o incluso a AlwaysOff. Así tampoco daría problemas.

Javier Aguinaga dijo...

@Jose: si DEP esta activado tampoco te deja ejecutar ninguna instruccion en .data

FE DE ERRATAS: La funcion para darle permisos de ejecucion es VirtualProtect no VirtualAlloc.

Jose Selvi dijo...

@Javier: Tendrás que usar las dos: VirtualAlloc para reservar la zona de memoria y VirtualProtect para decir que esa zona de memoria tiene permiso de ejecución.

No es algo que haga todos los días, pero creo recordar que era así.

Javier Aguinaga dijo...

@Jose, no es necesario allocar espacio porque la shellcode ya esta en memoria, solo faltaria darle permisos de ejecucion para que DEP no se queje.

Por lo tanto la parte de solicitar una zona de memoria con VirtualAlloc lo podriamos saltar. En caso de usar VirtualAlloc por alguna cirscuntancia rara, lo que podriamos hacer es darle permisos de ejecucion directamente con el cuarto parametro y evitar usar VirtualProtect, pero si la zona ya esta cargada como en el caso de .data, solo necesitamos darle permisos

Jose Selvi dijo...

@Javier: Estaba pensando en un caso general, no en este problema concreto.

Yo para este problema configuraría el sistema en AlwaysOff y a correr, que a fin de cuentas es una máquina que uso yo para debuggear cosas.

De todas maneras, tu aportación me ha parecido muy interesante y te lo agradezco. Creo que ha dado lugar a comentar un poquito sobre el OptIn, Optout, etc en otro post.

Muchas gracias :)

Javier Aguinaga dijo...

Gracias a ti.

Es lindo tener feedback rapidamente jejeje