$cat move-it.mdReversingHackultet 2026
Move It
A binary compiled with a joke compiler that emits nothing but mov instructions hides a simple XOR check behind an obfuscation so heavy that brute force is easier than reading it.
$man move-it
What this one teaches:
- What the M/o/Vfuscator does, compiling ordinary code into a stream of only mov instructions, using intentional faults for control flow.
- Recognising when static disassembly has stopped being worth the effort and a small brute force is the faster path.
Task description
A move-it binary reads an environment variable and, with the right value, reveals the flag.
Spotting the obfuscation
The task’s own name is the hint. file reports a stripped, dynamically linked 32-bit ELF, and a quick pass over the disassembly shows nearly every single instruction is mov, the unmistakable signature of the M/o/Vfuscator, a compiler that takes the Turing-completeness of mov seriously and rewrites ordinary C into a stream of almost nothing else.
Its control flow has to come from somewhere, since there’s no jmp or call left to carry it. The binary’s only imported functions give it away: getenv reads an environment variable called CONTROL_VAR, atoi converts it to a number, and sigaction installs handlers for SIGSEGV and SIGILL, deliberate crashes that the movfuscator uses as its substitute for jumps, each handler stepping execution to the next block.
Why static analysis isn’t worth it here
Standard disassemblers and decompilers (Ghidra, IDA) struggle badly with an instruction stream that’s uniformly mov, since nearly all of the structure a decompiler relies on (branches, calls, distinct instruction types) simply isn’t present in the usual form. Dedicated tools exist for recovering a movfuscated binary’s control-flow graph (such as demovfuscator), but for a search space this small, dynamic observation is faster than reconstructing anything statically.
What the binary is actually doing
Watching it run under a debugger shows the real logic: a 21-byte array inside the binary gets XORed, byte by byte across 22 signal-triggered steps, with a mask built from CONTROL_VAR mod 256 and a fixed per-position constant. With the correct CONTROL_VAR, all 21 bytes decode to the flag’s ASCII text.
Brute-forcing the input
Since the mask only depends on CONTROL_VAR modulo 256, there are only 256 possible inputs to try, small enough to just run all of them and look for a flag-shaped result:
for i in $(seq 0 255); do
result=$(CONTROL_VAR=$i gdb -batch -q \
-ex "b _exit" -ex "run" -ex "x/21c 0x805c020" \
./a.out 2>/dev/null | grep "0x805c020")
echo "$result" | grep -q "CTF" && echo "CONTROL_VAR=$i: $result" && break
done
CONTROL_VAR=158 is the one that decodes the array into the flag:
CTF2026[375102425819]
The takeaway
Heavy obfuscation raises the cost of reading a binary statically, but it doesn’t shrink the actual decision space underneath it. Once the real logic turned out to depend on a single byte, dynamic analysis (watch the memory, try all 256 values) was far cheaper than reconstructing the movfuscated control flow by hand.
$cat flag.txt reveal
CTF2026[375102425819]