Buffers Overflow y Exploits

Introducción

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.

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

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.

– ¿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í:
  5. 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

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:

Miremos la pila:

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.

Quedandonos con los opcodes unicamente obtenemos algo así:

y la shellcode codeada

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 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. Por tanto hora de codear nuestro exploit, en este tipo de buffers siempre es: buffer + offset + shellcode

Y esto es todo amigos!!!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *