crackme#05 解析手引き

この解析環境はWindows98 OllyDBG1.07(日本語化)で行っています。


OllyDBGを、起動して「crkme05.exe」を読込んでください。
「検索」→「ラベル一覧」(「Search for」→「Name(Label)」)で、ラベル一覧の一覧を、表示します。
例によってGetWindowTextAに「全ての参照にブレークポイントをセット」でBPを仕掛けて下さい。

Names in crkme05
アドレス  セクション  タイプ ( 名前 コメント 
00401000 .text Export <ModuleEntryPoint>
00402000 .rdata Import ( KERNEL32.GetModuleHandleA
00402004 .rdata Import ( KERNEL32.ExitProcess
0040200C .rdata Import ( USER32.GetWindowTextA ←ココ
00402010 .rdata Import ( USER32.LoadIconA
00402014 .rdata Import ( USER32.GetDlgItem
00402018 .rdata Import ( USER32.DialogBoxParamA
0040201C .rdata Import ( USER32.EndDialog
00402020 .rdata Import ( USER32.SetWindowLongA
00402024 .rdata Import ( USER32.ShowWindow
00402028 .rdata Import ( USER32.CallWindowProcA
0040202C .rdata Import ( USER32.MessageBoxA
00402030 .rdata Import ( USER32.SendMessageA


「F9」キーで動かしてみましょう。
crackme #05のウィンドウが、表示されました。
フェークパス「98765」と入力して、「登録」ボタンクリックしてください。
004011F5に止まりました。

004011D9 > 6A 00 PUSH 0 ; /Result = 0
004011DB . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hWnd
004011DE . E8 A5000000 CALL <JMP.&USER32.EndDialog> ; \EndDialog
004011E3 .^E9 B6FEFFFF JMP crkme05.0040109E
004011E8 > 6A 7E PUSH 7E ; /Count = 7E (126.)
004011EA . 68 2C304000 PUSH crkme05.0040302C ; |Buffer = crkme05.0040302C
004011EF . FF35 04304000 PUSH DWORD PTR DS:[403004] ; |hWnd = 002A0170 (class='Edit',parent=001B027E)
004011F5 . E8 9A000000 CALL <JMP.&USER32.GetWindowTextA> ; \GetWindowTextA    ←ココ
004011FA . E8 1D000000 CALL crkme05.0040121C
004011FF . 85C0 TEST EAX,EAX
00401201 . 74 14 JE SHORT crkme05.00401217
00401203 . 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401205 . 68 F5304000 PUSH crkme05.004030F5 ; |Title = "登録情報"4030
0040120A . 68 FE304000 PUSH crkme05.004030FE ; |Text = "正解です!"4030F
0040120F . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hOwner
00401212 . E8 89000000 CALL <JMP.&USER32.MessageBoxA> ; \MessageBoxA
00401217 >^E9 82FEFFFF JMP crkme05.0040109E


下の行にcall , test ,je と来ています。比較の直前にあるcall命令は重要です。
逆に言えば、call命令の後に比較が無ければそのcall命令はさほど重要ではありません。(もちろん例外あり)
解析時間短縮のために頭の片隅に入れて置いて下さい。


それでは、「F7」でcall内部に潜りましょう。
アドレスは0040121Cです。

0040121C /$ 68 2C304000 PUSH crkme05.0040302C ; ASCII "98765"
00401221 |. E8 28000000 CALL crkme05.0040124E
00401226 |. 05 D2040000 ADD EAX,4D2
0040122B |. B9 0D000000 MOV ECX,0D
00401230 |. 33D2 XOR EDX,EDX
00401232 |. F7F1 DIV ECX
00401234 |. 2D DB030000 SUB EAX,3DB
00401239 |. B9 64000000 MOV ECX,64
0040123E |. F7E1 MUL ECX
00401240 |. 83F8 00 CMP EAX,0
00401243 |. 75 06 JNZ SHORT crkme05.0040124B
00401245 |. B8 01000000 MOV EAX,1
0040124A |. C3 RETN
0040124B |> 33C0 XOR EAX,EAX
0040124D \. C3 RETN

push命令(入力パスをpushしている)の後にcall命令があります。
call のすぐ後には比較命令(test,cmp)がありません。
「F8」でとばしてしまいましょう。

eax レジスタの値に注目してみましょう。
eax = 181CD 十進数に直すと98765です。

これより先は四則演算が登場します。
eax レジスタには入力パスの値が入っています。
では call の次の命令を見てみましょう。

00401226 |. 05 D2040000 ADD EAX,4D2

addは足し算です。eax に 4D2(十進数:1234)を加えます。
eax レジスタの値は 181CD から 1869F になったはずです。

次に

0040122B |. B9 0D000000 MOV ECX,0D

movは値の複写、ecx レジスタに0D(十進数:13)を代入しています。

00401230 |. 33D2 XOR EDX,EDX

これは edx レジスタの初期化です。なぜ初期化するのか?

次にくるのは
00401232 |. F7F1 DIV ECX
div はディヴァイド、わり算です。これはちょっとやっかいです。
div ecx としか書かれていませんが、上位4バイト(edx)、下位4バイト(eax)/ecx です。

例えば、 eax = 00112233 , edx = 44556677 , ecx = AABBCCDD と値が入っている場合、
4455667700112233 / AABBCCDD = ?????? となるわけです。
edx レジスタと eax レジスタとが繋がっているのです。


まとめ、ではなく実際の場合。
004011FFのTEST EAX,EAXは、eaxをフラグ、つまり0かそうではないかを判断しています。
ではこのEAXはどこで変化しているのでしょうか?
0040121Cにジャンプし、まずはずーっと下のRETNを探します。今回はすぐ見つかるけどね。
そこからさかのぼってeaxをいじっている場所を探します。
0040124Bにアタックする、というアプローチになると思います。


ecx で、 1869F(98765+1234) / 0D(13) が行われます。
ここで気になるのが商と余り。div命令では eaxには商、 edxには余りが入ります。(eax = 1E0C , edx = 3)

次の命令は sub eax, 3DB です。
subは引き算です。eax - 3DB(987)が行われます。引き算の結果はこの場合 eax レジスタに入ります。

次は mov ecx, 64
ecx レジスタに 64(100)を代入しています。

そして、mul ecx
mul はかけ算です。ecx しかありませんが、eax * ecx が行われます。

肝心の計算結果ですが、div(除算)と同じように eax レジスタと edx レジスタにまたがって格納されます。
乗算結果が ABCDEF1234567890 なら、eax = 34567890 , edx = ABCDEF12 といったふうに格納されます。

最後に cmp eax, 0, jnz short~ とあります。先ほどの乗算結果をうけて、eax = 0 であるかどうかを調べています。
ここでは eax = 1 でcall命令を返れば良いので、ジャンプしたらダメです。

すると、jnz 命令から eax = 0 になればオーケーということになります。
計算が多くてややこしいかもしれませんが、何が行われているかを落ち着いて見ていきましょう。

計算の流れを以下に示します。
add eax, 1234 eax = 入力パス + 0x1234 (0x は16進数の意味。C言語でおなじみ)
div ecx eax = eax / 13
sub eax, 987 eax = eax - 0x987
mul ecx eax = eax * 100

これをまとめると、(((1234 + x) / 13) - 987) * 100 = 0 を満たす x を求めればいいわけです。