$ cat writeup.md…
$ cat writeup.md…
UIUCTF 2026
Task: A stripped x86-64 ELF embeds LuaJIT bytecode and a virtualized finite-field flag checker. Solution: Carve and inspect the bytecode, invert finale, trace its fixed F_257 circuit, and replay all operations backward.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
Sparkle has reserved the stage for one final performance. Every mask hides another stage, and every actor moves with the whole cast. Find the face that earns the audience's applause.
The supplied stripped ELF accepts one line and prints an applause message only for the correct input. The objective is to recover that input despite several layers of LuaJIT, wasm2lua, and virtualized finite-field logic.
Initial inspection identified a 64-bit PIE ELF containing LuaJIT 2.1 runtime strings and the chunk name @curtain. The embedded bytecode starts at file offset 442592; carving 131403 bytes produces a clean chunk containing 115 prototypes:
from pathlib import Path elf = Path("sparkle").read_bytes() Path("curtain_clean.ljbc").write_bytes(elf[442592:442592 + 131403])
Disassembling it with the locally built LuaJIT and decompiling it showed wasm2lua-generated code. The module exports a function named finale. Its bytecode, preserved in func6_bytecode.txt, rejects any length other than 72 and compares 72 transformed words with a data segment at linear-memory offset 4096.
tools_luajit/src/luajit -bl curtain_clean.ljbc > curtain_bytecode.txt
The static inversion in solve2.lua reverses the 16 finale rounds and their word permutation. This yields required_u16.bin: 72 little-endian field values (144 storage bytes) that the outer layer must place in memory before calling finale(0, 72). Thus the apparent 72-byte requirement belongs to finale, not directly to stdin.
LuaJIT debug upvalue introspection made the otherwise hidden runtime objects reachable. The important pattern is:
local bc = assert(io.open("curtain_clean.ljbc", "rb")):read("*a") local checker = assert(loadstring(bc, "@curtain"))() local _, vm = debug.getupvalue(checker, 1) local _, objects = debug.getupvalue(vm, 3) local finale = objects[349][1] local state = objects[349][10]
...
$ grep --similar