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
,逐步:
v=0 → 0*64+58 = 58
v=58 → 58*64+82 = 3794
v=3794 → 3794*64+101 = 242,917
v=242,917 → 242,917*64+119 = ... ≡ **5,546,807** (mod 10,000,000)
與目標相等 → 輸入 :Rew
會顯示 Good cracker !
。
4) 為什麼解不只一個?怎麼「拼出」任意解?
- 先決定最後一個字元該長什麼樣
想像驗證公式的最後一步像在做:「前面那堆數 ×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 的尾數」對齊目標。
- 算一下:
- 把最後一步「倒退一步」
一旦選了y
,就可以把最後一步「拆掉」:- 先算
T - y
,因為剛好是「前面那堆 ×64」。 - 再除以 64:
T1 = (T - y) / 64
(這裡一定整除,因為我們挑 y 就是為了對齊)。
這樣,原本「整串字」的問題,就縮小成「只要讓前綴做到前綴的結果 ≡ T1 (mod 156,250)
」就好,因為10,000,000 / 64 = 156,250
。
- 先算
- 在比較小的世界裡找前綴
現在只要找一段短短的前綴P
,讓H(P) % 156,250 == T1
。
用可列印 ASCII(空白到波浪,32–126),長度 2~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),補充一個更直觀的想法:
- 每一步都在「繞圈」
不是單純一直變大到最後,而是每一步都把數字壓回 0~9,999,999(也就是對 10,000,000 取餘數)。v0 = 0
- 讀第1字:
v1 = (v0*64 + ASCII1) % 10,000,000
=ASCII1
- 讀第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。
- 最後一碼要對齊
%64
因為最後一步是「前面乘 64 + 最後一碼」,所以最後一碼的 ASCII 必須讓最後結果 % 64 == 最後一碼 % 64
。
也就是「最後一碼 ≡ 5,546,807 (mod 64)」,本題5,546,807 % 64 = 55
,
所以最後一碼可以選55('7')
、119('w')
、183(0xB7)
、247(0xF7)
這類同餘的值。