crackmes.one Very Easy Crackme Writeup: Reversing a Base-64 Hash Check
Challenge link: https://crackmes.one/crackme/67649fee60fa67152406bcac
1) Start with main: locate the comparison logic
Over in FUN_00401000 (which is essentially main) we see:
- Read the user's input:
gets_s(local_1f8, 500); - Feed the input into a computation function:
iVar1 = FUN_00401090(); - Compare against a constant:
if (iVar1 == 0x54A337) ...
0x54A337 in decimal is 5,546,807.
Goal: find any string s such that FUN_00401090(s) == 5,546,807, and the program prints Good cracker !.
2) Follow FUN_00401090: the rule
Boiled down to its equivalent logic:
int H(const unsigned char *s) {
int v = 0;
for (size_t i = 0; i < strlen((char*)s); i++) {
v = (v * 64 + s[i]) % 10000000; // 0x40 = 64
}
return v;
}
What does "base 64" mean here?
Think of the whole string as a number written in a custom radix:
decimal is "multiply each digit by 10, then add the next digit," hex is "multiply each digit by 16, then add the next digit,"
here it's "each step, multiply by 64 then add the ASCII value of the next character."
To keep the value from exploding, every step also takes the remainder % 10,000,000, keeping it in the range 0–9,999,999.
In plain terms:
It's a multiply-then-add all the way through algorithm:
start from 0, and for every character repeat "current value * 64 + character's ASCII, then % 10,000,000."
3) Worked example: why does :Rew pass?
Character ASCII values: ':'=58, 'R'=82, 'e'=101, 'w'=119. Step by step:
v=0 → 0*64+58 = 58v=58 → 58*64+82 = 3794v=3794 → 3794*64+101 = 242,917v=242,917 → 242,917*64+119 = ... ≡ **5,546,807** (mod 10,000,000)
Equal to the target, so entering :Rew prints Good cracker !.
4) Why is there more than one solution? How do you "assemble" an arbitrary one?
- First decide what the last character has to look like
Picture the final step of the check as: "that big pile of numbers so far ×64, then add the last character."
Since the last step "first multiplies by 64, then adds the last character," the last character of the answer has to make the overall "residue (the remainder mod 64)" line up with the residue of the target value.- Do the math:
r = target T % 64. HereT=5,546,807, sor=55. - A single byte can jump by +64, +128, +192 and still have the same residue.
So the last characterycan only be:{55, 55+64, 55+128, 55+192}intersected with[0..255].
→ For this challenge the choices are{55('7'), 119('w'), 183(0xB7), 247(0xF7)}.
Intuition to remember: the last character aligns the "mod-64 residue" with the target.
- Do the math:
- Undo the final step
Once you've pickedy, you can "peel off" the last step:- First compute
T - y, which is exactly "that big pile ×64." - Then divide by 64:
T1 = (T - y) / 64(this divides evenly, precisely because we chose y to line up).
Now the original "whole string" problem shrinks to "just make the prefix satisfyprefix's result ≡ T1 (mod 156,250)," since10,000,000 / 64 = 156,250.
- First compute
- Search for the prefix in a smaller world
Now all you need is a short prefixPsuch thatH(P) % 156,250 == T1.
Using printable ASCII (space through tilde, 32–126) and a small search over lengths 2–4, you find one fast. - Concatenate prefix + last character, and that's your answer
- Choosing
y='w'(119)givesT1=86,667, and you findP=":Re"→:Rew. - Choosing
y='7'(55)givesT1=86,668, and you findP=":Rf"→:Rf7(the combo you confirmed works in practice).
- Choosing
The one-liner takeaway
- Align the last byte to the
%64residue (this fixes y). - Roll back the final step (compute
T1=(T-y)/64). - Search for a short prefix in the mod-156,250 space, tack y back on, and you've got a working passphrase.
Because this construction yields a huge number of combinations, the answer isn't unique; but the rule is fixed, so the same set of passing combinations gets accepted every time.
5) Verification and an auto-generation script
Verify a given input
def H(s, M=10_000_000):
v = 0
for ch in s.encode():
v = (v * 64 + ch) % M
return v
print(H(":Rew")) # 5546807
print(H(":Rf7")) # 5546807
Auto-generate a "printable" solution
# Generate a printable solution (ASCII 32..126) with H(s) == T
import itertools
M = 10_000_000
B = 64
T = 5_546_807
PR = list(range(32, 127)) # printable ASCII
MOD_PREFIX = M // B # 156250
def H_mod(bytes_seq, mod):
v = 0
for b in bytes_seq:
v = (v * B + b) % mod
return v
def gen_one(max_len_prefix=4):
r = T % B
last_candidates = [r + 64*k for k in range(4) if r + 64*k <= 255 and 32 <= r + 64*k <= 126]
for y in last_candidates:
T1 = (T - y) // B
for L in range(1, max_len_prefix + 1):
for tup in itertools.product(PR, repeat=L):
if H_mod(tup, MOD_PREFIX) == T1:
s = bytes(tup + (y,)).decode('latin1')
return s
return None
print("solution =", gen_one()) # might print ':Rew', ':Rf7', etc.
6) Wrap-up
- The backbone: input →
FUN_00401090→ compare against the constant0x54A337. - The rule in plain English: each step is
×64 + character ASCII, with% 10,000,000at every step. - Demo solutions:
:Rew,:Rf7, plus you can generate a whole bunch of answers.
Lessons learned
Regarding section 4), here's a more intuitive way to think about it:
- Every step "wraps around"
It's not simply growing bigger and bigger to the end; every step squashes the number back into 0–9,999,999 (i.e. takes the remainder mod 10,000,000).v0 = 0- Read char 1:
v1 = (v0*64 + ASCII1) % 10,000,000=ASCII1 - Read char 2:
v2 = (v1*64 + ASCII2) % 10,000,000 - Read char 3:
v3 = (v2*64 + ASCII3) % 10,000,000 - Read char 4:
v4 = (v3*64 + ASCII4) % 10,000,000 - …and the final
vnhas to equal 5,546,807.
- The last byte must align to
%64
Because the final step is "everything before ×64 + the last byte," the last byte's ASCII must makefinal result % 64 == last byte % 64.
In other words "last byte ≡ 5,546,807 (mod 64)"; here5,546,807 % 64 = 55,
so the last byte can be any congruent value such as55('7'),119('w'),183(0xB7), or247(0xF7).
Member discussion