Cr4ck 3
- Challenge: Cr4ck 3
- Category: Rev
- Flag:
LYKNCTF{Dyn4m1c_0nly_LYKN_2026!!}
My initial read / first impressions
The challenge name was Cr4ck 3, and the description was just:
We are given a Windows executable:
So from the name alone, this looked like a classic serial / flag checker. I expected either a hardcoded serial hiding somewhere or a checker function that transformed the input and compared it against some constants.
The flag was not just sitting directly in strings, but the format was pretty clear after looking at the validation logic. The program expects:
where the inside has exactly 24 characters.
So the total flag length is:
Finding the actual check
The first layer of the check is just normal format validation:
After that, the interesting part starts. Each of the 24 inner characters is processed one at a time.
At first I thought this might just be a normal character transform, but it was slightly more annoying than that. The binary uses a small obfuscated VM-ish routine to transform each character, then compares the low 16 bits of the result against a target table.
The important part is that the state is dynamic. The checker does not treat each character completely independently. After a character is correct, the result becomes the new base value, and the seed also gets updated.
So the shape is basically:
check char 0 using initial state
-> update base and seed
check char 1 using new state
-> update base and seed
check char 2 using new state
-> ...
This means we cannot just solve every character with one static equation. We have to solve them in order.
The dynamic state
The binary hashes its .text section and uses that to derive the initial state. For the provided executable, the useful values ended up being:
The constants used by the transform were:
The target table had 24 values, one for each inner character:
targets = [
0x526e, 0xbb33, 0x417f, 0x11d4, 0xcef9, 0x9caf,
0xbdf1, 0x3623, 0x337c, 0xe83c, 0xc20b, 0x915c,
0x2664, 0xc495, 0x70c4, 0xbfb0, 0x8bd2, 0x716a,
0x2081, 0xb422, 0x8ae5, 0x2a77, 0xb1f9, 0xdd9e
]
Once I had those pieces, the challenge became much less scary. Since each flag character is printable ASCII, I only needed to try characters from 0x20 to 0x7e for each position.
For each candidate character:
- Run the same transform as the binary.
- Check whether
result & 0xffffmatches the current target. - If it matches, keep that character and update the state.
- Move on to the next target.
Rewriting the checker
The transform looked like this after cleaning it up:
x = base
t = (ch * 0x53 + seed) & 0xffffffff
x ^= t
x = rol(x, 7)
x ^= x >> 13
x = (x + seed) & 0xffffffff
x = (x * C) & 0xffffffff
x = rol(x, 11)
x ^= x >> 17
x = (x + A) & 0xffffffff
Then the checker compares:
against the target for that position.
The state update after a correct character is:
That explains the flag text too. The challenge is literally about the check being dynamic only.
Solution Script
Here is the final solve script I used.
def rol(x, n):
n &= 31
return ((x << n) | (x >> (32 - n))) & 0xffffffff
C = 0x9c5ab3d7
A = 0x3f1e5c2b
base = 0x613cdcaa
seed = base ^ 0xa5a5a5a5
targets = [
0x526e, 0xbb33, 0x417f, 0x11d4, 0xcef9, 0x9caf,
0xbdf1, 0x3623, 0x337c, 0xe83c, 0xc20b, 0x915c,
0x2664, 0xc495, 0x70c4, 0xbfb0, 0x8bd2, 0x716a,
0x2081, 0xb422, 0x8ae5, 0x2a77, 0xb1f9, 0xdd9e
]
body = ""
for target in targets:
found = False
for ch in range(0x20, 0x7f):
x = base
t = (ch * 0x53 + seed) & 0xffffffff
x ^= t
x = rol(x, 7)
x ^= x >> 13
x = (x + seed) & 0xffffffff
x = (x * C) & 0xffffffff
x = rol(x, 11)
x ^= x >> 17
x = (x + A) & 0xffffffff
if (x & 0xffff) == target:
body += chr(ch)
base = x
seed = rol((seed * C + A) & 0xffffffff, 13)
found = True
break
if not found:
raise RuntimeError("could not solve next character")
print("LYKNCTF{" + body + "}")
Running it prints:
Why this works
The checker looks annoying because of the obfuscation and the VM-style transform, but the actual search space is tiny once the logic is copied out.
Each character only has around 95 printable possibilities. Since the state update only happens after finding the correct character, we can solve the flag from left to right.
So instead of brute forcing the whole 24-character body, the solve is basically:
which is nothing.
The main thing was noticing that the state was dynamic. If I tried to solve every position using only the original base and seed, the results would be wrong after the first character. Once I updated the state exactly like the binary did, the whole flag fell out cleanly.