Overview
| Field | Value |
|---|---|
| Category | Intermediate |
| Points | 968 |
| Author | daax |
| Binary | PE64 x86-64, Windows, C++, MSVC, not stripped |
| Description | Anti-tamper crackme with obfuscated string encryption |
| Flag | CMO{0xbadc00d5} |
Recon
The binary is a 1.2MB Windows PE64 console application compiled with MSVC. Key imports include:
NtAllocateVirtualMemory,NtQueryInformationProcess– anti-tamper infrastructureAddVectoredExceptionHandler,RemoveVectoredExceptionHandler– exception-based obfuscationRegOpenKeyExW,RegQueryValueExW,GetFileVersionInfoSizeW– registry/version checkingOpenSCManagerA,AdjustTokenPrivileges– privilege checks_beginthreadex,Sleep,SleepConditionVariableSRW– multi-threaded architectureputs– sole output function
The program has a complex anti-tamper architecture with 17+ integrity-checking functions dispatched by a worker thread, plus anti-debug detection with a taunting message.
Architecture
String Encryption
All strings are encrypted using a dual-field table at unk_140111680 containing 89 entries. Each 16-byte entry has:
- 8-byte XOR value
- 8-byte function pointer (tiny
mov eax, X; retstubs)
Decryption formula: decoded_byte = stub_return_value ^ table_xor_byte
This produces a 89-character alphabet:
Index 0-25: a-z
Index 26-51: A-Z
Index 52-61: 0-9
Index 62-88: . : \ % \n " _ - ' ? { } < > ( ) = \t + ! # ; * , @
~30 decrypt functions exist, each for a specific string length (4 to 62 bytes), totaling 265+ call sites across the binary.
Exception-Based Obfuscation
The decrypt functions use a call r14 instruction that intentionally crashes (r14 is computed as an invalid address from KUSER_SHARED_DATA.QpcBias ^ 0x77190988 - constant). A Vectored Exception Handler catches the access violation and redirects execution to the real decryption path at 0x1400BB764, which performs the table-based XOR decryption.
Multi-Threaded Anti-Tamper
The main function at 0x140104520 spawns two background threads:
Thread 1 (sub_140108DE0): Worker thread that iterates through an anti-tamper task list at off_14012A4A0 (17 entries, each 16 bytes: [func_ptr][XOR_dword][completion_flag]). For each entry, it spawns a sub-thread that XORs the dword with 0x77190988 to get an index, calls the function, and sets a completion flag. After all tasks complete, it resolves an API via PEB walking (FNV-1 hash) and enters a 500ms sleep loop.
Thread 2 (sub_1401031A0): Anti-debug watchdog that spin-waits on byte_14012B250. When a debugger is detected, it decrypts and prints: "yikes, youll never recover the flag now."
Anti-Tamper Functions
The 17 task entries dispatch functions including:
- Version checking via
GetFileVersionInfoSizeW(sub_1400EE9C0) - VEH trap-flag anti-debug (
sub_1400C32F0) NtQueryInformationProcessanti-tamper (sub_1400C33B0)- Registry inspection of
SOFTWARE\Microsoft\RemovalTools\MRT - NTFS metadata path checks (
C:\$extend\$rmmetadata\$txflog\) - DLL enumeration via regex
\.(?:dll|DLL) - Various other integrity checks
Main Loop
The main loop (sub_140104520) performs registry enumeration, version checking, and system validation. It formats results as [crit] %s => %s log messages and outputs them via puts(). The function also contains the tamper detection message: "{!!} watchdog did not initialize. tampering likely."
Finding the Flag
Approach
The flag format CMO{...} requires characters { (index 74) and } (index 75) from the decrypt alphabet. Searching for immediate value 74 across the binary yielded 4 hits:
0x1400BF150– A decrypt stub function (mov eax, 0xFC; ret)0x1400EF8D9– Error format string:"failed to acquire basic process info for (%p {parent=%p})"0x1400FF71E– The flag string insub_1400FE8A00x140106C00– Tamper detection:"{!!} watchdog did not initialize. tampering likely."
Flag Decrypt Call
At address 0x1400FF6C3 in sub_1400FE8A0 (an anti-tamper function, entry 16 in the task list), the following indices are loaded and passed to sub_1400BDAC0 (15-byte string decryptor):
|
|
The decrypt function sub_1400BDAC0 calls NtAllocateVirtualMemory with size=15, confirming a 15-character output. The call r14 crash is caught by the VEH, which redirects to the actual XOR-based decryption using the 89-entry table.
Decoding the indices in parameter order: 28, 38, 40, 74, 52, 23, 1, 0, 3, 2, 52, 52, 3, 57, 75 -> CMO{0xbadc00d5}
Hint String
A separate decrypt call at sub_140109480 produces the hint: "where control and identity align youll find the ekey to the %s." – referencing the anti-tamper control flow and identification checks that guard the flag.
Decoded Strings
| Address | Function | String |
|---|---|---|
0x1400FF6C3 |
sub_1400FE8A0 |
CMO{0xbadc00d5} (the flag) |
0x140109480 |
main loop | where control and identity align youll find the ekey to the %s. |
0x140106BF0 |
main loop | {!!} watchdog did not initialize. tampering likely. |
0x140103245 |
Thread 2 | yikes |
0x14010367C |
Thread 2 | %s, youll never recover the flag now. |
0x14001A46A |
sub_140018970 |
BOCHS (anti-VM check) |
Lessons Learned
- Exception-based obfuscation using intentional crashes (
call r14with invalid computed addresses) is effective against static analysis but can be defeated by identifying the VEH and tracing the real decryption path. - Searching for rare alphabet indices (like
{and}) is an effective way to locate flag strings among hundreds of encrypted string call sites. - The x64 Windows calling convention (rcx, rdx, r8, r9, then stack at rsp+0x20) must be carefully tracked when the decompiler fails on obfuscated code – the stack arguments are set bottom-up in the instruction stream but map to parameters in ascending offset order.
KUSER_SHARED_DATAat0x7FFE0000(specificallyQpcBiasat+0x2F8) is used as an entropy source for computing the crash address, making the r14 value non-deterministic but always invalid.
Flag
CMO{0xbadc00d5}