4 min read

crackmes.one Very Easy Crackme Writeup: Reversing a Base-64 Hash Check

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:

  1. v=0 → 0*64+58 = 58
  2. v=58 → 58*64+82 = 3794
  3. v=3794 → 3794*64+101 = 242,917
  4. v=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?

  1. 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. Here T=5,546,807, so r=55.
    • A single byte can jump by +64, +128, +192 and still have the same residue.
      So the last character y can 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.
  2. Undo the final step
    Once you've picked y, 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 satisfy prefix's result ≡ T1 (mod 156,250)," since 10,000,000 / 64 = 156,250.
  3. Search for the prefix in a smaller world
    Now all you need is a short prefix P such that
    H(P) % 156,250 == T1.
    Using printable ASCII (space through tilde, 32–126) and a small search over lengths 2–4, you find one fast.
  4. Concatenate prefix + last character, and that's your answer
    • Choosing y='w'(119) gives T1=86,667, and you find P=":Re":Rew.
    • Choosing y='7'(55) gives T1=86,668, and you find P=":Rf":Rf7 (the combo you confirmed works in practice).

The one-liner takeaway

  • Align the last byte to the %64 residue (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 constant 0x54A337.
  • The rule in plain English: each step is ×64 + character ASCII, with % 10,000,000 at 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:

  1. 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 vn has to equal 5,546,807.
  2. The last byte must align to %64
    Because the final step is "everything before ×64 + the last byte," the last byte's ASCII must make
    final result % 64 == last byte % 64.
    In other words "last byte ≡ 5,546,807 (mod 64)"; here 5,546,807 % 64 = 55,
    so the last byte can be any congruent value such as 55('7'), 119('w'), 183(0xB7), or 247(0xF7).