crackmes Medium

Moment

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 infrastructure
  • AddVectoredExceptionHandler, RemoveVectoredExceptionHandler – exception-based obfuscation
  • RegOpenKeyExW, RegQueryValueExW, GetFileVersionInfoSizeW – registry/version checking
  • OpenSCManagerA, AdjustTokenPrivileges – privilege checks
  • _beginthreadex, Sleep, SleepConditionVariableSRW – multi-threaded architecture
  • puts – 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; ret stubs)

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)
  • NtQueryInformationProcess anti-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:

  1. 0x1400BF150 – A decrypt stub function (mov eax, 0xFC; ret)
  2. 0x1400EF8D9 – Error format string: "failed to acquire basic process info for (%p {parent=%p})"
  3. 0x1400FF71EThe flag string in sub_1400FE8A0
  4. 0x140106C00 – 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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
; Stack args (rsp+0x20 through rsp+0x80)
mov  [rsp+170h+var_150], 28h   ; 40 -> 'O'
mov  [rsp+170h+var_148], 4Ah   ; 74 -> '{'
mov  [rsp+170h+var_140], 34h   ; 52 -> '0'
mov  [rsp+170h+var_138], 17h   ; 23 -> 'x'
mov  [rsp+170h+var_130], 1     ; 1  -> 'b'
mov  [rsp+170h+var_128], 0     ; 0  -> 'a'
mov  [rsp+170h+var_120], 3     ; 3  -> 'd'
mov  [rsp+170h+var_118], 2     ; 2  -> 'c'
mov  [rsp+170h+var_110], 34h   ; 52 -> '0'
mov  [rsp+170h+var_108], 34h   ; 52 -> '0'
mov  [rsp+170h+var_100], 3     ; 3  -> 'd'
mov  [rsp+170h+var_F8],  39h   ; 57 -> '5'
mov  [rsp+170h+var_F0],  4Bh   ; 75 -> '}'
; Register args
mov  r8d, 1Ch                   ; 28 -> 'C'
mov  r9d, 26h                   ; 38 -> 'M'
call sub_1400BDAC0              ; 15-byte string decrypt

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 r14 with 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_DATA at 0x7FFE0000 (specifically QpcBias at +0x2F8) is used as an entropy source for computing the crash address, making the r14 value non-deterministic but always invalid.

Flag

CMO{0xbadc00d5}