- Security Engineer en Brightsight. Alumno de la 3ª edición del Máster en Seguridad Ofensiva del Campus Internacional de Ciberseguridad
- Security Engineer en Brightsight. Alumno de la 3ª edición del Máster en Seguridad Ofensiva del Campus Internacional de Ciberseguridad
Las vulnerabilidades de tipo stack buffer overflow o desbordamiento de buffer en la pila son un problema recurrente en seguridad de software, el cual parece no tener fin.
El software sensible a este tipo de problemas está muy extendido y, a menudo, a cargo de operaciones críticas, p.e. el kernel de un sistema operativo. Los métodos de ataque y defensa han estado compitiendo por más de 40 años, haciéndose cada vez más sofisticados: canarios, prevención de ejecución de datos (DEP), programación orientada al retorno (ROP), aleatoriedad en la disposición del espacio de direcciones (ASLR), etc.
En este artículo vamos a explorar una de las maneras en que la instrumentación dinámica de binarios (DBI) puede ayudar a la hora de proteger programas vulnerables. Para una explicación más completa y detallada, animo al lector a consultar el repositorio git retShield, creado para este TFM. En él, se puede encontrar además un método adicional para utilizar instrumentación dinámica como mecanismo de protección.
El problema de desbordamiento de buffer nace al copiarse datos en un buffer de tamaño limitado sin tener control sobre cuántos bytes se escriben en él. A continuación se muestra un fragmento de un programa vulnerable. En él, la función main recibe un dato del usuario y la pasa a la función processData, donde ese dato se copia a la variable local buffer , a la que se asignan 64 bytes. Al tratarse de una variable local, buffer se encuentra en la pila.
void processData(char *data) {
char buffer[64];
strcpy(buffer, data); // <---------------- BoF!
printf("Data processed: %s\n", buffer);
}
int main(int argc, char** argv) {
// (...)
processData(input);
// (...)
}
|
Observemos ahora el efecto de pasarle a la función processData una entrada maliciosa. Fijémonos en una primera foto de la pila, correspondiente al momento en que se va a llamar a la función strcpy. En la cima de la pila se encuentra la dirección de memoria de la variable buffer: 0xffffcd60. En la parte baja de la imagen podemos ver los 64 bytes de espacio asignado en la pila para el buffer, a partir de la dirección 0xffffcd60, y un poco más abajo, la dirección de retorno desde processData a main: 0x565556a9.
La segunda foto de la pila corresponde al momento en que la función strcpy ha terminado de ejecutarse y se regresa a la función processData. Como puede verse, se han seguido copiando datos más allá de los 64 bytes asignados, se ha desbordado el buffer. Como consecuencia, la dirección de retorno a main ha cambiado a 0xf7f5674b, que ha sido colocada ahí por el atacante.
En este punto se ha conseguido redirigir el flujo de ejecución, lo cual puede significar ejecución de código arbitrario. En la siguiente imagen se muestra una shell con privilegio de root obtenida como resultado de la explotación.
Hagamos un pequeño paréntesis para explicar el concepto de instrumentación dinámica, para después centrarnos en cómo esto puede ayudar para desbaratar un exploit como el que acabamos de describir.
Instrumentación dinámica de binarios
La instrumentación dinámica de binarios, o dynamic binary instrumentation (DBI) en inglés, puede definirse como el proceso de modificación de las instrucciones de un binario mientras se está ejecutando, a través de la inyección de código en memoria.
Frida
Una de las herramientas de instrumentación dinámica más populares en los últimos años es Frida, es utilizada por desarrolladores, profesionales de ingeniería inversa, e investigadores de seguridad. Frida es potente, flexible y fácil de usar. Se puede trabajar mediante scripts, es multiplataforma, software libre, y está ampliamente testeado. No es casualidad que numerosos proyectos y herramientas se hayan desarrollado por encima de Frida, pues ésta proporciona una excelente sobre la que crear.
Interceptor
El Interceptor de Frida permite, entre otras cosas, colocar hooks en funciones y definir callbacks donde podemos definir acciones para realizar antes y después de que la función «hookeada» se ejecute. Las acciones a realizar antes se definen en la callback onEnter y las acciones a realizar después se definen en la callback onLeave.
El código en Javascript para usar el Interceptor tendría una estructura como esta, donde target podría ser la dirección de una función o un símbolo:
Interceptor.attach(target, {
onEnter(args) {
// acciones a realizar antes de ejecutarse el target
// (...)
},
onLeave (retval) {
// acciones a realizar después de ejecutarse el target
// (...)
}
});
|
Blindando la dirección de retorno mediante hooking
En el momento en que la función strcpy se va a empezar a ejecutar, la dirección de retorno de strcpy a processData se encuentra en la cima de la pila. Unas cuantas posiciones más abajo, se encuentra la dirección de retorno de processData a main. Nótese que, en este momento en que no se ha ejecutado aún ninguna instrucción destrcpy, el registro EBP sigue apuntando a la base del marco de pila de processData, es decir, justo encima de la dirección de retorno a main.
Cuando hablábamos antes del desbordamiento de buffer, diferenciábamos dos momentos clave: (1) el momento antes de que strcpy se ejecutara, y (2) justo después de que se ejecutara. Estos dos momentos aparecen representados en la siguiente imagen: arriba el antes, con la dirección de retorno a main legítima: 0x565556a9; abajo el después, con la dirección de retorno maliciosa, colocada a través del exploit: 0xf7f568ab. ¿Cómo podríamos evitar esto? Si fuésemos capaces, de algún modo, de actuar en esos dos momentos, podríamos leer los 4 bytes de la dirección de retorno, almacenada en 0xffffcd9c en el primer momento, y volver a hacer lo mismo en el segundo momento. Si los valores leídos difiriesen, habríamos detectado el desbordamiento de buffer y podríamos abortar la ejecución del programa. En cuanto a la dirección donde se almacena la dirección de retorno a main, ésta podría obtenerse a partir del registro EBP, como hemos indicado anteriormente.
Así que tenemos dos momentos en los que nos gustaría poder actuar… ¿Podemos? Si nos retrotraemos a lo que hemos visto antes sobre Frida y el Interceptor, nos damos cuenta de que sí. El Interceptor nos daba precisamente la oportunidad de actuar antes y después de que se ejecutara la función en la que colocábamos el hook. Esto lo podemos hacer escribiendo nuestro código en las callbacks onEnter y onLeave, respectivamente.
En el código mostrado a continuación podemos observar una implementación minimalista del algoritmo explicado antes:
- En el bloque onEnter se recupera, a partir del registro EBP, la dirección donde se encuentra la dirección de retorno a main: this.callerRetAddrPtr (llamada &a en el dibujo anterior). Desreferenciando el puntero this.callerRetAddrPtr, se obtiene la dirección de retorno a main y se almacena en la variable this.originalCallerRetAddr.
- En el bloque onLeave, se desreferencia de nuevo el puntero this.callerRetAddrPtr y el valor obtenido se compara con el this.originalCallerRetAddr, almacenado en el bloque onEnter.
Interceptor.attach(Module.getExportByName(null, 'strcpy'), {
onEnter(args) {
this.callerRetAddrPtr = this.context.ebp.add(4);
this.originalCallerRetAddr = Memory.readPointer(this.callerRetAddrPtr);
},
onLeave (retval) {
var callerRetAddrBeforeRet = Memory.readPointer(this.callerRetAddrPtr);
if (this.originalCallerRetAddr.toString() !== callerRetAddrBeforeRet.toString()){
// abort
}
}
});
|
En la siguiente captura, podemos observar cómo la aplicación de esta estrategia neutraliza el exploit, que no devuelve una shell.
Si quieres ver el proyecto completo de Gerardo Pinar Loriente, rellena el formulario y te lo mandamos a tu correo electrónico.
¿Quieres obtener el mismo conocimiento que Gerardo?