Polished CTF Writeup

HTB Callfuscated

An Insane reversing challenge built around call-threaded control flow, misleading function boundaries, and a compact VM-style validator hidden inside noisy x86.

Category
Reversing
Difficulty
Insane
Technique
Dynamic trace reduction + VM lifting
Binary
64-bit Linux ELF, stripped
Recovered Flag
HTB{Sliced_Up_the_Function_4_Ya}

Overview

Callfuscated looked at first like a normal Linux crackme, but normal reverse-engineering workflows quickly broke down. The binary was stripped, the real validation routine was not represented cleanly as a function, and Ghidra could not produce a trustworthy high-level view of the critical code path.

The final solve came from changing abstraction levels: instead of trying to read the decompiler output, I traced the executed code, recognized a call-threaded VM/stack-machine, recovered its bytecode semantics, and reduced the final password check to simple 32-bit XOR/subtraction constraints.

Initial Triage

The archive contained a single target binary:

rev_callfuscated/crackme

Basic properties:

PropertyValue
FormatELF 64-bit LSB executable
Architecturex86-64
LinkingDynamically linked
SymbolsStripped
SHA-2562389a35ac38bff9546a4c356c7d22633b22e8a1616b61adb85991afda40a3e36

Runtime behavior:

Welcome to callfuscated crackme.
To register enter your password:
Incorrect flag. Try again

Important strings and addresses discovered during analysis:

entry:                 0x401080
real main-ish target:  0x409002
input buffer:          0x40f080
prompt string:         0x40d008
scanf format:          0x40d04b  "%63s"
success format:        0x40d050  "Correct. Validate the challenge using the flag: %s"
failure string:        0x40d084

Callfuscation Model

The binary used a repeated pattern like:

call target
pop r8
...
call next_block
pop r8

This destroys the assumptions normal disassemblers and decompilers make about calls, returns, and function boundaries. Ghidra could identify many pieces of code, but the apparent functions were not the semantic units that mattered.

Key pivot: the problem was not “find the right function in Ghidra.” The problem was “recover the executed instruction stream and infer the higher-level machine being interpreted.”

Dynamic Analysis

I used controlled dynamic analysis to locate the actual validation flow. The input was stored globally at:

0x40f080

Watchpoints and traces showed that the program consumed exactly 32 bytes of input. A simple prefix oracle did not exist; wrong inputs produced similar late failure states, so the validator was not a straight character-by-character compare.

The decisive late predicate was observed around:

0x40b537: test eax, eax
0x40bcc2: jne  0x4096e1

For bad inputs, eax ended as 0xffffffff and execution reached the failure path.

VM Reduction

Tracing the semantic instruction stream revealed that the validator behaved like a small stack machine. The clean bytecode semantics were recovered as:

OpcodeMeaning
op0push immediate
op2add
op3subtract
op5multiply
op7bitwise OR
op8XOR
op10load byte from computed address

The VM built eight big-endian 32-bit words from the 32-byte input and checked each chunk with:

((word ^ const_a) - const_b) == 0

Therefore each required 32-bit input word was simply:

word = const_a ^ const_b

Recovering the Flag

The extracted constant pairs produced the full 32-byte input:

ExpressionResultASCII
0x0915033a ^ 0x414141410x4854427bHTB{
0x427d7872 ^ 0x111111110x536c6963Slic
0x30310a00 ^ 0x555555550x65645f55ed_U
0x2a052e32 ^ 0x5a5a5a5a0x705f7468p_th
0xcff5ecdf ^ 0xaaaaaaaa0x655f4675e_Fu
0x1914031e ^ 0x777777770x6e637469ncti
0xf6f7c6ad ^ 0x999999990x6f6e5f34on_4
0x6c6a524e ^ 0x333333330x5f59617d_Ya}

Verification

Combining the chunks gives:

HTB{Sliced_Up_the_Function_4_Ya}

Running the binary with that value reached the success path and printed the expected validation string.

Lessons Learned

  • Decompiler failure was a signal, not a dead end.
  • The right abstraction was an executed VM trace, not static function recovery.
  • Call-threading can make ordinary control-flow graphs actively misleading.
  • The obfuscated arithmetic ultimately collapsed to simple XOR/subtraction equations.