¡Saludos
amigos!
Ya
hacía tiempo que estaba un poco desconectado de varias cosas por cuestiones
personales y laborales, sin embargo hace un par de días me encontraba
monitoreando la lista de CLS me di cuenta que Boken (uno de sus integrantes)
publicó un ExploitMe básico y haciendo un poco de espacio en mi apretada agenda
me di a la tarea de crear este pequeño tute en el que explico su resolución
paso a paso a mi modo y conforme a mis conocimientos y experiencia me lo
permitan.
El
ExploitMe objetivo se centra en la identificación de código fuente (HLL) en C y
las instrucciones en Ensamblador que correspondan a dicho código. Para los que no
tengan mucho skill en esta parte, les recomiendo MUCHO leer los tutoriales de
Ricardo de "C y Reversing" así como la guía de Dennis Yurichev con
nombre "Reverse Engineering for Beginners" y pueden descargar el
libro de forma gratuita desde acá: http://beginners.re.
Considero
que si es importante y además muy útil el poder hacer dicha
"traducción" entre C y Ensamblador -y viceversa- ya que podrán tener
una completa visibilidad acerca de lo que está sucediendo entre el programa y
la máquina, ya sea que se encuentren debuggeando, desensamblando o lo que sea.
Ya explicaré en algunos otros tutes lo que he aprendido sobre la marcha.
Regresando
un poco al tute, les pido de favor que si encuentran algún error me lo hagan
saber y con gusto arreglo el documento.
Herramientas utilizadas:
• Virtual Box (Windows XP SP2 32b - ESP)
• Code::Blocks (Compilador)
• Python
• Immunity Debugger
Código del ExploitMe:
Bien, resolvamos el primer punto:
1. Compílalo, e identifica la dirección de las funciones main() y checkpass()
Utilizando Code::Blocks, prácticamente copiamos, pegamos y compilamos el código del ExploitMe. Posteriormente lo abrimos en el Immunity Debugger.
Ahora, nos saltamos (CTRL + G) directo al VA (401000) y le seteamos un BP.
Nos vamos instrucción por instrucción (F7) y llegaremos hasta 4010F8 que es la dirección hacia donde saltaremos al codigo que nos atañe (a la función que nos muestre el mensaje de "Access granted!"). Bien, le seteamos un BP de igual manera.
Si se preguntan la razón del por qué los BP's, siento que es bueno siempre estar parando en aquellos puntos clave en los cuales uno puede parar y darse una vuelta por el stack, el memory dump o lo que sea; por lo menos a mi me ha servido.
Si saltamos (ENTER) a ese CALL, entraremos prácticamente al main().
Vemos como desde la dirección 4013C0 hasta 401427 se encuentran las instrucciones relacionadas a la función main(). En la imagen siguiente se encuentra el stack frame de main, seleccionada en color azul.
De igual manera, se encuentra la función checkpass() en el segundo bloque:
Aquí veremos que tanto aprendí del curso de Ricardo y esos días de práctica de transiciones de C->ASM->C. Siento que sería más educativo e ilustrativo si pudiéramos utilizar el Hex-Rays o hacer uso de referencias a código fuente, pero bueno, sigamos las reglas, aunque no estoy muy seguro de acatarlas XD. Veamos...
NOTA: Las instrucciones en Ensamblador se encuentran debajo de su línea correspondiente en código C.
Me gustaría de igual manera recomendarles que cuando compilen código en C, generen su equivalente código en ensamblador y de esta manera podrán saber exactamente "qué corresponde a qué". La salida es un poco similar a la que muestro en la parte superior aunque en sí lo que se hace es convertir el código C a código ensamblador, no propiamente a solo traducir el código directo a instrucciones. Por ejemplo, si en C se asigna una variable, en el listado en ensamblador se verá la asignación de variable en ensamblador.
Si no quedo muy claro, no importa. Lo más probable es que dedique un post en mi blog específicamente para tocar dicho tema de la generación del código, la configuración requerida en el compilador y demás cuestiones. Ahora al punto #3.
3. Dibuja el estado de la pila desde la función main() justo antes de ejecutar el strcpy()
Si ya has identificado las strings contenidas en el ejecutable verás que hay una la cual al ser utilizada como primer parámetro, da la salida "Access granted!".
Aunque de igual modo si se escribe cualquier otra string lo único que cambiará es que saldrá el mensaje "Access denied!"
Considero que es bueno probar ambas opciones ya que la que indica "Access granted!" está apuntando a la dirección de memoria que ejecutará la instrucción objetivo, o mejor dicho, hacia donde deberemos de "saltar". Ya veremos cómo en un rato mas.
El paso siguiente consistirá en analizar el stack al momento de enviar un parámetro. Podríamos utilizar solamente el string stupidlyinsecure si es que deseas resolver el reto de una manera rápida y de la manera no correcta XD, sin embargo, haremos lo que el reto indica. Continuemos...
Vamos a intentar identificar 2 cosas
1. Como crashea el programa con una string larga
2. Como se visualiza el stack al momento del crash
Para el número 1, abrimos el Immunity y asignamos una string de 40 caracteres (las viejas A's). Podemos intentar de primer instancia utilizar 40 A's.
Iniciamos la ejecución y presionamos F9 hasta que termine el programa.
Vemos que el programa nos manda un mensaje de "Access denied!" y si miramos los registros, veremos que EBP es igual a 00004141. Un buen indicio.
La idea es que podamos tomar control del registro EIP el cual nos permitirá saltar hacia donde queramos; que en este caso sería a la dirección del printf() el cual mostrará "Access granted!".
Ahora, veamos cuantos bytes necesitamos para tomar dicho control de EIP. Como ya vimos, pudimos sobre-escribir parte de EBP con dos bytes de A's. La pregunta ahora es: ¿Cuantos bytes necesitamos en total?. Bien, hagamos cuentas.
Si sobrescribimos 2 bytes de 4 (4 - los requeridos para un DWORD) + 4 bytes para el control de EIP nos da 46 bytes en total. Probemos ahora con esos 46 bytes...
Nice!! Tenemos control de EIP. Ahora, veamos los registros...
w00t!, EBP y EIP contienen 41h, o lo que es lo mismo el caractér "A".
Vamos avanzados; sin embargo, parte de las instrucciones es explicar que pasa en el stack momentos antes de mandar llamar a la función strcpy() y por tal, ahora que ya tenemos el número exacto de bytes, vamos a ir armando bien el PoC. Veamos...
De manera rápida, generemos un script en Python que nos permita mantener el control de una manera "programática" lo que le enviamos al programa como argumento.
El contenido del script será el siguiente:
Como verán, ahora el contenido (payload) que utilizaremos estará dividido en 3; por una parte 34 A's, 8 B's y 4 C's.
Guardamos el script en el mismo directorio del programa, lo ejecutamos y veamos que sucede.
Perfecto. EBP = 42424242 (BBBB) y EIP = 43434343 (CCCC). Ya vamos avanzando. ]¬)
NOTA: Decidí irme por este camino de la división del string por que será mas fácil - al menos para mí lo es - si visualización en el stack.
Ya que probamos que desde el script funciona, vamos a tomar el string que generamos previamente e iniciamos en el debugger una nueva ejecución tomando como argumento dicha string.
Reiniciamos la ejecución (CTRL + F2) y ahora nos detenemos en el segundo BP (401428) y empezamos a monitorear el stack mientras ejecutamos instrucción tras instrucción.
Empezaremos viendo el prólogo (inicio) de la función así como la asignación de espacio en el stack
NOTA: Doy por hecho que el seguir con la secuencia se requiere F7, en caso concreto si se requiere utilizar F8, CTRL + F9 o alguna otra combinación, la haré notar.
A continuación nos damos cuenta que en 40142B se asigna un espacio de memoria de 38h bytes (56d). Para los que no sepan, es aquí en donde se guardará el espacio de memoria requerido para las variables utilizadas en la función. En este caso, este "espacio" será utilizado para contener los bytes que recibe el programa como argumento. Aquí es fácil identificar el problema. ¿Qué pasa cuando sobrepasas el límite de memoria asignado para la variable? Ya verán...
Este es el estado actual del stack ANTES de ejecutar la instrucción:
La ejecutamos:
En el stack de realiza la reservación de espacio requerido.
Continuamos hasta llegar a la dirección 401CA8
Ahora sí, llegamos - y nos paramos un poco - en el borde de la ejecución del strcpy()
Un dato que me gustaría recalcar es acerca de los parámetros de la función. Si se fijan a detalle verán que el armado sería así: strcpy(0022FF12, "AAAA..CCC"). Se preguntarán ¿Qué significa 0022FF12? Fácil, es el buffer destino el cual "guardará" lo que se envíe como argumento al programa. En la imagen siguiente se muestra el contenido de ese espacio de memoria.
Me parece que hasta aquí ya completamos el paso #3.
4. ¿Cuántos bytes hay entre el ultimo byte de pass y el valor de retorno guardado de la función main() (RET)?
¡Ya nos vamos acercando a la parte interesante! Es hora de contar y calcular.
El paso siguiente consiste en ejecutar la función strcpy() y es precisamente lo que haremos:
Ahora, veamos que sucede en el stack:
Hagamos una comparación de los estados del stack del "antes y después" del strcpy()
Pongan atención a la dirección 22FF3C y verán algo MUY interesante. ¿Recuerdan que cuando "tronó" la ejecución vimos que EIP contenía 43434343? Esto sucedió porque EIP (lado derecho) contenía dicha "dirección inválida". Pero qué contendría 22FF3C en una ejecución "normal", veamos:
¡Un RETurn! Se supone que EIP debería tener esta dirección en una condición normal pero como pudimos llegar a "controlar" EIP, llenando el buffer y además sobrescribiendo EBP, pues bueno, hasta acá andamos.
La pregunta del millón:
¿Qué pasa si en vez de que "truene" con 43434343 lo re direccionamos a alguna dirección de nuestra elección?
Prosigamos... Ah, se me olvidaba. Los bytes antes del RET son: 42
5. Desborda la variable pass para modificar el flujo del programa y se ejecuta el comando:
printf("Access granted!\n"); No necesariamente saltando a esa dirección, pero si ejecutando la función printf()
¡HEMOS LLEGADO! ¡Por fin! Ya me duele el cuello, la espalda, los ojos, las rodillas, las piernas.. Ya estoy viejo XD ]¬)
Ya tenemos prácticamente todo listo. Hagamos check-list:
- Tenemos el número exacto de bytes requeridos para controlar EIP
- Al buen amigo Python, listo para aventar el veneno. XDD Jajaja
Ahora, investiguemos en el Immunity Debugger en qué dirección se encuentra nuestra "persona de interés"
...Y aquí está (401407), el dichoso mensajito "Access granted!".
Regresemos al script en Python y hagamos una pequeña modificación:
Como verán, los últimos 8 bytes (que originalmente eran BBBB y CCCC respectivamente) ahora son 77C1196D y 401407.
401407 es a donde queremos saltar y por consiguiente EIP contendrá esta dirección válida en el programa al momento de "explotar" la vulnerabilidad.
En este caso, 77C1196D es utilizado como trampolín (RETN) para que al momento de que se brinque a 401407 tenga la posibilidad de salir de manera "limpia".
NOTA: Hay que estar vivos al momento de generar el payload para prevenir cualquier problema relacionado s los Null-bytes. Pero esto será una tarea para ustedes y que sería bueno lo puedan debatir en los foros.
Ahora, el momento de la verdad, ¡ejecutemos el script modificado!
Agradecimientos:
A Boken (por el challenge) a Ricardo y Nahuel por sus guías y a toda la banda de CLS.
Dudas, sugerencias, correcciones o saludos: chr1x@izpwning.me o @chr1x
Para descargar el código asociado y el post en formato PDF pueden hacerlo desde mi Github: http://github.com/chr1x
¡COMENTEN!
Para descargar el código asociado y el post en formato PDF pueden hacerlo desde mi Github: http://github.com/chr1x
¡COMENTEN!
Esta bien como esta explicado, gracias. Espero otros post como estos.
ReplyDeleteGracias Anonymous por tu comentario. Decidí irme por una explicación a detalle ya que considero que es mas fácil para los iniciados en el tema. Sin duda los próximos posts serán tocando temas relacionados e intentaré explicarlos de manera detallada. ¡Gracias nuevamente!
DeleteSiempre he dicho que se deben dar las gracias como mínimo quien aporta algo de conocimiento hacia los demás. Muy bien explicado y muchas gracias por el aporte.
ReplyDeleteCada uno de nosotros fuimos ayudado en algún momento. En mi caso, he aprendido de varios recursos desde hace mucho tiempo pero sin duda, gran parte ha sido gracias a los tutoriales de Ricardo Narvaja. Obviamente la cuestión es reforzar áreas, con esos tutoriales reforzas la parte de Reversing, Ensamblador y el "skill" al resolver crackmes o escribir tutes, y hay otros temas (Exploiting, Malware) de los cuales existen otro tipo de recursos; sin embargo, todo el conocimiento adquirido se cruza en algún momento. Espero conseguir mas tiempo para seguir escribiendo. Abrazo y muchas gracias.
DeleteBuen post man! Saludos.
ReplyDeleteMuchas gracias, Abrazo!
DeleteLo mas hermoso que he visto, muy bien explicado. Excelente aporte.
ReplyDeleteSaludos.
¡Wow! ¡Gracias César! Te invito a que te adentres al área de Ingeniería Inversa y verás lo hermoso que es estar entre los meros bits & bytes. Al principio te sentirás abrumado, pero eventualmente todo se volverá mas claro y fácil. ¡Toma el reto! Abrazo
Delete