La semana pasada, desde el Laboratorio de Investigación de ESET Latinoamérica, abrimos un nuevo reto relacionado con la explotación de una aplicación vulnerable en Linux. Hoy tengo el agrado de mostrarles la respuesta al Desafío ESET #32.
El primer paso consiste en analizar la aplicación vulnerable, para así poder explotarla. Para ello, vamos a abrir el ejecutable con gdb:
Podemos ver que la aplicación tiene información de debug; en particular nos interesa su única rutina de usuario: main. Si pedimos a gdb que nos muestre el disassembly de main, obtenemos:
Del código observamos que se reservan 64 bytes (40h) para variables locales en el stack. También vemos que se llama a una función estándar y se le pasa uno de los argumentos con que se ejecutó la aplicación y un puntero a ese buffer de 64 bytes en el stack. No sabemos qué hace esa rutina estándar, pero cuando colocamos un breakpoint y corremos la aplicación, gdb nos muestra que esa rutina es realmente un strcpy que copia argv[1] a buf. Luego, si cerramos gdb y volvemos a debuggear con el argumento “string_prueba”, podemos verificar que esa cadena es copiada al stack:
Resulta evidente que, si se ejecuta la aplicación con un argumento de mayor longitud que 64 bytes, se va a pisar el stack, pudiendo alterar la dirección de retorno de main de tal modo de ejecutar código arbitrario. El código correspondiente (en C) es el siguiente:
Si compilamos el código de la shell, veremos que no es apto para usarlo como payload, dado que contiene bytes nulos que detendrían la copia de strcpy:
Hay varias formas de modificar el código para que sea útil. A continuación se muestra una posible solución que se me ocurrió (aunque no la más sencilla):
En forma resumida, se realizaron los siguientes cambios:
- El código de execve se deposita en AL y no EAX: así las instrucciones generadas no rellenan el valor 11 con bytes nulos.
- No se asigna el valor cero a ECX y EDX; se hace uso de xor.
- Para no depender de una dirección fija de memoria, la string “/bin/sh\x00” se construye en el stack y se copia ESP en EBX.
- Uso de la instrucción ROL para que la string termine en null (esta parte es optimizable, y hay soluciones más sencillas).
Al compilar se obtiene lo siguiente:
Y pegamos los opcodes en el archivo que servirá de input al programa vulnerable:
Vemos que el tamaño del payload es de 29 bytes. El resto de los 64 bytes del buffer se rellenará con instrucciones NOP. Luego se pisarán los 4 bytes correspondientes al frame pointer almacenado por main (EBP), con un valor cualquiera (en nuestro caso, escribimos “AAAA”). Como último paso, debemos escribir la dirección de memoria que pisará la dirección de retorno de main (EIP almacenado), por lo que acudiremos nuevamente al debugger:
Ahí vemos que, para la sesión de gdb, el buffer comienza en 0xbffff2f8. Por lo tanto, podemos elegir la dirección de retorno para que salte al medio del bloque de NOP, por ejemplo en 0xbffff309. Luego, modificamos nuestra shellcode agregando esa dirección (little endian).
Podríamos probar el exploit en gdb, pero vamos a correr la aplicación vulnerable fuera del debugger. Debemos, sin embargo, considerar una pequeña trampa: dado que no podemos garantizar el espacio de direcciones donde se va a cargar el ejecutable, dejando inservible la dirección elegida entre ejecución y ejecución, por simpleza desactivaremos ASLR.
La ejecución falla porque la dirección base del buffer en el stack ha cambiado, pero podemos activar un dump completo y revisar si el shellcode quedó por debajo o encima:
Se observa que la dirección base del buffer está en 0xbffff328. Salimos de gdb, actualizamos la dirección a la mitad del bloque de NOP y corremos nuevamente la aplicación vulnerable:
Ha funcionado el buffer overflow y se ha abierto una shell bajo el mismo usuario, sin privilegios. Está claro que si la aplicación vulnerable tuviese mayores privilegios, también los tendría la shell.
Concluido el reto, felicitamos a @nitr0usmx por haber resuelto el desafío con éxito y resultar ganador de las tres licencias de ESET Smart Security; pueden ver su solución en la sección de comentarios. También felicitamos a Emiliano por su participación: aunque su solución llegó a buen puerto, faltaban algunas explicaciones para que pudiera probarse.
¡Hasta la próxima!