Hace unas semanas publicamos un desafío relacionado con la explotación de un buffer overflow sencillo en Linux. En la solución publicada se observaba cómo era posible cambiar el flujo de ejecución del programa vulnerable, al ser sobrescrito el puntero a la próxima instrucción como consecuencia del desbordamiento del buffer. Hoy vamos a mostrar la explotación de otra clásica vulnerabilidad de tipo buffer overflow, en la que se pisa el registro ebp para lograr la ejecución de código arbitrario.
Arriba vemos el código, del que podemos mencionar al menos dos errores de programación. Primero, el bucle de copia siempre va a mover la misma cantidad de bytes, independientemente de la longitud de la cadena a copiar. Segundo, y más importante, la comparación del bucle es errónea: el buffer tiene 256 bytes y el bucle copia 257, pisando el byte menos significativo de lo que le sigue inmediatamente en memoria (el valor de ebp almacenado de main).
Como se observa en el esquema anterior, luego de ebp se encuentra almacenada la dirección de retorno. La idea de la explotación de esta vulnerabilidad consiste en modificar ebp para que apunte a una parte del buffer de donde se va a leer una dirección de retorno que, a su vez, apunte al payload dentro del mismo buffer.
En el desensamblado vemos que luego de salir del bucle de copia (pisando ebp) se ejecutan dos instrucciones: leave y ret. La instrucción leave restablece el stack frame de main(); leave es equivalente a mov %ebp,%esp y pop %ebp, es decir, lo contrario a las primeras dos instrucciones de func(). Al haber alterado ebp, la instrucción ret va a tomar el valor de eip desde otra ubicación, y no la ubicación original en el stack.
Si hacemos la prueba de correr la aplicación con 256 letras “A” como argumento, ebp se pisa con el byte nulo de fin de cadena (el byte 257), produciendo que la dirección de retorno sea tomada del buffer lleno de letras “A”, generando un error al no existir la dirección 0x41414141 (“A” = 0x41).
Si colocamos un breakpoint inmediatamente después de que se almacena el valor original de ebp en el stack, vemos que este es 0xbffff3f8; también vemos que le sigue en memoria la dirección de retorno original en main(), 0x08048261. Luego colocamos un breakpoint antes de que se ejecute la instrucción leave y observamos que el valor almacenado de ebp ahora es 0xbffff300.
Es importante notar que el buffer se encuentra entre 0xbffff2ec y 0xbffff3ec, con lo cual la dirección modificada de ebp es útil, ya que se encuentra dentro del buffer. Luego de que leave se ejecute, podemos comprobar que apunta a la dirección deseada:
Para construir el shellcode vamos a:
- Colocar bytes basura desde 0x2ec hasta 0x300, es decir 20 bytes de relleno. Podemos colocar NOP (0x90), por ejemplo.
- A 0x300 va a apuntar nuestro nuevo ebp, por lo que rellenamos con 4 bytes más de basura.
- En 0x304 colocamos la dirección de la shell dentro de nuestro buffer, por ejemplo 0xbffff308.
- En 0x308 empieza la shell.
- El resto de los 256 bytes se rellena.
Luego de la ejecución vemos que se abre la shell dentro de gdb.
En próximas entregas estaremos viendo técnicas de complejidad creciente.