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

  1. Launch the executable under x64dbg with g0ld3n_t0k3n_426 as its argument.
  2. Search the disassembly for the immediate constant 0x1337c0de and place a breakpoint on
    the comparison or the conditional jump immediately after it.
  3. Continue until the breakpoint is reached. The value being compared is the masked result
    from gate, so it will not equal the magic constant.
  4. Set the comparison register (commonly EAX in this build) to 0x1337c0de, set the Zero
    Flag, or temporarily change/NOP the conditional jump.
  5. 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:

  1. license key length → 16
  2. the license key → g0ld3n_t0k3n_426 (static recovery)
  3. the 32-bit magic at the feature check (hex) → 0x1337c0de
  4. 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

Key lessons

Show flag

Securinets{st4t1c_r3ads_dyn4m1c_wr1t3s_th3_r3g1st3r}