← Back to Writeups
CUSTOMMediumReversing

Три Лика Тени (Three Faces of Shadow)

XESXOR9/2/20264 min read
#reversing#custom#medium#binary_patching_detection#cexit_patching_detection#crt_exit_flow_analysis#idata_section_code_injection#red_herring_elimination#subtraction_cipher_reversal#windows

Три Лика Тени (Three Faces of Shadow)

Platform: hackerlab | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-04-06 | Status: Solved Techniques: binary_patching_detection, cexit_patching_detection, crt_exit_flow_analysis, idata_section_code_injection, red_herring_elimination, subtraction_cipher_reversal

Summary

Task: PE64 Windows binary with massive obfuscation — anti-debug, custom VM, fake flags, Braille art distractions, and obfusheader decoys — all red herrings. Solution: discovered patched _cexit CRT function jumping to hidden code in .idata section that decrypts the real flag via byte subtraction (0x69).

Три Лика Тени (Three Faces of Shadow) — Hackerlab

Description

Расклад "Три Лика Тени" — tarot-themed description with cryptic hints about looking beyond the obvious, fake protections, and hidden truths. Key hint: "Когда найдёшь все двери — стучи в стену" (When you find all doors — knock on the wall).

English summary: we are given vm_scary_task.exe — a PE32+ x86-64 Windows executable (MinGW GCC 8.1.0) and encrypted_flag.txt containing Braille Unicode art. The binary is packed with layers of obfuscation, anti-debugging, a custom VM, and multiple fake flags. The real flag is hidden in a patched CRT exit handler that runs after main() returns. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Step 1: Initial Recon — A Sea of Red Herrings

The binary contains an extraordinary amount of misdirection designed to waste the analyst's time:

Anti-Debug (AntiHacker_3000):

  • IsDebuggerPresent() check
  • FindWindowA() checks for: "ida64", "x64dbg", "dota2", "MyFriendPeppaPig", "PONY"
  • If debugger detected: XORs Braille art with key 0x42, prints insult, exits
  • Contains fake flag: WTF{Zapomni_chto_debug_eto_ne_lyubov}

Custom VM (Igraem_V_VM):

  • Instrukcia_VM struct: 8 bytes {int opcode, byte op1, byte op2, padding}
  • 4 opcodes: 0=ADD, 1=XOR, 2=CMP, 3=HALT
  • VM instructions: ADD regs[0]+input[0], XOR regs[1]^input[1], CMP regs[0] vs 0xAA, HALT
  • VM result is never used for any real validation — pure red herring
  • Contains fake flag: FSB{NE_Reshaite_Eto} ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Password Check (TUT_FLAG_BISTREE):

  • strcmp(input, "PONY") at 0x402fa9
  • If match: prints "Ura! Ti vzlamal etu zashitu!" — another dead end
  • Contains fake flag: WTF{Debug_vedet_k_rasstroystvam}

obfusheader Library:

  • 10 decoy functions that do nothing
  • Watermark hook (does nothing)
  • All numeric constants computed through elaborate arithmetic that always evaluates to 1
  • Custom alphabet abcdefghijklmnopqrstuvwxyzSLAIDP — stored but never used

Decorative Distractions:

  • DLYA_TEBYA (0x406060): Braille skull art with "FOR YOU ❤️"
  • Zmeika (0x406440): Braille snake art with "Are ya winning son?"
  • encrypted_flag.txt: Braille skull/dagger art — purely decorative, no hidden data
  • Fourth fake flag in .rdata: OMG{M2g@_VM_FLG_MOST2R} ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: The Key Insight — Patched _cexit

The task description hint — "When you find all doors — knock on the wall" — means: after exhausting all visible functions (main, VM, password check, anti-debug), look at what happens outside main().

The CRT (C Runtime) startup/exit flow in MinGW executables:

  1. _mainCRTStartup → calls main()
  2. After main() returns → calls _cexit() for cleanup
  3. _cexit() normally jumps to __imp__cexit (the imported CRT function)

Examining the _cexit function at 0x4054C8: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Normal _cexit:  jmp [__imp__cexit]    ; indirect jump to CRT cleanup
Patched _cexit: jmp 0x40B9D0          ; direct jump into .idata section!

The _cexit thunk has been patched to redirect execution into the .idata section — a section that normally only contains import address tables, not executable code.

Step 3: Hidden Decryption Code at 0x40B9D0

The injected code in the .idata section performs a simple decryption loop:

asm
0x40B9D0:  mov rax, 0x40B9B0      ; pointer to encrypted flag data
           mov rcx, 0              ; counter = 0
loc_loop:
           sub byte ptr [rax+rcx], 0x69  ; subtract 0x69 from each byte
           inc rcx
           cmp byte ptr [rax+rcx], 0     ; check for null terminator
           jnz loc_loop
           ; jump to real _cexit at 0x4054A0
           mov eax, 0x400000
           add eax, 0x54A0
           push rax
           ret                     ; effectively: jmp 0x4054A0

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

After decrypting the flag in memory, the code constructs the address of the real _cexit (0x400000 + 0x54A0 = 0x4054A0) and jumps to it via push+ret, so the program exits normally.

Solution

Step 1: Extract Encrypted Bytes

The encrypted flag data is at virtual address 0x40B9B0 (file offset 0x6BB0):

ac b8 ad ae ab c2 e4 d1 9a cd ce c8 cd a9 dd ca c8 ca b7 cd c8 9a c8 cf d5 a9 d0 e6

Step 2: Decrypt the Flag

python
#!/usr/bin/env python3
"""
Three Faces of Shadow — Flag Decryption
The real flag is hidden in patched _cexit → .idata section code.
Each byte is encrypted by adding 0x69; we reverse by subtracting.
"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Encrypted bytes at 0x40B9B0 (file offset 0x6BB0)
enc = bytes.fromhex(
    'ac b8 ad ae ab c2 e4 d1'
    '9a cd ce c8 cd a9 dd ca'
    'c8 ca b7 cd c8 9a c8 cf'
    'd5 a9 d0 e6'.replace(' ', '')
)

# Decrypt: subtract 0x69 from each byte
flag = bytes([(b - 0x69) & 0xFF for b in enc])
print(f"Flag: {flag.decode()}")
# Output: CODEBY{REDACTED}

Verification

Running the binary (or emulating the decryption) produces the flag after main() returns, when the patched _cexit handler executes the hidden decryption loop before passing control to the real CRT exit. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍