Description
Can you conjure the right bytes? Download app.py and the compiled spellbook binary, then reconstruct the required payload.
Setup
Download app.py and the compiled spellbook binary.
Read app.py and analyse the binary - both are needed to understand the full validation.
cat app.pyfile spellbookchmod +x spellbookSolution
Walk me through it- Step 1Read the source, confirm non-PIEapp.py asks for the raw 4-byte little-endian address of one of four named functions, for 3 random rounds:
ember_sigil,glyph_conflux,astral_spark,binding_word.file spellbookshould printELF 64-bit ... not strippedwith no "PIE" in the output, which is what makes the symbol addresses fixed.bashcat app.pybashfile spellbook # expect 'ELF ... not stripped' and NO 'PIE'Learn more
This challenge introduces function addresses in binary files. Every named function in a compiled ELF or PE binary has a fixed address determined at link time (for non-PIE binaries). This address is stored in the binary's symbol table - a data structure that maps function names to their memory addresses.
The server asks for the address in little-endian 4-byte format. Little-endian means the least significant byte comes first. For example, if a function is at address
0x08048420, the 4-byte little-endian representation is\x20\x84\x04\x08. This byte order is used by x86 and x64 processors, and is why exploit payloads in binary exploitation always write addresses in this format.The concept of sending a raw function address connects directly to return-oriented programming (ROP) and buffer overflow exploitation, where an attacker overwrites a saved return address on the stack with the address of a desired function or gadget. Mastering address formats and byte ordering is a fundamental prerequisite for binary exploitation.
- Step 2Read all four function addresses from the binaryUse pwntools'
ELFto read the symbol table. Ifelf.symbols['ember_sigil']raisesKeyError(binary is stripped), fall back to harvesting addresses in Ghidra or radare2.pythonpython3 - <<'EOF' from pwn import ELF, p32 elf = ELF("./spellbook", checksec=False) funcs = ["ember_sigil", "glyph_conflux", "astral_spark", "binding_word"] try: addrs = {name: elf.symbols[name] for name in funcs} except KeyError as e: raise SystemExit(f"Symbol missing ({e}); binary is stripped, recover addresses in Ghidra/r2 manually.") for name, addr in addrs.items(): # p32() is little-endian 4-byte pack: 0x08048420 -> b'\x20\x84\x04\x08' print(f"{name}: {hex(addr)} -> {p32(addr).hex()}") EOFLearn more
pwntools is the standard CTF binary exploitation library for Python. Its
ELFclass parses an ELF binary and provides convenient access to symbols (elf.symbols['name']), GOT/PLT addresses, sections, and more. Thep32(addr)function packs a 32-bit integer into 4 bytes in little-endian order - equivalent tostruct.pack('<I', addr).A PIE (Position Independent Executable) binary has all addresses randomized by ASLR at load time - its symbol addresses in the ELF file are relative offsets from the base. A non-PIE binary has fixed absolute addresses, making it possible to precompute function locations exactly. You can check with
checksec --file=binaryor by inspecting the ELF header'se_typefield (ET_EXEC for non-PIE, ET_DYN for PIE).The four function names in this challenge (ember_sigil, glyph_conflux, astral_spark, binding_word) are custom symbols added by the challenge author. In real binaries, stripped of debug symbols, function names are unavailable - recovery requires heuristic analysis (function prologue patterns, cross-reference analysis) in tools like Ghidra, which can partially reconstruct symbol names from library call patterns.
- Step 3Write the solve scriptConnect, read each round's prompt, match the requested function name, and send its 4-byte little-endian address. Use r.send (not sendline) - the protocol expects exactly 4 raw bytes; an extra '\n' makes it 5 and fails. Print one received line first to confirm the quote style before relying on the in check.python
python3 - <<'EOF' from pwn import * HOST, PORT = "<HOST>", <PORT_FROM_INSTANCE> elf = ELF("./spellbook", checksec=False) funcs = ["ember_sigil", "glyph_conflux", "astral_spark", "binding_word"] addrs = {name: elf.symbols[name] for name in funcs} r = remote(HOST, PORT) r.recvuntil(b"unlock the flag.") # banner for _ in range(3): line = r.recvuntil(b"==> ", timeout=5).decode() print("DEBUG round prompt:", repr(line)) # confirm quote style on first run # The parsing is brittle: if app.py prints "ember_sigil" without quotes, # adjust the substring test (e.g., drop the quotes). for name in funcs: if f"'{name}'" in line: r.send(p32(addrs[name])) # send, NOT sendline - exact 4 bytes break print(r.recvall(timeout=3).decode()) EOFLearn more
The
recvuntil(delimiter)method reads bytes from the socket until the delimiter appears, then returns everything including the delimiter. This is the standard way to synchronize with a server that sends multiple prompts - you wait for a known marker before sending your response, ensuring correct alignment in the conversation.The
r.send(payload)call (withoutline) sends raw bytes without a newline. This is important here because the server expects exactly 4 raw bytes - adding a newline would make it 5 bytes and fail the comparison.sendline()adds\nautomatically and is appropriate for text-based protocols, whilesend()is for binary protocols where exact byte counts matter.Iterating over 3 random rounds with the correct function each time demonstrates the power of pre-computation: you extract all four addresses once before connecting, then look them up by name during the interactive session. This pattern appears in time-limited challenges where the server demands a correct answer within milliseconds - you cannot afford to analyze the binary after connecting, so all analysis happens offline beforehand.
Flag
picoCTF{byt3m4ncy_3_...}
app.py randomly selects 3 of 4 functions (ember_sigil, glyph_conflux, astral_spark, binding_word) from the spellbook binary and asks for their raw 4-byte little-endian addresses. Use pwntools ELF to read symbol addresses and send them per round.