$ cat writeup.md…
$ cat writeup.md…
uiuc2026
Task: A static stripped ELF64 hides a fragmented image behind an obfuscated setup VM and a distracting custom hash. Solution: Trace the VM, capture each reconstructed byte by destination index, open the resulting PNG, and verify its text with the real ELF.
The curtain has fallen on Evernight, but March 7th's camera kept one last memory. Oblivion scattered it behind the Veil, and no single reflection shows the whole truth. Return every fragment to the place where it belongs. When the whole picture comes into focus, the memory itself will be the key that lets the mirrored soul remember.
The artifact is evernight, a statically linked, stripped x86-64 ELF. The goal is to recover the 29-byte input accepted by its checker.
The program's main routine is at 0x28cc70. It reads standard input, removes the newline, and ultimately requires exactly 29 bytes. A transform at 0x28cdf0 resembles a large custom sponge: it contains many mixed Boolean-arithmetic expressions, constants, rotations, and rounds.
That transform is a distraction rather than the shortest solution. Direct inversion, Z3 modeling, phrase guessing, raw .rodata searches, extraction of immediate constants, reconstruction of the recursive MBA grammar, and pixel-LSB searches did not produce the accepted input.
The challenge description instead points toward assembling scattered image fragments. The useful code is setup_vm at 0x28cda0, which runs before the apparent hash checker.
The VM stores 23 64-bit virtual registers in a stack array. Instrumenting the byte-read instruction at native RIP 0x2a0474 records 391,632 reads: exactly two source reads for each of 195,816 reconstructed output bytes.
The VM processes 765 chunks. There are 764 chunks of 256 bytes and one final chunk of 232 bytes. Register profiling gives the essential placement semantics:
| Register | Meaning |
|---|---|
r7 | Destination chunk number; across all chunks it is an exact permutation of 0..764 |
r8 | Offset within the current chunk |
r10 | Reconstructed output byte |
r12 | Absolute output index, r7 * 256 + r8 |
r22 | VM bytecode program counter |
Tracing decoded instructions at native address 0x29a858 reveals the byte loop:
0x75 loads source byte A into r10.0x75 loads source byte B into r11.0x43 XORs the two values.0xe4 computes a position-dependent mask K(r7, r8).0x43 leaves r10 = A XOR B XOR K.r12 = r7 * 256 + r8.0x31 consumes the reconstructed byte.This makes opcode 0x31 the ideal observation point: both the final byte and its final destination are already available. There is no need to reimplement the mask or understand the VM's later state updates.
0x31The local oracle.py maps the ELF into Unicorn, resolves static IFUNCs, runs the initialization decryptor, and provides a helper for invoking setup_vm. The following reduced extractor uses that loader and hooks the VM dispatcher immediately after it fetches an opcode:
#!/usr/bin/env python3 import struct from pathlib import Path import oracle from unicorn import UC_HOOK_CODE from unicorn.x86_const import UC_X86_REG_RBP SETUP_DISPATCH = 0x29A858 TOTAL = 195_816 uc = oracle.make_uc() ctx = oracle.SCRATCH + 0x5000 uc.mem_write(ctx, bytes(0x100)) output = bytearray(TOTAL) seen = bytearray(TOTAL) def capture(uc, address, size, user_data): rbp = uc.reg_read(UC_X86_REG_RBP) # The dispatcher has fetched the current opcode into this stack byte. if uc.mem_read(rbp - 0xF1, 1)[0] != 0x31: return regs = struct.unpack( "<23Q", bytes(uc.mem_read(rbp - 0xE8, 23 * 8)) ) position = regs[12] value = regs[10] & 0xFF assert position < TOTAL and not seen[position] output[position] = value seen[position] = 1 uc.hook_add( UC_HOOK_CODE, capture, begin=SETUP_DISPATCH, end=SETUP_DISPATCH, ) assert oracle.call(uc, oracle.SETUP, [ctx]) == 1 assert all(seen) Path("recovered.png").write_bytes(output) print(f"captured {sum(seen)} unique bytes") print(output[:8].hex())
Run it from the challenge directory, where evernight and oracle.py are present. It captures 195,816 unique positions. The first eight output bytes are:
89504e470d0a1a0a
That is the PNG signature. The earlier observation that 195816 = 328 * 199 * 3 was merely a numerical coincidence; treating the stream as raw RGB produces noise because the stream is a complete compressed PNG file.
The recovered payload has these properties:
90a48b7c4c7f3e122a8f8a38ee2a621adf4e516b3fdce0d6007bf925049f42dfOpening the PNG shows an Evernight image with the accepted flag handwritten over it.
The final authority is the original, unmodified executable. Supply the transcription from the image without a newline-changing shell transformation:
printf '%s' 'uiuctf{REDACTED}' | \ docker run --rm -i --platform linux/amd64 \ -v "$PWD":/w -w /w debian:12-slim /w/evernight
The program reaches its success branch and prints:
The mirrored soul remembers.
fastoracle.py and oracle.TARGET rejected the correct transcription. A full Unicorn setup-plus-transform run produced digest e7a056a684e7b4a13cb284d4057de19c40b5362641d61c869a4ac3f2c0ab52fb, whereas the modeled target began 65fb23f2. The cause of that discrepancy was not proven, so the safe conclusion is only that those extracted validation values were stale or incorrect. The real ELF's success path is authoritative.trace_vm_load_states.py and vm_load_states_report.txt: all 23 register profiles and the 391,632 paired reads at 0x2a0474.trace_vm_instructions.py and vm_instruction_trace.txt: decoded opcode sequence and register changes.capture_vm_plaintext.py: concise opcode-0x31 extraction implementation.reconstruct_vm_fragments.py: chunk counts, r7 permutation evidence, and earlier fragment-combination experiments.vm_plaintext/plain.bin and vm_plaintext/recovered.png: the exact reconstructed PNG and viewable copy.oracle.py and fastoracle.py: Unicorn instrumentation and the stale-validator debugging pitfall.$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar