Twofold
Points: 800 points · Provided: twofold.exe
Some locks you can read your way through. Some you have to be holding open while you look. This one wants both.
Hint 1
Every operation in the key check is reversible. Undo them in reverse order: subtraction first, XOR last.
Hint 2
The second comparison can never be true on a normal run. Work out the largest value gate() can return, then change the outcome in a debugger instead of hunting for a key.
What it targets
Twofold is deliberately split into two kinds of reverse engineering. The first gate can be
solved by reading code and inverting a byte transform. The second gate is mathematically
unreachable during an ordinary execution, so understanding it statically is not enough: the
solver must stop the program at runtime and change its state.
| Stage | Technique | Result |
|---|---|---|
| Triage | PE inspection and strings | Identify a 64-bit Windows console executable and its success/failure messages |
| Gate 1 | Static analysis | Recover the 16-character license key |
| Gate 2 | Debugging | Force an impossible comparison to succeed |
| Validation | Question service | Answer four questions about the binary to receive the flag |
Useful tools include Ghidra or IDA for decompilation, x64dbg or WinDbg for runtime control,
and a few lines of Python for reversing the key transform. The binary is benign, but it should
still be analyzed in a disposable Windows VM because running unknown CTF executables directly
on a host is a bad habit.
Initial triage
Running the program without arguments reveals the expected interface:
Twofold license validator
usage: twofold.exe <license-key>
An arbitrary key fails immediately. A valid key reaches a second message—premium feature locked—which tells us there are two independent checks. Basic string extraction reveals the
messages, but not the key, unlock token, or final flag. Those values are either encoded or kept
on the remote asker.
The key, the token and the flag are all absent from the binary as plaintext: the table and
token are obfuscated, and the flag is only returned by the question service.
Recovering the license key step by step
The decompiler shows three important constants: 0x5a, 0x11, and 0x3c. For index i,
the program first de-obfuscates an expected byte and then compares it with a transformed key
byte:
expected[i] = OBF_EXPECTED[i] XOR 0x3c
transformed = ((key[i] XOR 0x5a) + i * 0x11) mod 256
To invert the operation, undo the addition first and XOR last:
key[i] = ((expected[i] - i * 0x11) mod 256) XOR 0x5a
The complete recovery script is short enough to audit by eye:
obfuscated = [
0x01, 0x47, 0x64, 0x4d, 0x91, 0xb5, 0x57, 0x99,
0xce, 0xf6, 0x2f, 0xd3, 0xed, 0x77, 0x6a, 0x57,
]
key = []
for i, value in enumerate(obfuscated):
expected = value ^ 0x3c
original = ((expected - i * 0x11) & 0xff) ^ 0x5a
key.append(chr(original))
print("".join(key))
# g0ld3n_t0k3n_426
Running twofold.exe g0ld3n_t0k3n_426 confirms the inversion: the program accepts the
license and proceeds to the premium-feature check.
Why the second gate cannot pass normally
The gate function starts with an accumulator and repeatedly multiplies it by 31, adds one
key byte, and masks the result with 0x00ffffff. Consequently, every possible return value
is at most 0x00ffffff. The comparison target is 0x1337c0de, which is outside that range.
This is not a password that can be brute-forced. No license key can make the branch true. The
important reverse-engineering conclusion is that the program must be manipulated at the
comparison itself.
Forcing the branch in a debugger
- Launch the executable under x64dbg with
g0ld3n_t0k3n_426as its argument. - Search the disassembly for the immediate constant
0x1337c0deand place a breakpoint on
the comparison or the conditional jump immediately after it. - Continue until the breakpoint is reached. The value being compared is the masked result
fromgate, so it will not equal the magic constant. - Set the comparison register (commonly
EAXin this build) to0x1337c0de, set the Zero
Flag, or temporarily change/NOP the conditional jump. - Continue execution. The previously unreachable branch decodes the protected byte array at
runtime and prints:
[+] feature unlocked -> UNLOCK TOKEN: TW0F0LD_UNL0CK3D
Changing the register or flags is preferable during analysis because it leaves the original
file untouched. Patching the jump also works, but then the analyst must track which binary was
modified.
Getting the flag
The flag came from a question service that asked four questions in order, one at a time:
- license key length → 16
- the license key → g0ld3n_t0k3n_426 (static recovery)
- the 32-bit magic at the feature check (hex) → 0x1337c0de
- the UNLOCK TOKEN (only from forcing the branch) → TW0F0LD_UNL0CK3D (dynamic)
Because the questions come in order, the static stage has to be finished before the dynamic
one. The final flag is never embedded in the executable.
Why it needs both, and resists shortcuts
- Q2 is pure static (invert the transform); Q4 cannot be answered without actually running
the binary under a debugger and manipulating execution state — there is no readable
string to copy. - Patching the check to "always taken" in a static patch still requires knowing to take
the branch and yields the same token, so the intended skill (dynamic control-flow
manipulation) is what unlocks it.
Key lessons
- Decompilation is a model of the machine code, not a substitute for reasoning about ranges
and reachable states. - Reversible obfuscation only slows inspection; once the transform is visible, inversion is
deterministic. - A debugger can alter registers, flags, and control flow without permanently patching a file.
Show flag
Securinets{st4t1c_r3ads_dyn4m1c_wr1t3s_th3_r3g1st3r}