5 min read

crackmes.one - Very Easy Crackme writeup (zh-TW)

crackmes.one - Very Easy Crackme writeup (zh-TW)

題目連結:https://crackmes.one/crackme/67649fee60fa67152406bcac

1) 先看 main:定位比對邏輯

FUN_00401000(等同 main)看到:

  • 讀入使用者輸入:gets_s(local_1f8, 500);
  • 把輸入丟進計算函式:iVar1 = FUN_00401090();
  • 與常數比較:if (iVar1 == 0x54A337) ...

其中 0x54A337 十進位是 5,546,807
目標:找任意字串 s,使 FUN_00401090(s) == 5,546,807,就會顯示 Good cracker !

2) 追 FUN_00401090:規則

精簡後等價邏輯:

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;
}

「以 64 為基底」是什麼意思?

把整串字元當成一個特製的「進位數」:
十進位是「每一位 ×10 再加下一位」,十六進位是「每一位 ×16 再加下一位」,
這裡是「每一步 ×64 再加上下一個字元的 ASCII」
為了避免爆大,每一步都再取餘數 % 10,000,000,讓值維持在 0~9,999,999。

簡單來說:

就是一路乘、一路加的算法:
從 0 開始,對每個字元重複「目前值 * 64 + 字元ASCII,再 % 10,000,000」。

3) 實例:為什麼 :Rew 能過?

字元 ASCII:':'=58, 'R'=82, 'e'=101, 'w'=119,逐步:

  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)

與目標相等 → 輸入 :Rew 會顯示 Good cracker !

4) 為什麼解不只一個?怎麼「拼出」任意解?

  1. 先決定最後一個字元該長什麼樣
    想像驗證公式的最後一步像在做:「前面那堆數 ×64,再加上最後一個字元」。
    既然最後一步會「先乘 64 再加最後一個字元」,那答案的最後一個字元,一定要讓整體的「尾數(除以 64 的餘數)」對得上目標值的尾數。
    • 算一下:r = 目標 T % 64。本題 T=5,546,807,所以 r=55
    • 一個位元組可以 +64、+128、+192 這樣跳,同樣會有相同的尾數。
      所以最後一個字元 y 只可能是:{55, 55+64, 55+128, 55+192} 交集 [0..255]
      → 本題可選 {55('7'), 119('w'), 183(0xB7), 247(0xF7)}
      直覺記憶:最後一個字元要把「%64 的尾數」對齊目標。
  2. 把最後一步「倒退一步」
    一旦選了 y,就可以把最後一步「拆掉」:
    • 先算 T - y,因為剛好是「前面那堆 ×64」。
    • 再除以 64:T1 = (T - y) / 64(這裡一定整除,因為我們挑 y 就是為了對齊)。
      這樣,原本「整串字」的問題,就縮小成「只要讓前綴做到 前綴的結果 ≡ T1 (mod 156,250)」就好,因為 10,000,000 / 64 = 156,250
  3. 在比較小的世界裡找前綴
    現在只要找一段短短的前綴 P,讓
    H(P) % 156,250 == T1
    用可列印 ASCII(空白到波浪,32–126),長度 2~4 個字元小小搜尋一下,很快就找到
  4. 把前綴 + 最後一個字元串起來,就是答案
    • y='w'(119),會得到 T1=86,667,找得到 P=":Re":Rew
    • y='7'(55),會得到 T1=86,668,找得到 P=":Rf":Rf7(你實測成功的那組)。

一句帶走

  • 最後一碼對齊 %64 的尾數(決定 y)。
  • 把最後一步回推(算 T1=(T-y)/64)。
  • 在模 156,250 的小空間找短前綴,接回 y 就是一組通關密碼。
    因為這樣組得出來的組合非常多,所以答案不是唯一的;但規則固定,所以能過的那些組合每次都一樣會被接受。

5) 驗證與自動產生程式

驗證某個輸入

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

自動生成一組「可列印」的解

# 產生一個 H(s) == T 的可列印解(ASCII 32..126)
import itertools

M  = 10_000_000
B  = 64
T  = 5_546_807
PR = list(range(32, 127))  # 可列印 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())  # 可能輸出 ':Rew' 或 ':Rf7' 等

6) 小結

  • 主骨幹:輸入 → FUN_00401090 → 比對常數 0x54A337
  • 規則白話:每一步 ×64 + 字元ASCII,每步 % 10,000,000
  • 示範解:Rew:Rf7,還能生成一票答案。

經驗分享

關於4),補充一個更直觀的想法:

  1. 每一步都在「繞圈」
    不是單純一直變大到最後,而是每一步都把數字壓回 0~9,999,999(也就是對 10,000,000 取餘數)。
    • v0 = 0
    • 讀第1字:v1 = (v0*64 + ASCII1) % 10,000,000ASCII1
    • 讀第2字:v2 = (v1*64 + ASCII2) % 10,000,000
    • 讀第3字:v3 = (v2*64 + ASCII3) % 10,000,000
    • 讀第4字:v4 = (v3*64 + ASCII4) % 10,000,000
    • …最後得到的 vn 要等於 5,546,807
  2. 最後一碼要對齊 %64
    因為最後一步是「前面乘 64 + 最後一碼」,所以最後一碼的 ASCII 必須讓
    最後結果 % 64 == 最後一碼 % 64
    也就是「最後一碼 ≡ 5,546,807 (mod 64)」,本題 5,546,807 % 64 = 55
    所以最後一碼可以選 55('7')119('w')183(0xB7)247(0xF7) 這類同餘的值。