Category: Binary Exploitation
Flag: texsaw{ezpzlmnsqzy_didyoulikethepaper?_23948102938409}
Challenge Description
Do you like threaded interpreters? https://www.charles.systems/publications/SCROP.pdf
Analysis
The shipped files make the intended execution pipeline very clear: a Rust compiler reads a Scheme-like language from standard input, the Python assembler turns the emitted assembly mnemonics into 16-byte instructions, and the interpreter binary executes that bytecode. The important detail is in the assembler source: every mnemonic is converted into a hard-coded address such as LOAD -> 0x10AD000, ADD -> 0x0ADD000, and DONE -> 0x0D0D0000. That means the instruction stream is not an abstract opcode stream at all; it is a sequence of raw code pointers plus immediates. The SCROP paper in the challenge description is the hint that this design can be exploited directly.
Before doing anything fancy, it helps to confirm the toolchain actually works as expected with a harmless program.
echo "(+ 1 2)" | ./compiler/target/debug/compiler | python ./assembler/main.py | timeout 5 ./interpreter3That same sanity check also worked against the remote service, which showed that the network endpoint was just running the interpreter on whatever bytecode we sent it.
timeout 45 bash -c 'echo "(+ 1 2)" | ./compiler/target/debug/compiler | python ./assembler/main.py; sleep 3' | nc 143.198.163.4 19003At that point the solve becomes a matter of following the SCROP model. The solve log showed that the interpreter consumes 16-byte instructions made of an 8-byte opcode address and an 8-byte immediate, and that the interesting target inside the static binary is the flag-reading routine at 0x8008570. That routine eventually reaches an execve wrapper and uses the embedded /bin/cat and Yflag.txt strings, so landing there should print the flag.
The only wrinkle is the interpreter’s stack setup. After initialization, rsp is repointed at the read-only bytecode buffer while a separate writable data stack is kept elsewhere. Jumping straight to 0x8008570 therefore crashes immediately because the first instruction is push %rbp, which tries to write to a read-only stack. The nice part is that the function does not actually need that prologue write for the rest of its work. Entering one byte later at 0x8008571 skips the push, keeps the remaining instructions valid, and still reaches the execve call with the baked-in argv data.
Because the interpreter treats the first 8 bytes of our input as the next code pointer, the exploit is just one forged instruction whose opcode field is 0x8008571 and whose immediate field is zero.
Solution
timeout 60 bash -c 'echo -ne "\x71\x85\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; sleep 5' | nc 143.198.163.4 1900texsaw{ezpzlmnsqzy_didyoulikethepaper?_23948102938409}