Три Лика Тени (Three Faces of Shadow)
Три Лика Тени (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()checkFindWindowA()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_VMstruct: 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")at0x402fa9- 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:
_mainCRTStartup→ callsmain()- After
main()returns → calls_cexit()for cleanup _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 read-only • copy allowed0x40B9D0: 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 read-only • copy allowed#!/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.