A la hora de introducirse en el tema, es recomendable deshabilitar las técnicas de mitigación presentes por defecto en los sistemas modernos. A medida que se avanza con ataques más sofisticados es posible habilitar una a una estas mitigaciones para poner en práctica cómo subvertirlas y trabajar en el marco de escenarios más “reales”. Es por ello que se recomienda compilar los programas vulnerables con ciertos flags específicos que harán la tarea más fácil y se recomiendan ciertas configuraciones extras en el sistema.
Como el desbordamiento de búfer es una vulnerabilidad de larga data, los sistemas operativos actuales desarrollaron prevenciones para este tipo de ataques, entre otras la randomización del espacio de direcciones y la implementación de canaries de protección de la pila.
Por eso para introducir estos ataques es necesario deshabilitar una serie de configuraciones para, en una segunda instancia, explicar cómo superarlas. A continuación se hace un repaso de las mitigaciones y se usa el script checksec
para detectar cuando se encuentran habilitadas o no.
El ASLR (“Address Space Layout Randomization” o mapa de espacio de direcciones aleatorio) es una tecnología diseñada para impedir la explotación de ciertas vulnerabilidades. El ASLR vuelve aleatorias las direcciones de memoria de un proceso y por ende, dficulta la tarea de adivinar direcciones de la pila. Esta información es necesaria para la lectura y reescritura de sectores claves de la pila, para la inyección y ejecución de código malicioso y en el caso de técnicas de explotación más avanzadas, para la creación de ROP gadgets.
Es posible temporalmente deshabilitar esta protección desde la terminal configurando el espacio de memoria como “no random”.
user@abos:~$ sudo sysctl -w kernel.randomize_va_space=0 #no random
user@abos:~$ sudo sysctl -w kernel.randomize_va_space=1 #random
Es una política en relación a la memoria que indica que nunca se debe tener una página de memoria escribible y ejecutable al mismo tiempo (representado como write XOR execute o W^X). Es por ello que se marcan los sectores escribibles dentro de las direcciones de un proceso como no ejecutables. La pila entonces deviene en un área de memoria no ejecutable.
Los ataques iniciales consistiran en parte en inyectar un código arbitrario en la pila (escribir en la pila) y ejecutarlo (ejecutar en la pila). Para ser capaces de escribir y ejecutar en la pila, es necesario inicialmente deshabilitar esta mitigación.
Por defecto gcc
compila con esta mitigación habilitada. Para deshabilitar protección de ejecución en la pila, al compilar con gcc
agregamos el flag -z execstack
.
user@abos:~$ gcc -o no-exec programa.c
user@abos:~$ ./checksec.sh --file no-exec
NX ... FILE
NX enabled ... no-exec
user@abos:~$ gcc -z execstack -o exec programa.c
user@abos:~$ ./checksec.sh --file exec
NX ... FILE
NX disabled ... exec
Para prevenir técnicas de explotación que sobreescriben la Global Offset Table, se obliga al linker a resolver todas las funciones linkeadas dinámicamente al iniciarse el programa y, una vez actualizada la tabla GOT, volverla de sólo lectura de modo que no pueda ser sobreescrita de manera arbitraria.
Es por ello que en exploits que involucran la GOT es necesario que la mitigación RELRO (en inglés RELocation Read-Only) se encuentre deshabilitada o habilitada parcialmente con la variante “RELRO parcial”. Esta última opción es usada por defecto en la mayoría de distribuciones de Linux modernas, por lo que no es necesaria ninguna configuración extra.
No obstante en casos donde un ataque utilice la sección DTORS será necesario deshabilitarlo completamente con el flag -Wl,-z,norelro
.
user@abos:~$ gcc -o con-relro programa.c
user@abos:~$ ./checksec.sh --file con-relro
RELRO ... FILE
Partial RELRO ... con-relro
user@abos:~$ gcc -Wl,-z,norelro -o sin-relro programa.c
user@abos:~$ ./checksec.sh --file sin-relro
RELRO ... FILE
No RELRO ... sin-relro
En esta técnica de mitigación el compilador inserta una marca o canario en la pila cuando detecta una función que accede a variables locales por referencia.
En estos casos inmediatamente después de almacenar la dirección de retorno, se almacena a su vez un valor (el canario o la cookie) entre la variable local (datos) y la dirección de retorno (información de control).
Frente a un ataque de reescritura de la dirección de retorno, el valor del canario se verá modificado levantando alertas de que se produjo un desbordamiento de búfer. Y se detiene la ejecución del programa antes de que la función retorne a la dirección vulnerada por ejemplo con una excepción de violación de segmento.
En una primera instancia, de esta manera se evitaría una reescritura de la dirección de retorno de una función y la consecuente ejecución de código malicioso.
Dado que inicialmente se trabajará con técnicas de corrupción de la dirección de retorno en memoria es necesario deshabilitar esta protección de la pila compilando con gcc
con el flag -fno-stack-protector
.
user@abos:~$ gcc -o con-canary programa.c
user@abos:~$ ./checksec.sh --file con-canary
STACK CANARY ... FILE
Canary found ... con-canary
user@abos:~$ gcc -fno-stack-protector -o sin-canary programa.c
user@abos:~$ ./checksec.sh --file sin-canary
STACK CANARY ... FILE
No canary found ... sin-canary
-m32
para compilar ejecutables de 32 bits.-mpreferred-stack-boundary=2
para el alineamiento de la pila.-ggdb
habilita información de debugging-no-pie
para evitar compilar el binario como Position Independent Executable, sino cambiaría en cada ejecución la ubicación del código en el espacio de memoria virtual.Inicialmente se compilan con los flags mencionados previamente:
user@abos:~$ gcc -m32 -no-pie -fno-stack-protector -ggdb -mpreferred-stack-boundary=2 -z execstack -o abo1 abo1.c
No se utiliza el flag -Wl,-z,norelro
para deshabilitar RELRO
, dado que con una configuración de RELRO parcial
es posible avanzar sin problemas.
En muchos exploits es necesario hacer cálculos sobre direcciones de la pila (la dirección de variables, punteros, etc). Como gdb
agrega variables de entorno que se almacenan en la pila y la modifican, los cálculos sobre las direcciones obtenidas en gdb
no resultan útiles para explotar el binario por fuera de un entorno de debugging.
Por ejemplo, para ver las diferencias en variables de entorno:
user@abos:~$ /usr/bin/printenv
XDG_SESSION_ID=...
TERM=...
SHELL=...
(...)
user@abos:~$ gdb -q /usr/bin/printenv
XDG_SESSION_ID=...
TERM=...
SHELL=...
(...)
COLUMNS=111 ; env var de gdb
LINES=48 ; env var de gdb
Existen varias maneras de alinear la memoria dentro y fuera de gdb
.
Para limpiar las variables de entorno es posible ejecutar el programa con el wrapper env -i
. Como ejemplo podemos ver como se limpian las variables de entorno al ejecutar env -i /usr/bin/printenv
:
Printenv
user@abos:~$ env -i /usr/bin/printenv
user@abos:~$
No obstante, vemos que gdb
igualmente agrega variables de entorno:
user@abos:~$ env -i gdb /usr/bin/printenv
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
(gdb) r
COLUMNS=72
PWD=/home/usuarix/abos/stack1
LINES=34
env -i
Una posible solución es usar el wrapper env -i
para limpiar las variables de entorno tanto usando gdb
como a la hora de ejecutar el programa. Dentro de gdb
con show env
sabemos qué variables de entorno se mantienen y optar por sumarlas en la ejecución o eliminarlas de gdb
con unset env
.
Por ejemplo, ejecutamos printenv
con las variables de entorno que incluye gdb
que observamos recién (usando siempre el path
completo para ejecutar el programa como hace el debugger):
user@abos:~$ env -i COLUMNS=72 PWD=$(pwd) LINES=34 /usr/bin/printenv
COLUMNS=72
PWD=/home/usuarix/carpeta
LINES=34
O debugeamos printenv
con env -i
y eliminamos dentro de gbd
las variables de entorno que se mantienen con unset env
:
user@abos:~$ env -i gdb /usr/bin/printenv
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
(gdb) show env
LINES=34
COLUMNS=95
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) show env
(gdb) r
Starting program: /usr/bin/printenv
PWD=/home/usuarix/carpeta
[Inferior 1 (process 4546) exited normally]
Y si lo ejecutamos:
user@abos:~$ env -i PWD=$(pwd) /usr/bin/printenv
PWD=/home/usuarix/carpeta
Stack 1
Probamos la primer solución con el programa Stack 1 y observamos cómo las direcciones de buf
y cookie
son idénticas al ejecutar y debuggear el programa:
user@abos:~$ env -i LINES=34 PWD=$(pwd) COLUMNS=72 /home/usuarix/abos/stack1
buf: bffffd94 cookie: bffffde4
user@abos:~$ env -i gdb ./stack1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
>>> r
buf: bffffd94 cookie: bffffde4
Y sino, de otra manera, probamos sin variables de entorno en gdb
y vemos como logramos las mismas direcciones para las variables:
user@abos:~$ env -i PWD=$(pwd) /home/usuarix/abos/stack1
buf: bffffdb4 cookie: bffffe04
user@abos:~$ env -i gdb ./stack1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
(gdb) show env
LINES=34
COLUMNS=95
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) show env
(gdb) r
buf: bffffdb4 cookie: bffffe04
Si es necesario enviarle un input por stdin al programa vulnerable (que cuenta por ejemplo con una función como gets()
) esta solución también funciona:
user@abos:~$ python exploit.py | env -i PWD=$(pwd) /home/usuarix/abos/stack1
buf: bffffdb4 cookie: bffffe04
...
user@abos:~$ python exploit.py > in
user@abos:~$ env -i gdb ./stack1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
(gdb) show env
LINES=34
COLUMNS=95
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) show env
(gdb) r < in
buf: bffffdb4 cookie: bffffe04
Y también cuendo el programa vulnerable espera que se le pasen argumentos por ejemplo con una función como strcpy(buf,argv[1])
):
user@abos:~$ env -i PWD=$(pwd) /home/usuarix/abos/abo1 "$(./arg-exploit.py)"
buf: bffff558
user@abos:~$ env -i gdb ./abo1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
(gdb) show env
LINES=34
COLUMNS=95
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) show env
(gdb) r "$(./arg-exploit.py)"
buf: bffff558
Para facilitar esta configuración es posible editar el archivo .gdbinit
:
bash -c "echo 'unset env LINES' >> .gdbinit"
bash -c "echo 'unset env COLUMNS' >> .gdbinit"
fixenv
Otra solución es utilizar un script como Fixenv de Hellman que fija las direcciones del stack. Es posible ver el funcionamiento del script de Hellman (usando ./r.sh
como wrapper) con el programa Stack 1.
; sin las direcciones de la pila consistentes
user@abos:~$ ./stack1
buf: bffff6a4 cookie: bffff6f4
user@abos:~$ gdb ./stack1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
>>> r
buf: bffff654 cookie: bffff6a4
; con las direcciones de la pila consistentes
user@abos:~$ ./r.sh ./stack1
buf: bffff734 cookie: bffff784
user@abos:~$ ./r.sh gdb ./stack1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
>>> r
buf: bffff734 cookie: bffff784
A diferencia de la solución anterior con env -i
cuando el programa vulnerable toma un input por stdin, este script no resuelve las diferencias entre las direcciones con y sin gdb.
En cambio funciona correctamente cuando es necesario pasarle argumentos al programa vulnerable:
user@abos:~$ ./r.sh ./abo1 "$(./exploit.py)"
buf: bffff558
user@abos:~$ ./r.sh gdb ./abo1
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
>>> r "$(./exploit.py)"
buf: bffff558
Si bien en este caso nos ocupamos de compilar nosotrxs mismxs los binarios de los programas vulnerables, cuando se trabaja en estos temas es una buena práctica aislar en una máquina virtual la ejecución de binarios no conocidos.
[1]. Reece, Alex. (02 de mayo de 2013). Introduction to format string exploits [Post]. Code Arcana. Recuperado de http://codearcana.com/posts/2013/05/02/introduction-to-format-string-exploits.html