Veil of Evernight
Veil of Evernight
Platform: Uiuc2026 | Category: Reversing | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-08-08 | Status: Solved Techniques: dynamic_vm_tracing, unicorn_instrumentation, vm_register_recovery, payload_reconstruction
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
uiuc2026| ID:20260808_uiuc2026_veil_of_evernight - Tags: elf64, png, custom_vm, static_binary, stripped_binary, mba_obfuscation
- Indicators: setup_vm at 0x28cda0, 23 virtual registers, 391632 reads at 0x2a0474, opcode 0x31 consumes reconstructed bytes
- Source:
20260808_uiuc2026_veil_of_evernight.md
Foothold
Vulnerability / Misconfiguration
- Dynamic_vm_tracing
- Unicorn_instrumentation
- Vm_register_recovery
- Payload_reconstruction
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- dynamic_vm_tracing
- unicorn_instrumentation
- vm_register_recovery
- payload_reconstruction
- Tags: elf64, png, custom_vm, static_binary, stripped_binary, mba_obfuscation
Original Writeup
<details><summary>Click to expand original content</summary>Description
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.
Analysis
Avoiding the checker distraction
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.
Tracing the setup VM
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:
- Opcode
0x75loads source byteAintor10. - A second
0x75loads source byteBintor11. - Opcode
0x43XORs the two values. - Opcode
0xe4computes a position-dependent maskK(r7, r8). - Another
0x43leavesr10 = A XOR B XOR K. - The VM computes
r12 = r7 * 256 + r8. - Opcode
0x31consumes 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.
Solution
Capture the bytes consumed by opcode 0x31
The 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:
- Format: valid PNG, 466 by 341 pixels
- Color: 8-bit RGB, non-interlaced
- Size: 195,816 bytes
- SHA-256:
90a48b7c4c7f3e122a8f8a38ee2a621adf4e516b3fdce0d6007bf925049f42df
Opening the PNG shows an Evernight image with the accepted flag handwritten over it.
Verify against the real ELF
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.
Pitfalls and Failed Approaches
- Inverting the apparent sponge: the deep, lossy custom transform and data-dependent indexing make direct inversion or a full Z3 preimage solve impractical.
- Treating MBA constants as image bytes: raw immediates, recursive grammar choices, pairwise combinations, and simple permutations rendered only noise. The image is reconstructed dynamically by the setup VM.
- Treating 195,816 bytes as raw RGB: the convenient factorization into three color channels is accidental. Checking the first bytes immediately identifies a PNG container.
- Trusting an instrumented validator over the executable:
fastoracle.pyandoracle.TARGETrejected the correct transcription. A full Unicorn setup-plus-transform run produced digeste7a056a684e7b4a13cb284d4057de19c40b5362641d61c869a4ac3f2c0ab52fb, whereas the modeled target began65fb23f2. 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.
Reproducibility Artifacts
trace_vm_load_states.pyandvm_load_states_report.txt: all 23 register profiles and the 391,632 paired reads at0x2a0474.trace_vm_instructions.pyandvm_instruction_trace.txt: decoded opcode sequence and register changes.capture_vm_plaintext.py: concise opcode-0x31extraction implementation.reconstruct_vm_fragments.py: chunk counts,r7permutation evidence, and earlier fragment-combination experiments.vm_plaintext/plain.binandvm_plaintext/recovered.png: the exact reconstructed PNG and viewable copy.oracle.pyandfastoracle.py: Unicorn instrumentation and the stale-validator debugging pitfall.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR