-------------------------------------------- Exploits Basados en Stack Overflows Para x86 -------------------------------------------- Exposicion para la UnderCon III ------------------------------- The Dark Raver -------------- Introduccion ~~~~~~~~~~~~ Sin duda la gama de procesadores estrella de los ultimos tiempos es la familia de los x86 de intel y derivados. Ya no solo en ordenadores de sobremesa sino tambien en maquinas que se encargan de labores pesadas. (Servidores, estaciones de calculo, etc...) Esto se puede apreciar sobre todo por la gran diversidad de sistemas operativos basados en esta plataforma. Tanto unixes como otros nuevos que intentan sin mucho exito convertirse en SO fiables para labores criticas. La pila en x86 ~~~~~~~~~~~~~~ No voy a extenderme mucho en esto, teneis montones de textos muy completos sobre la pila en x86. Esquematicamente y de forma muy simplificada, la pila en x86 seria asi: 9 0x00000009 => EBP | La pila crece hacia abajo 8 0x00000008 | 7 0x00000007 | 6 | 5 V 4 3 2 1 0x00000001 => ESP push -> la pila crece -> decrece ESP pop -> la pila decrece -> crece ESP Ahora supongamos una parte de un ficticio programa en C: main() { call funcion() <= push EIP <= push EBP } funcion() { buf[64] <= push buf strcpy(buf,ovf); } => Restauramos EIP Si el tamaño de la cadena ovf es mayor que 64 bytes (El tamaño de buf) entonces se producira un desbordamiento de pila, y se pisaran los valores de la pila anteriores a la variable buf. 9 0x00000009 => EBP ^^ 8 0x00000008 xx 7 0x00000007 <=== EIP xx 6 <=== EBP xx 5 <| xx 4 <|=buf[3] xx 3 <| xx => Datos que inyecatmos 2 1 0x00000001 => ESP Entre estos valores sobreescritos se encuentra la copia de EIP que se hace en la pila en cada llamda. De esta forma podemos pisar el EIP que sera restaurado al salir de la funcion. De esta forma podemos redirigir el flujo de ejecucion hacia donde queramos. Formas genericas de exploit ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Por lo tanto la forma generica del codigo a inyectar seria esta: ........[shellcode].........[EIP] ^---------------------^ El codigo inyectado contiene un pequeño shellcode en ensambaldor que ejecutara el comando que queramos y contendra 4 bytes con los que pisara la direccion de retorno con la direccion en memoria del shellcode. Sin embargo esta tecnica es muy dependiente de que el buffer sea siempre del mismo tamaño, de que la zona de memoria donde se encuentre el shellcode sea la misma, etc... O sea unas condiones muy estrictas. Para hacer nuestro shellcode mas "todoterreno" lo construimos de la siguiente forma: [nops_nops_nops_nops][shellcode_shellcode][retorno_retorno_retorno] 909090909090909909090XXXXXXXXXXXXXXXXXXXXX1234567812345678123456780 ^ [ EIP ] ----------------------------------------------^ Introducimos un grupo de instrucciones nulas antes del shellcode. De esta forma el valor del puntero de instrucciones no necesitara tomar un valor exacto del inicio del shellcode sino que podra ser aproximado, mientras caiga dentro del rango de nops. Tambien introducimos muchas direcciones de retorno despues del shellcode, por si el tamaño del buffer varia entre distintas versiones asegurandonos que siempre es pisada EIP. Esto introduce un nuevo problema, el alineamiento. Muchos programadores toman como obvio el alineamiento o lo calculan a ojo, esto no es demasiado profesional. :) Por ejemplo supongamos que queremos redirigir el flujo hacia la direccion de memoria 12345678 para ello inyectamos nuestro codigo: [xxxxxxxxxxxxxxx][ EBP ][ EIP ] 12345671234567812345678123456781234567812345678 34567812 => Este es el valor con el que pisamos EIP, no es el que buscabamos. Probemos con un alineamiento de +1 desplazando un byte hacia la derecha nuestras direcciones de retorno. [xxxxxxxxxxxxxxx][ EBP ][ EIP ] 0012345671234567812345678123456781234567812345678 12345678 => Ahora si que es la direccion correcta De esta forma, para programar un exploit para desbordamiento de pila necesitamos 4 requisitos: 1) Un buffer que sea desbordable 2) Un shellcode en alguna zona de memoria 3) Conocer la direccion de memoria del shellcode 4) Un alineamiento corecto 1) Para encontrar un buffer explotable: - Seguir habitualmente boletines de seguridad o de desarrollo donde se informe de este tipo de bugs. - Conseguir las fuentes del programa objetivo y buscar instrucciones peligrosas, cmo strcyp, strcat, sprintf, vsprintf, etc... - Probra el programa enviandole largas cadenas de texto, intentanto encontrar algun buffer desbordable. 2) Normalmente lo meteremos en la pila en el propio buffer desbordado, aunque no siempre es posible. (Ver seccion de problemas) 3) Para localizar nuestro shellcode en memoria usaremos un debuger, por lo tanto necesitamos tener acceso a por lo menos una maquina como la que queramos explotar. Hay otras tecnicas, como probar por fuerza bruta, pero son demasiado imprecisas. 4) Hay 4 alineamientos posibles, podemos calcularlos a ojo, o probar los 4 hasta dar con el correcto. Limitaciones ~~~~~~~~~~~~ 1) El shellcode no puede contener caracteres nulos -> El caracter nulo significa final de cadena, con lo cual si nuestro shellcode tiene caracteres nulos, solo se copiara en el buffer hasta el primer caracter nulo. 2) Normalmente el tamaño del buffer explotado es limitado, nuestro shellcode no debe ser demasiado grande. Problemas ~~~~~~~~~ 1) El overflow no se produce en la pila, sino en el heap => No hay nada que hacer con esta tecnica. Algunos de estos overflows son explotables, pero hacer un exploit de este tipo portable es muy dificil 2) El programa produce un SIGFAULT antes de restaurar EIP => Hay que inyectar un codigo que intente no pisar, o pisar con informacion valida las variables de la pila, para evitar que el error fatal que termine con el programa, de todas formas suele ser dificil hacerlo. 3) No tenemos espacio para meter el shellcode => Podemos meter el shellcode en otra variable que reciba el programa, o despues de la direccion dde retorno si es posible. 4) La pila no es ejcutable => Habra que recurrir a algunos trucos, dificulta bastante el exploit. Shellcodes ~~~~~~~~~~ Casi todo lo anterior es aplicable a todos los SO operativos basados en x86, sin embargo los shellcodes para cada uno seran diferentes. [ LINUX ] Empezamos con el SO clasico cuando hablamos de exploits y de overflows. Hay poco que decir sobre el. Teneis montones de shellcodes y de textos explicativos si quereis echarles un vistazo. Sobre todo el texto imprescidible es el de Aleph One "Smashing The Stack For Fun And Profit" [ SCO Y BSD ] Dentro de estas 2 siglas se incluyen multitud de sistemas operativos: SCO -> Las series Unixware y Openserver BSD -> Las series FreeBSD, BSDi, OpenBSD, NetBSD Todos con una caracteristica comun. Su estructura interna es muy similar, y su ensamblador es el mismo, es decir, los shellcodes son totalmente compatibles. Esto nos evitara tener que programar un shellcode para cada uno de ellos y facilitara la labor de portar algunos exploits. Para mas detalles echarle un vistazo a mi texto "Programando un shellcode en SCO" y su secuela "Programando un shellcode en BSD" [ NT ] En NT trabajamos de forma algo diferente a los unix, aunque la filosofia es la misma. La pila funciona de la misma forma y se sobreescribe en las mismas situaciones. En vez de la llamada al sistema excev() en NT usamos la llamada system() que encontramos en la libreria msvcrt.dll Por lo tanto para que funcione el shellcode necesitamos tener cargada en memoria esta libreria, bien cargandola nosotros mismos con la llamada al sistema LoadLibraryA() o bien si el programa explotado ya la ha cargado. system() -> msvcrt.dll Un problema muy habitual en los NT es que la direccion de la pila donde se encuentra nuestro shellcode contiene caracteres nulos 0x00123456. Por lo tanto si actuamos de la forma clasica deberiamos pisar la direccion de retorno con este valor. Pero como sabeis no podemos introducir caracteres nulos en el codigo que inyectamos para producir el overflow. Para solucionar esto normalmente se recurre a un pequeño truco. Se busca en las librerias estandares del sistema (Normalmente kernel32.dll) una instruccion que redirija el flujo del programa a nuestro codigo normalmente un jmp ESP o un jmp EBP. Las librerias del sistema estan en una zona de memoria cuyas direcciones no contienen caracteres nulos, y que ademas son fijas para la misma version de cada libreria. Por lo tanto pisamos la direccion de retorno con la direccion de la instruccion que necesitemos previamente localizada en las librerias y esta instruccion redirigira el flujo hacia nuestro shellcode. Esta misma tecnica es tb aplicable a la arquitectura Win32 (W95/98) Conclusion ~~~~~~~~~~ He intentado hacer este texto lo mas ligero posible, aun asi comprendo que es dificil pillarlo todo a la primera sobre todo en una conferencia. Asi que no os preocupeis si algo no ha quedado claro. Si quereis mas detalles teneis montones de textos sobre el tema, mucho mas detallados si quereis profundizar. Y para cualquier duda no dudeis en preguntarme. The Dark Raver