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

Ollyは立ち上げてcrkme02.exeを開いてください。
画面左上には逆アセンブルリストが表示されていますが、
ここで右クリックして検索→ラベル一覧 を選択して下さい。
すると、なにやら命令のリストが出力されるので、その中のGetWindowTextAを選択して下さい。


Names in CRKME02
Address Section Type ( Name Comment

00401000 .text Export
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

Get~上で右クリック「全ての参照をブレークポイントにセット」を選択して下さい。
すると、GetWindowTextAにブレークポイントがセットされるはずです。

BPかける時に、OSが2kまたは、XPの方は全てにBPかけた後に、
「jmp dword ptr ds:[<&USER32.GetWindowTextA>]」の行選択してF2で解除してください。

0040128A $-FF25 0C204000 JMP DWORD PTR DS:[<&USER32.GetWindowTextA>]    ←ココ

ここ解除しないと、OS部分に行ってしまいます。

では、ブレークポイントは仕掛けてありますね?
F9を押してプログラムを実行します。
crackme#02ウィンドウが現れました。
crackme#02ウィンドウ上のパス入力欄に適当なパスワード(フェイクパス)を入れて登録ボタンを押しましょう。
OllyDbgに制御が移ります。

004011E8 > 6A 7E PUSH 7E ; /Count = 7E (126.)
004011EA . 68 2C304000 PUSH CRKME02.0040302C ; |Buffer = CRKME02.0040302C
004011EF . FF35 04304000 PUSH DWORD PTR DS:[403004] ; |hWnd = 0000036C (class='Edit',wndproc=0040102B,parent=00000364)
004011F5 . E8 90000000 CALL <JMP.&USER32.GetWindowTextA> ; \GetWindowTextA       ←ココ
004011FA . 83F8 08 CMP EAX,8
004011FD . 75 22 JNZ SHORT CRKME02.00401221
004011FF . E8 36000000 CALL CRKME02.0040123A
00401204 . 85C0 TEST EAX,EAX
00401206 . 74 19 JE SHORT CRKME02.00401221

004011F5 のGetWindowTextAで止まりましたか?
それでは、F8で抜けます。

00401272 $-FF25 28204000 JMP DWORD PTR DS:[<&USER32.CallWindowProcA>]
00401278 $-FF25 18204000 JMP DWORD PTR DS:[<&USER32.DialogBoxParamA>]
0040127E $-FF25 1C204000 JMP DWORD PTR DS:[<&USER32.EndDialog>]
00401284 $-FF25 14204000 JMP DWORD PTR DS:[<&USER32.GetDlgItem>]
0040128A $-FF25 0C204000 JMP DWORD PTR DS:[<&USER32.GetWindowTextA>]      ←ココ
00401290 $-FF25 10204000 JMP DWORD PTR DS:[<&USER32.LoadIconA>]
00401296 $-FF25 2C204000 JMP DWORD PTR DS:[<&USER32.MessageBoxA>]
0040129C $-FF25 30204000 JMP DWORD PTR DS:[<&USER32.SendMessageA>]
004012A2 $-FF25 20204000 JMP DWORD PTR DS:[<&USER32.SetWindowLongA>]
004012A8 $-FF25 24204000 JMP DWORD PTR DS:[<&USER32.ShowWindow>]
004012AE $-FF25 04204000 JMP DWORD PTR DS:[<&KERNEL32.ExitProcess>]
004012B4 $-FF25 00204000 JMP DWORD PTR DS:[<&KERNEL32.GetModuleHandleA>]

この2カ所目のブレークポイントでも引っかかるのでもう一度F8を押します。
OSが2kまたは、XPの方は先程ここのブレークポイントを解除していますので、先に進んでいると思います。
cmp eax,8の行に行って下さい。

004011FA . 83F8 08 CMP EAX,8          ←ココ
004011FD . 75 22 JNZ SHORT CRKME02.00401221
004011FF . E8 36000000 CALL CRKME02.0040123A
00401204 . 85C0 TEST EAX,EAX
00401206 . 74 19 JE SHORT CRKME02.00401221

この命令は eax レジスタの値と 8 を比較しています。
eax レジスタには入力した文字の文字数が入っています。
これは GetWindowTextA の戻り値です。

次の jnz 命令は直前の cmp 命令で、eax と 8 が正しく無い場合にジャンプするという命令です。
ここで飛んだら登録失敗になるので、とりあえず、無理矢理有効にします。
画面右上のRegistersの真ん中辺に、CPAZSTDOといったアルファベットが縦に並んでいます。
この内、 Z の横にある数字が 0 ならば、数字をクリックして 1 にしましょう。
こうすると、ゼロフラグが立つので、無理矢理分岐を曲げることが出来ます。


次に call, test, je 命令があり、チェックはこれで終わっています。
となると、チェックルーチンは call の中にあるので、F7キーを押して call 内部に入る必要があります。
何度かF7を押すとcmpとjnzが交互に続くルーチンに入ったはずです。

0040123A /$ 33C0 XOR EAX,EAX            ←レジスタを初期化
0040123C |. BE 2C304000 MOV ESI,CRKME02.0040302C ; ASCII "12345678"  ←現在のフェイクパス
00401241 |. 803E 35 CMP BYTE PTR DS:[ESI],35     
00401244 |. 75 2B JNZ SHORT CRKME02.00401271
00401246 |. 807E 01 45 CMP BYTE PTR DS:[ESI+1],45
0040124A |. 75 25 JNZ SHORT CRKME02.00401271
0040124C |. 807E 02 48 CMP BYTE PTR DS:[ESI+2],48
00401250 |. 75 1F JNZ SHORT CRKME02.00401271
00401252 |. 807E 03 39 CMP BYTE PTR DS:[ESI+3],39
00401256 |. 75 19 JNZ SHORT CRKME02.00401271
00401258 |. 807E 04 56 CMP BYTE PTR DS:[ESI+4],56
0040125C |. 75 13 JNZ SHORT CRKME02.00401271
0040125E |. 807E 05 33 CMP BYTE PTR DS:[ESI+5],33
00401262 |. 75 0D JNZ SHORT CRKME02.00401271
00401264 |. 807E 06 51 CMP BYTE PTR DS:[ESI+6],51
00401268 |. 75 07 JNZ SHORT CRKME02.00401271
0040126A |. 807E 07 57 CMP BYTE PTR DS:[ESI+7],57
0040126E |. 75 01 JNZ SHORT CRKME02.00401271
00401270 |. 40 INC EAX
00401271 \> C3 RETN


では、トレースして行きます。
xor eax, eax で eax レジスタを初期化
mov esi, CRKME~ で esi レジスタに入力パスの開始アドレスが入ります。
esi は文字列の先頭アドレスを指します。 esi+1 は文字列の先頭アドレス+1、つまり2文字目を指すわけです。
同様に esi+2 は3文字目、esi+3は4文字目を指します。
よく使われる手法ですね、Cで言えばポインタです

こうやって全ての文字が一致した場合、inc eax を実行して ret 、call 元へ戻ります。
inc eax は eax の値に1を足せ、という命令です。C言語風に書くと eax++ でしょうか。
ここで、文字列が一致すると、eax = 1, 一致しない場合は eax = 0 という風になります。

次に cmp 命令です。cmp byte ptr ds:[esi], 35 これは、esi レジスタの指す値
つまり入力文字列の先頭と 35 =アスキー文字"5" を比較しています。
アスキー文字は文字コード表を見るとわかります。

cmp byte ptr ds:[esi], 35 の byte ptr とは何でしょうか。
byte ptr は先頭1バイトのみを取り出すという意味です。つまり、esiレジスタの指す文字列から1文字だけを比較します。

次に jnz で、比較結果が正しくない場合はジャンプして終了、正しければジャンプせずにチェックを続行します。
あとは同じことの繰り返しなので、逆汗リストを見るだけで答えが出せます。

35 45 48 39 56 33 51 57 = "5EH9V3QW" です。


F7連打で call 元に復帰しましょう。(パスが間違っていてもいいです)

00401204 . 85C0 TEST EAX,EAX
00401206 . 74 19 JE SHORT CRKME02.00401221
00401208 . 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
0040120A . 68 EF304000 PUSH CRKME02.004030EF ; |Title = "登録情報"4030
0040120F . 68 05314000 PUSH CRKME02.00403105 ; |Text = "正解です!"40310
00401214 . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hOwner
00401217 . E8 7A000000 CALL <JMP.&USER32.MessageBoxA> ; \MessageBoxA
0040121C .^E9 7DFEFFFF JMP CRKME02.0040109E
00401221 > 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401223 . 68 EF304000 PUSH CRKME02.004030EF ; |Title = "登録情報"4030
00401228 . 68 F8304000 PUSH CRKME02.004030F8 ; |Text = "不正解です。"030F8
0040122D . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hOwner
00401230 . E8 61000000 CALL <JMP.&USER32.MessageBoxA> ; \MessageBoxA
00401235 .^E9 64FEFFFF JMP CRKME02.0040109E


test eax, eax je short CRKME02.~ という命令が待ち受けています。
crkme01でも同じような命令が有りましたが、test eax, eax の次に je がくると、eax = 0 ならジャンプ、それ以外はジャンプしません。
この場合、ジャンプすると登録失敗になるので、call 命令の結果 eax = 1 になれば良いのです。


crackme#02のまとめ

GetWindowTextAで文字列を取得、文字数が8文字かを比較する。

004011F5 . E8 90000000 CALL <JMP.&USER32.GetWindowTextA> ; \GetWindowTextA
004011FA . 83F8 08 CMP EAX,8
004011FD . 75 22 JNZ SHORT CRKME02.00401221
004011FF . E8 36000000 CALL CRKME02.0040123A
00401204 . 85C0 TEST EAX,EAX
00401206 . 74 19 JE SHORT CRKME02.00401221

比較して何処へ行くかを見る。
これで正しければチェック続行。次に call 命令で文字列を比較する。
call 内部ではまずxor eax, eax で eax レジスタの値を初期化。

次に cmp 命令で1文字ずつ比較する。
1文字でも間違っていたらチェックを終了して call から復帰、1文字ずつのチェックを8回行う。
全て正しいとき、eax に 1 が入る。(inc eax)

最後に eax の値を比較して eax = 1 なら登録完了。

JNZ SHORT crkme02.00401271というのが連続していますが、複数の場所から一箇所のアドレスに条件ジャンプしているというのは、
条件ジャンプ先がエラーメッセージ行きになっているということを指します。
大事なのは、『逆に言えば、「00401271」を逆アセンブリリストから検索すれば、
トレースしていって発見できなかったエラー行きルーチンを全て潰すことができる!』ということです。
これはパッチ当てのテクニックの一つ(だと思う)ですから覚えておいてください。

MOV EAX,1 /RETNに書き換えて全てのパスで正解など(^^;