← Back to Writeups
HTBN/AReversing

Veil of Evernight

XESXOR8/23/20266 min read
#reversing#htb#n/a

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
PortServiceVersionNotes
<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

  1. Dynamic_vm_tracing
  2. Unicorn_instrumentation
  3. Vm_register_recovery
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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:

RegisterMeaning
r7Destination chunk number; across all chunks it is an exact permutation of 0..764
r8Offset within the current chunk
r10Reconstructed output byte
r12Absolute output index, r7 * 256 + r8
r22VM bytecode program counter

Tracing decoded instructions at native address 0x29a858 reveals the byte loop:

  1. Opcode 0x75 loads source byte A into r10.
  2. A second 0x75 loads source byte B into r11.
  3. Opcode 0x43 XORs the two values.
  4. Opcode 0xe4 computes a position-dependent mask K(r7, r8).
  5. Another 0x43 leaves r10 = A XOR B XOR K.
  6. The VM computes r12 = r7 * 256 + r8.
  7. Opcode 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.

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.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.

Reproducibility Artifacts

  • 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.
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR