NOTE: If you want to read the post in English, please use the Google Translate button located on the right side of the page.
Un nuevo año, un nuevo reto y un nuevo Pwn. ]¬)
En esta ocasión el reto se trata de realizar la traducción de un listado de código en Lenguaje Ensamblador a código en Lenguaje "C", identificar la vulnerabilidad existente y aprovecharse de la misma para imprimir en pantalla un mensaje.
Bien, revisemos los puntos que se necesitan completar:
1. ¿Cuántas variables locales hay en la función main?
2. ¿De qué tipo son cada una de las variables locales identificadas en el punto anterior?
3. Detecta la vulnerabilidad existente en el código.
4. Define en qué condiciones exactas es posible explotarla.
5. Convierte a código C el codigo ASM y compílalo sin errores para conseguir el ejecutable final.
6. Explota el fallo y modifica el flujo del programa para que imprima por pantalla el siguiente mensaje "[*] Pwned!"
7. Propón correcciones para evitar la vulnerabilidad tanto en el código en C como en ASM
NOTA: Intentaré llevar en la secuencia que indican las preguntas, sin embargo, es probable que o salte puntos o toque ciertos temas antes de abordar por completo el objetivo de la pregunta.
Bien, sin más ni más, ¡al ataque!
Para empezar, hay que ponernos cómodos con respecto a nuestro objetivo a analizar. En este caso, demos un vistazo al código en Lenguaje Ensamblador para darnos una idea que como se encuentra compuesto y de igual modo, identificar puntos importantes que nos ayudarán en próximos pasos.
5.- Convierte a código C el codigo ASM y compílalo sin errores para conseguir el ejecutable final.
Después de un breve análisis, nos dimos cuenta que la estructura es muy intuitiva, es decir, es muy fácil de identificar los "pedazos" que se refieren a alguna rutina la cual ejecuta cierta acción y dará un resultado posterior. Las rutinas antes mencionadas son las que comienzan con un punto y la letra L (.L), los que tengan experiencia programando en Ensamblador se sentirán cómodos con esta estructura.
Ahora, para simplificar un poco la tarea del análisis, vamos a identificar las funciones utilizadas en el listado. Esta tarea la resolvemos fácilmente ya que las funciones inician con un "CALL". Veamos...
Identificamos 5 funciones y nos podemos dar una idea de que se trata:
1. Hay un cálculo de tamaño de una string (strlen();)
2. Hay copiado de datos (strncpy(); y strcpy(); respectivamente) <- buffers? ]¬)
3. Las funciones (printf(); y puts();) muestran en pantalla datos
Para los que se encuentran familiarizados con la programación en lenguajes de alto nivel, sabrán que el llamado a una función es muy sencilla. Si requerimos imprimir en pantalla "ph33r" simplemente tenemos que hacer un printf("ph33r"); sin embargo, cuando dicha función es ejecutada, las instrucciones en ensamblador "internamente" ejecutan la función de manera distinta.
Ahora, veamos cómo se visualiza la función printf en el código ensamblador que tenemos:
En el ejemplo anterior, antes de la ejecución de la función printf, hay varias instrucciones las cuales al final permitirán a printf, tomar en el stack el valor de la dirección de memoria que apunta al string.
Un consejo que me gustaría compartirles es que cuando tengan que analizar funciones hay que tomar en cuenta que las funciones reciben distintos parámetros con diferentes tipos de dato. Desde la perspectiva de Ensamblador no es lo mismo un printf("ph33r"); que printf("%s", <variable ph33r>);. ¿Por qué?
Bueno, hagan las prueba, visualícenlo en el debugger y verán que en esta segunda, habrá instrucciones adicionales que se ejecutan antes la invocación de la función printf. Tal como se vio en el ejemplo anterior.
Ahora, que ya tenemos una idea acerca del cómo trabajan las funciones en Ensamblador, hay que identificar los argumentos utilizados por cada una de las funciones; de este modo podremos separar en pedazos las funciones y eventualmente el código se verá mejor estructurado y nos será más fácil entenderlo, y por consiguiente, traducirlo.
Veamos prototipos de las funciones que identificamos en los CALL's y así sabremos si es uno -o varios- los argumentos requeridos por función.
Veamos la primera función: strlen();
Acorde a los que nos indica la descripción, strlen(); recibe solamente un argumento de tipo caractér, que en si es un puntero que direcciona a un string en memoria y su valor de retorno es el número de caracteres contenidos en el string.
No haré el mismo análisis del resto de las funciones, me parece que es claro lo que hay que hacer; sin embargo, aquí incluyo la dirección la cual pueden consultar para obtener la información del resto de las funciones.
NOTA: No explico línea por línea que instrucción hace qué, no lo dejaré tan fácil así los empujaré un poco a por lo menos probar por ustedes mismos que ya con tener el código C considero es una muy buena ayuda. Si quieres aprender hay que sudar por lo menos un poco.
Un detalle que hay que tener en cuenta antes de proceder es la pista que nos da el código del reto, y ésta es la línea que indica: ".intel_syntax noprefix" la cual nos indica el tipo de sintaxis utilizada para compilar el código. Como vimos, la sintaxis "Intel" fue la utilizada al momento de la compilación que generó el listado original en Ensamblador. Les recomiendo ampliamente que investiguen un poco acerca de la vieja historia de las diferencias entre la sintaxis Intel y AT&T. Pueden encontrar ligas informativas al final del documento.
Para que podamos generar el mismo código, vamos a requerir de agregar un par de líneas a la configuración de nuestro IDE.
En mi caso, utilicé el Code::Blocks con la configuración de generación de código Ensamblador.
"Global Compiler Settings => Other Settings => Advanced options". En ese punto las opciones deberán quedar como en la siguiente imagen:
Las líneas agregadas nos permitirán la generación del archivo ".asm" que mencioné con anterioridad.
Ya que tenemos la configuración necesaria, ahora procedamos a la compilación del código C.
El siguiente punto es compilarlo sin errores. Compilemos...
Bien, ya tenemos el ejecutable listo. Como habilitamos en el IDE la configuración de generación de código Ensamblador al momento de compilar, entonces veremos que nos generó un archivo con extensión ".asm", archivo en el cual encontraremos el código Ensamblador generado por nuestro código en C.
Ahora, un punto crucial para saber si ganamos o perdimos es realizar la comparación "One-by-One" del código Ensamblador del reto contra el generado por nuestra compilación. ¡A cruzar los dedos!
Veamos la salida en Ensamblador de la compilación del código C :
w00t! - ¡Misma salida! Vamos por el buen camino. ]¬)
Hasta este punto, hemos concluido con el punto #5 y pasamos al punto #1
1.- ¿Cuántas variables locales hay en la función main?
Tal como pudimos "desenmascarar" en el código C, las variables locales son 3 y son de tipo caractér.
1. char buffer[32];
2. char buffer_limit = 32;
3. char len_string = strlen(argv[1]);
Un punto a importante con respecto a las variables en Lenguaje C es que para cada tipo de variable contiene un tipo de entero (Integer type) los cuales permiten asignar -de forma arbitraria- el tamaño de la variable a utilizar y esto se realiza mediante la asignación -o uso- de su signo.
Una variable con signo (signed), es la que puede contener números negativos y de manera contraria las variables sin signo (unsigned).
Veamos la tabla de tipos de datos utilizados en el Lenguaje C, así como su capacidad (en tamaño).
Como verán en el caso de 'char', su tamaño corresponde a 1 byte aún sin expresamente asignar la variable como tipo 'signed char' con un rango de -128 (negativo) hasta 127 (positivo).
Una de las problemáticas bien conocidas con relación a la asignación arbitraria de la propiedad de signo al hacer uso de variables es la de realizar operaciones/conversiones con tipos de datos y propiedades diferentes. Ya veremos el impacto de dicha problemática durante el proceso de identificación de vulnerabilidades contenidas en este ExploitMe, ya que una de ellas se relaciona a este tema en particular.
2.- ¿De qué tipo son cada una de las variables locales identificadas en el punto anterior?
La primera variable es un buffer de tamaño restringido (o "fixed size" como le conoce comúnmente), es decir, que el programador asigna el tamaño del buffer y del mismo modo, el número de bytes soportados por dicho buffer. En este caso dicha variable reserva un espacio de memoria de (32 bytes) que en realidad ocupará un poco mas de bytes.
La segunda variable es de también de tipo char, pero en este caso aloja una solo byte, el cuál es 32d (20h). En el listado en Ensamblador aparece de la siguiente manera:
mov BYTE PTR [esp+63], 32 ; 32d == 20h
La tercera variable, es la que se encarga de guardar el valor del resultado de la ejecución de la función strlen(argv[1]);
Por default, todas las variables de tipo char son consideradas con signo. Esta cuestión estará más clara conforme vayamos avanzando.
3.- Detecta la vulnerabilidad existente en el código.
El simple hecho de que un código en C maneje funciones "peligrosas" o para entenderlo mejor, funciones que manejan intercambios de datos; esto nos da un indicio acerca de lo que podría pasar, y tomando en cuenta que estamos resolviendo un ExploitMe, entonces sabemos por dónde va la cuestión; sin embargo, existen otro tipo de vulnerabilidades, entre ellas las de tipo "Lógico", de las cuales pueden ser de igual manera peligrosas para el programa y que a su vez, pueden ser partícipes de una vulnerabilidad de mayor impacto, como es el caso de este ExploitMe.
Cuando me refiero a funciones peligrosas, me refiero a aquellas funciones que manejan bytes y que requieren mucho cuidado por el lado del programador para "controlar" las entradas utilizadas por dichas funciones. El lenguaje en sí, permite hacer uso de funciones mas "seguras" las cuales incrementan en gran cantidad la seguridad mediante el control de datos de entrada, sin embargo, éstas siguen estando bajo el control del programador.
Existen muchos escenarios para explotar una vulnerabilidad, y no me refiero específicamente a la vulnerabilidades de tipo overflow, ya que como lo comente anteriormente, muchas de estas vulnerabilidades que se pueden considerar fuera del espectro normal de las más peligrosas o en su defecto, -y muchas veces sucede- vulnerabilidades de "bajo riesgo" las cuales sirven como trampolín a alcanzar una explotación exitosa o probablemente la elevación del riesgo de una vulnerabilidad que era considerada de bajo riesgo.
Una de las formas disponibles de identificación de vulnerabilidades en código son las herramientas automatizadas para ejecutar dicha tarea. En este caso como ya contamos con el código fuente, podemos utilizar algunas de las herramientas que se encuentran disponibles para su descarga de manera gratuita en Internet. En este caso usaré RATS y FlawFinder.
Si ejecutamos RATS para escanear el código vulnerable en Lenguaje C, veremos que la herramienta de igual manera identifica las dos vulnerabilidades antes mencionadas.
En la pantalla siguiente tendremos la salida de FlawFinder, la cual indica de igual manera las dos vulnerabilidades identificadas por RATS, sin embargo identificó 2 más.
Cabe mencionar que existen distintos factores para confirmar que una vulnerabilidad pueda ser explotada. Existen ocasiones en las cuales los reportes de herramientas de escaneo de vulnerabilidades contienen FP's/FN's (Falsos positivos y negativos) o de igual manera identifiquen de una manera muy básica el uso de ciertas funciones vulnerables pero que su explotación sería muy difícil o probablemente no existan las condiciones ideales para llegar a aprovecharse de la vulnerabilidad.
En otras palabras, el hecho que la herramienta diga una cosa, puede diferir en la realidad y no existe mayor confirmación que la de analizar manualmente la vulnerabilidad e intentar explotarla.
Un punto importante a recalcar es que el código C contiene una vulnerabilidad la cual no fue detectada por ninguna de las dos herramientas utilizadas. Esto marca de manera concisa que no hay que confiar siempre en el resultado de las herramientas y nunca dejar de lado la confirmación.
Regresando a la pregunta de este punto. Existen dos vulnerabilidades, una relacionada a los comparación de variables con signo (char's) y por último una que entra en la categoría de los Overflows.
Si nos referimos expresamente al código C que generamos, podemos visualizar que las condiciones y funciones vulnerables son las siguientes:
1. if ((len_string + 1) > buffer_limit) // Al manipularla nos permitirá saltar a strcpy();
2. strcpy(buffer, argv[1]); // La función recibirá lo que venga desde el argumento 1
Pongan atención a dichas funciones, ya que en los próximos puntos serán cruciales para alcanzar nuestro objetivo.
Existen más vulnerabilidades además de los "overflows", de hecho, no todo reside en "enviar A's" a la entrada de algún programa y esperar a desbordar el buffer; hay algunas otras como los "Format strings" -entre otras- y que no requieren de cierta manera que el buffer sea pequeño (hablando del tamaño de bytes) para poder ser explotada la vulnerabilidad.
En este el caso particular de este ExploitMe, el código contiene 2 tipos de vulnerabilidades; una de conversión de tipo (Type conversion/sign-extension) y otra relacionada a buffer overflow.
Sin importar en que lenguaje estemos hablando, las vulnerabilidades de validación de entrada son siempre una de las mayormente explotada, llámesele en ejecutables, aplicaciones Web, etc. La regla de "oro" para un desarrollador es y será:
"Nunca confiar en las entradas brindadas al usuario y hay que asumir
que cualquier dato proveniente debe ser considerado como malicioso"
Si se encuentran interesados en el tema, pueden encontrar mayor información en el sitio oficial de CWE de MITRE. Esta y demás ligas al final del documento.
Bien, después de toda esta pequeña explicación que considero importante recalcar, regresaré al punto que nos concierne.
El código utiliza dos funciones para copiar datos: strcpy(); y strncpy();, ésta última con la "n". La versión un poco más "segura" es strncpy(); ya que se puede asignar el número esperado de bytes a utilizar, y no depender del tamaño del buffer asignado.
Veamos como son utilizadas en el código C que generamos:
1. strncpy(buffer, argv[1], buffer_limit);
2. strcpy(buffer, argv[1]);
En la función #1 podrán ver que el último argumento es "buffer_limit" el cual asigna el límite de caracteres permitido, mientras que para la función #2 no hay dicho control. Si en nuestro código C hay un buffer destino, y utilizamos una función vulnerable con la cual no se puede controlar el número de caracteres de entrada, entonces hemos encontrado un buen punto de explotación.
Ahora, se podrán imaginar que pasa cuando en nuestro código generado se utiliza la siguiente combinación:
char buffer[32];
[...]
strcpy(buffer, argv[1]);
Si alguna persona con conocimientos de "Exploiting" se encuentra con dicha condición, sabrá que al momento de ser explotada la vulnerabilidad contenida puede concluir en una ejecución de código.
Ya que tenemos bien entendido este punto, prosigamos...
4.- Define en qué condiciones exactas es posible explotarla.
Como ya lo había comentado con anterioridad, el objetivo principal será el identificar la manera de poder llegar hasta la función strcpy(); atravesando cualquier inconveniente (Condicionales?) que nos encontremos en el camino y para esto, hay varias formas de hacerlo. Una, leyendo el código y generar la estrategia a utilizar -o mejor dicho- formar y enviar la entrada correcta al programa el cual nos permita llegar a la función vulnerable. Esto muchas veces es difícil, pero no imposible. ]¬)
En este caso en particular contamos con el código fuente que logramos reconstruir, entonces será mucho más fácil la identificación de cualquier problema, llámesele Bug o vulnerabilidad.
Contamos con 2 funciones, la condicional (IF) y la función de copiado de datos (strcpy();) y la pregunta ahora será cómo llegar a strcpy();, o en otras palabras, ¿Cuál es la condición necesaria para llegar a ella?
Para dar una respuesta a dicha pregunta de una manera didáctica, vamos a trabajar directamente en el ejecutable mediante el uso del Debugger y así ir viendo en tiempo de ejecución lo que va sucediendo en el área "caliente" de nuestro interés.
NOTA: No analizaré instrucción por instrucción, solamente me iré a los puntos cruciales que atañen a la identificación de la vulnerabilidad, antes de su explotación.
Para empezar, ya sabeos de antemano cuál es la condición, porque lo podemos ver en el código, y por consiguiente veamos todos los puntos que tienen que ver en dicha condición.
1. if ((len_string + 1) > buffer_limit)
Si lo dividimos, sabremos que hay algunas "dependencias" o variables utilizadas que fueron asignadas antes de llegar a la condición, como lo es el caso de "len_string" y "buffer_limit".
El siguiente paso será habilitar un BP en la función strlen() y de igual modo en la comparación (CMP)
• 004013DE |. E8 BD080000 CALL <JMP.&msvcrt.strlen> ; ||\strlen
• 004013F4 |. 39C2 CMP EDX,EAX ; ||
Ahora, arranquemos el Debugger y mandamos como parámetro: AAAAA (5 caracteres), y nos pararemos automáticamente en el primer BP (strlen();), nos pasamos a la siguiente instrucción y veremos que el valor de EAX que es ahora 5, como era de esperarse.
Si tratamos de ver los detalles de EAX, veremos la siguiente ventana:
Vemos que AL es igual a 05 y que la propiedad de signo/no-signo es 5. Llegamos hasta la comparación (CMP) y vemos lo siguiente:
EAX es igual a 20h y EDX es igual a 6. Esto sucede porque antes de llegar a la comparación, el valor de la variable buffer_limit (20h) es movido a EAX, y EDX contiene lo que es el valor de strlen(); (5 +1) = 6.
La siguiente instrucción indica que como el valor NO es mayor o igual, entonces brinca hasta la función strcpy();. Obviamente hasta este punto solo logramos copiar 5 bytes al buffer.
Un punto importante. Como sabemos, en el Sistema Hexadecimal solo tenemos disponibilidad de números desde 0 hasta el 9. ¿Qué pasa si enviamos 10 caracteres? Veamos...
Era de esperarse (0Ah == 10d). Hasta este momento es claro que en realidad el resultado de strlen(); que es asignado a EAX.
Ahora, sabemos que en la comparación entre EDX y EAX se encuentra el valor Hexadecimal (20h). Hagamos la conversión en la calculadora:
OK, ¿Qué pasa si intentamos enviar 32 bytes?
w00t, EAX == 20. Al llegar hasta la comparación vemos:
Vemos ambos valores de EAX y EDX, y como sabemos que el número es mayor, entonces a la hora del salto indicará que el origen de los datos es demasiado grande y por consiguiente no podremos continuar con nuestra ruta a strcpy();.
EAX = 20 y EDX = 21 (si recuerdan el strlen(variable) + 1?) EDX es dicho resultado.
...y por consiguiente nos manda al mensaje de error:
¿Qué pasa si intentamos ahora con 128 caracteres?
Ejecutamos la segunda instrucción después de strlen (MOVSX);
004013E7 |. 0FBE4424 3E MOVSX EAX,BYTE PTR SS:[ESP+3E] ; ||
Veremos ahora el valor de EAX después de ejecutar la instrucción anterior:
Como vemos, la instrucción no solamente copio el valor a EAX, sino también lo hizo a su signo.
Veamos rápidamente lo que indica el manual de Intel acerca de la instrucción MOVSX:
Code
|
Mnemonic
|
Description
|
0F
BE / r
|
MOVSX
r16, r/m8
|
Move byte to word with sign-extension
|
0F
BE / r
|
MOVSX
r32, r/m8
|
Move byte to doubleword, sign-extension
|
0F
BF / r
|
MOVSX
r32, r/m16
|
Move word to doubleword, sign-extension
|
Description
Copies the contents of the source operand (register or memory location) to the destination operand (register) and sign extends the value to 16 or 32.
Lo que hace la instrucción básicamente es rellenar (extender) el valor en el registro y es por eso que visualizamos FFFFFF80 en el registro EAX.
Continuamos en flujo de la ejecución...
004013EC |. 8D50 01 LEA EDX,DWORD PTR DS:[EAX+1] ; ||
En este momento estaríamos pensando que el valor próximo sería 129 (00000081h); sin embargo, vemos que el valor de EDX después de ejecutar la instrucción anterior es diferente al esperado:
Seguimos hasta el próximo MOVSX...
004013EF |. 0FBE4424 3F MOVSX EAX,BYTE PTR SS:[ESP+3F] ; ||
Lo ejecutamos, y los registros se visualizan de la siguiente manera:
Como recordarán, éste es el valor de 32d (20h), el cual se encontraba en el stack y es cargado a EAX justo una instrucción antes de llegar a la comparación (CMP EDX, EAX).
Continuamos hasta la comparación y vemos que los registros contienen lo siguiente:
Si la siguiente condición if ((len_string + 1) > buffer_limit)en el código C se cumple, entonces no habría salto a strcpy(); sin embargo, como sucedió lo contrario, es decir:
EAX = 00000020 (Signed: 32 / Unsigned: 32)
EDX = FFFFFF81 (Signed: -127 / Unsigned: 4294967169)
if (-127 > 32) {
[...]
strcpy(...);
Ejecutamos la comparación:
Si ponen atención a las banderas, verán que la S (Signed Flag) fue habilitada, lo que indica que el resultado obtenido ha sido negativo.
La siguiente instrucción es JLE
004013F6 |. /7E 3E JLE SHORT exploitm.00401436 ; ||
y vemos que 401436 es el salto a strcpy();
00401436 |> \8B45 0C MOV EAX,DWORD PTR SS:[EBP+C] ; ||
00401439 |. 83C0 04 ADD EAX,4 ; ||
0040143C |. 8B00 MOV EAX,DWORD PTR DS:[EAX] ; ||
0040143E |. 894424 04 MOV DWORD PTR SS:[ESP+4],EAX ; ||
00401442 |. 8D4424 1E LEA EAX,DWORD PTR SS:[ESP+1E] ; ||
00401446 |. 890424 MOV DWORD PTR SS:[ESP],EAX ; ||
00401449 |. E8 6A080000 CALL <JMP.&msvcrt.strcpy> ; |\strcpy
w00t!. ¿Qué pasaría si continuamos con la ejecución (F9)?
Básicamente "bypasseamos" la condición requerida, de igual manera pudimos desbordar el buffer a través de la función strcpy(buffer, argv[1]); y finalmente pudimos tener control de EIP.
Es claro que requerimos de 128 bytes para generar nuestro Exploit, pero ¿Por qué?. Veamos...
Como bien sabemos el buffer utilizado en el código C es de tamaño definido (32 bytes) y como ya lo mencionaba anteriormente, al desbordar el buffer y tomar control de EIP lo pudimos hacer con un total de 128 bytes. Es claro que lo que importa aquí no es de primera mano el tamaño del buffer, sino que para poder llegar hasta EIP, tuvimos que saltar la condicional de manera que al querer hacerla de tamaño mayor, su signo no lo permitió y por consiguiente se convirtió en negativo y lo que eventualmente nos permitió el salto antes mencionado y poder llegar al strcpy(); (o buffer) y finalmente al control de EIP.
Cuando no se cuenta con el código del programa es mucho más difícil el identificar alguna falla, Bug o vulnerabilidad, ya que no se tiene información precisa acerca de si hay o no buffers, variables, entradas desde los argumentos y demás. Si ese fuera el caso, entonces hay dos opciones:
1. Hacer un análisis de código muerto y tratar de figurar la estructura del programa y demás
2. Fuzzear de manera "tonta" el ejecutable, probar distintos parámetros de entrada, con distintas longitudes e intentar identificar cuantos caracteres fueron los que "crashearon" el programa y partir desde ese punto para pasar a las tareas de elaboración de PoC o del Exploit en sí.
3. Fuzzear de manera "inteligente". Como ya tenemos código, podríamos armarnos con un Fuzzer que envíe los parámetros esperados, pero al mismo tiempo distintos.
Para este caso en particular del ExploitMe, lo haré a través de la segunda opción. No generaré un Fuzzer, pero lo que si haré será una prueba muy sencilla para identificar rápidamente cual es el camino correcto a seguir. Considero esta es la forma más didáctica de igual manera así que probaré directamente sobre el ejecutable generado desde nuestro código en C.
Respondiendo en concreto a cuáles son las condiciones necesarias para explotar la vulnerabilidad, es claro que se requerirá saber específicamente el número de bytes exactos tanto para "alcanzar" la vulnerabilidad, así como su explotación o en otras palabras realizar los arreglos necesarios para "brincas" a nuestro shellcode, realizar la ejecución de código y otras posibles cuestiones como la del "padding" o alineamiento, los Bytes nulos y demás. Este punto quedará más claro cuando estemos adentrados en la parte de explotación de la vulnerabilidad.
Pasemos al punto #6 en donde continuaremos dichas pruebas.
6.- Explota el fallo y modifica el flujo del programa para que imprima por pantalla el siguiente mensaje "[*] Pwned!"
¡Por fin! Llegamos a la parte que más me gusta - Explotación ]¬)
Ahora sí, ya identificamos muy por encima que existe una potencial vulnerabilidad, y me refiero a "potencial" porque aún no la hemos explotado.
Como vimos en el código, hay una condicional, dependiendo del resultado de la misma o se va por el camino "A" o por el "B", o lo que es lo mismo, nos manda a " "[!] Datos de origen es demasiado grande, se perderán los datos sobrantes." o a "[+] Datos copiados correctamente.\n%s". Es claro que la segunda opción es a donde queremos llegar; por consiguiente, nuestro objetivo será proveer de una entrada en la cual nos arroje el mensaje que estamos esperando (el segundo); sin embargo, el objetivo será la condicional, pasar hasta la función strcpy(); PERO con la cantidad de bytes requerida que nos sirva para preparar posteriormente nuestro payload de ataque.
Para este objetivo, codifiqué un pequeño script para enviar argumentos de diferente longitud y para que de igual manera veamos cómo se comporta el ejecutable al momento de recibir dicho parámetro.
Bien, enviamos 30 caracteres "A" y la salida fue la esperada. Probemos con 80 bytes...
Al parecer, 80 bytes sobrepasa lo que es el límite esperado por el programa. Sigamos probando, ahora con 200 bytes...
Ahora nos indica que los datos fueron copiados de manera correcta, y de igual modo pudimos ver que el contenido de la entrada de prueba también es mostrado en pantalla.
Algo curioso a retomar en este momento es, claramente 200 bytes es mucho más que lo que en realidad la variable del programa soporta. Entonces, Cómo es que a pesar de haber sobrepasado el límite del buffer, el programa no "crasheo". Intentemos identificar el punto medio entre la condicional, es decir, en la posición exacta entre recibir mensaje "A" y "B".
Probemos ahora con 124 bytes...
Ahora con 128 bytes...
w00t! El debugger saltó y de igual manera nos muestra el viejo mensaje que la dirección 41414141 es inválida. Se da por entendido que tenemos control en la ejecución del programa.
NOTA: No me detendré en este punto a explicar lo que sigue acerca de qué pasa cuando se recibe este mensaje. Les sugiero vayan a mi sitio web y descarguen mis tutes anteriores en donde lo explico mas a detalle. Por ahora partiremos que los bytes necesarios para sobrescribir EIP son 128.
Ahora empieza la parte "fina", es decir, el control de la ejecución y por consiguiente, la ejecución de código de nuestra elección.
Hagamos un check-point de lo obtenido hasta este momento:
1. Sabemos que debemos imprimir en pantalla "[*] Pwned!"
2. Sabemos el número de bytes requeridos para tomar control de EIP (128)
¿Qué nos falta?
1. Encontrar un punto de salto (JMP, CALL, etc.)
2. Encontrar la función que imprima en pantalla contenido de nuestra elección
3. Formar la string que vamos a desplegar en pantalla, generar los preparativos necesarios en el stack para posteriormente llamar a la función de impresión en pantalla
4. Identificar la función de salida o término de proceso
Empecemos...
Punto #1
Para identificar a donde vamos a saltar, deberemos identificar en donde se encuentra un "JMP ESP" en memoria y para ello, vamos a elegir una DLL que sabemos que siempre esta activa, es decir: KERNEL32.DLL. Para ubicarla vamos a utilizar la vieja herramienta llamada "Findjmp2.exe".
Como vemos en la imagen, encontró una dirección válida en 0x7C82385D (CALL ESP). Guardémosla ya que la vamos a utilizar muy pronto.
Punto #2
Casi de la misma manera que en el punto anterior, vamos a buscar ahora la función que nos permita hacer una salida a consola. Como sabemos de antemano que la aplicación es de tipo "consola", entonces vamos a requerir ubicar una función que permita hacer dicha salida. El código utiliza "printf" y de igual manera podremos utilizarla.
Ahora, con la herramienta llamada "arwin.exe", vamos a correrla buscando la función "printf" la cual es incluida en MSVCRT.DLL.
Bien, nos encontró la función en la dirección 0x77c1186a
¿Por que utilizamos estas herramientas? Son útiles cuando buscamos funciones en direcciones de memoria que no contengan Bytes nulos. La misma recomendación que les hago es ir a mis tutes anteriores para mayor información.
Punto #3
Igual que el punto anterior, pero ahora en lugar de buscar "printf", buscaremos "ExitProcess".
Encontrada en 0x7c81caa2 en KERNEL32.DLL. En este punto en particular vamos a hacer algo un poco diferente. Deberemos de hacer uso de esa dirección para hacerla válida e incluirla como argumento a una instrucción JMP.
Si utilizamos el debugger para Ensamblar la instrucción para ejecutar un "JMP 7c81caa2"
Veremos que se convierte en la siguiente instrucción:
En nuestro código Exploit aparecerá de la siguiente forma:
"\xE9\x20\xCB\x5E\x7C"
Punto #4
Para generar nuestro shellcode lo haremos a mano. Gracias a "b33f" por sus grandiosos artículos sobre explotación y del cual estoy utilizando su técnica de generación.
El shellcode estará compuesto de lo siguiente:
Con esto, estaremos "pusheando" de 4 en 4 (DWORD's) el string "[*] Pwned!". Esto será mucho más claro cuando el Exploit quede listo.
Bien, ya tenemos todo listo. Ahora falta generar el Exploit.
El Exploit estará compuesto de la siguiente forma:
Si cuentan el número de bytes utilizados por el Exploit, podrán ver que en realidad fueron los 128 bytes que identificamos previamente.
El momento de la verdad. Lo ejecutamos y...
Exploit probado en sistema Windows XP SP2 (Español) - 32bits
7.- Propón correcciones para evitar la vulnerabilidad tanto en el código en C como en ASM.
Ya nos aprovechamos de la vulnerabilidad, ejecutamos código de nuestra elección, ya saltamos de felicidad y le dimos un beso a nuestro perro. Pero, ¿Qué pasaría si nosotros fuéramos los responsables de dicho código vulnerable? Estaríamos en problemas.
Una parte importante en Seguridad no es solamente el cómo atacar, de igual importancia es como proteger y para aquellos que visualicen el área como una oportunidad de trabajo, sin duda alguna les servirá poseer ambos conocimientos.
Una propuesta de corrección siempre varía, y puede ir desde sencilla, una simple corrección al código, otras pueden involucrar algún sistema de protección que independientemente que el código se encuentre vulnerable, este software/hardware podrá hacer frente a los ataques y en su mayoría podrá ser capaz de neutralizar dichos ataques; la desventaja esta cuando dicho software/hardware ya no se encuentra disponible y pone al código/programa/aplicación vulnerable ante cualquier intento de ataque. Existen otros mecanismos de protección que pueden ir desde el compilador, hasta la seguridad propia del sistema operativo.
Para este caso en particular implementemos las siguientes correcciones al código:
1. Definición del tipo de dato "unsigned char" (líneas 7 y 8)
2. Limitación del buffer destino mediante el uso de strncpy(); (líneas 13 y 17)
3. Asignación del caractér nulo al último byte del buffer (líneas 14 y 18)
Existen varias posibilidades para la corrección de este código fuente, sin embargo, para la corrección aplicada anteriormente no eliminé el uso de las variables originales (char's) para que se entendiera mejor la importancia de la asignación precisa de tipo de datos, propiedades y demás.
Ahora, para confirmar que funciona, probemos el mismo Exploit contra el nuevo ejecutable:
Hoy en día la explotación es más difícil; sin embargo, no imposible. Para darse una idea acerca de ello les recomiendo la lectura de Aaron Portnoy llamada "Exploiting all of the Things". Pueden encontrar la liga al final del documento.
Otra recomendaciones con respecto al código en Lenguaje C, son:
1. No utilizar buffers de tamaño específico. Es siempre mejor utilizar una asignación tipo: "char *variable;" al momento de requerir el uso de strings.
2. Tener cuidado con las variables y las conversiones de tipo.
3. Cuando se manejen condicionales, no utilizar solamente IF/ELSE, siempre hay que especificar la condición y qué es lo que se espera de la misma. Al momento se llegar a un IF, se sabe que es lo que se desea, pero al llegar a ELSE, se puede atrapar distintos resultados. Siempre tener cuidado con la lógica manejada en el código ya que de eso depende muchas veces la introducción de bugs o de problemas de seguridad.
4. Utilizar las versiones seguras de las funciones provistas por el lenguaje , principalmente las que muevan información como las que hemos visto en el ExploitMe.
5. Habilitar las opciones de seguridad del compilador
5. Habilitar las opciones de seguridad del compilador
La propuesta de corrección en Ensamblador es prácticamente lo mismo aplicado para el código C. En este caso posterior a la comparación (CMP) habría que reemplazar JLE (Jump if Less or Equal) con JB (Jump if Below), adicional a la adición de strncpy(); al código.
En la tabla siguiente incluyo la tabla comparativa entre el Listado de código vulnerable en Ensamblador y el corregido para obtener una clara y mejor comparación de ambos.
Notas finales:
Después de haber recorrido un largo camino pero a su vez muy interesante, nos hemos dado cuenta de la importancia acerca de las vulnerabilidades, su impacto y de igual manera -y no menos importante- las correcciones de las mismas.
Espero que este tute les sea de ayuda en su viaje al excitante mundo del Reverse Engineering, Vulnerability Research y Exploit Development.
Agradecimientos:
1. Boken, por los retos que publica y por empujarnos a aprender cada día más
2. Ruben "b33f " Boonen (@FuzzySec/fuzzysecurity.com) y a Ron Bowes (@iagox86/skullsecurity.org) por sus grandiosos sitios
3. Ricardo Narvaja y a +NCR/CRC! [ReVeRsEr] por sus guías y su labor de compartir su conocimiento (puro Old School Power!)
4. ...y a toda la banda de México y de todos los CLS.
Ligas recomendadas:
• Librerías utilizadas en C, http://www.tutorialspoint.com/c_standard_library/
• Signedness, http://en.wikipedia.org/wiki/Signedness
• Sintaxis Intel & AT&T, http://www.imada.sdu.dk/Courses/DM18/Litteratur/IntelnATT.htm
• CWE MITRE, http://cwe.mitre.org/
• Bypassing all of the things, https://www.exodusintel.com/files/Aaron_Portnoy-Bypassing_All_Of_The_Things.pdf
• Secure Programming with GCC and GLibc, https://cansecwest.com/csw08/csw08-holtmann.pdf
"You may lose your faith on us but never in yourselves
from here the fight will be your own."
- Optimus Prime
No comments:
Post a Comment