CTFやセキュリティコンテストで出題される問題を解いたり、チート対策等のセキュリティ対策を行うなど、セキュリティ対策技術を向上させる目的のために「解析魔法少女美咲ちゃん マジカル・オープン!」を読み、書籍の解説を元にざっくりと手を動かした読書メモです。見出しの「レベル01~」は、独自に括り直したもので、書籍の目次見出しとは連動してません。(途中スキップなどある)
筆者:坂本 昌彦
書籍サイト
- 「解析魔法少女美咲ちゃん マジカル・オープン!」
- 著者サイト
- 書籍中で使われている crackme01/02.exe については上記の書籍サポートサイトか、著者サイトからダウンロードできる。
- なお、こちら、どうも書籍後半で解説されている crackme03.exe が入ってないっぽい???
本読書メモの作業環境:
- Windows 10 Pro 64bit 日本語版 (バージョン 1709 / 1803)
- OllyDbg 1.10
- OllyDbg 日本語化パッチ OllyDbg110J3a.lzh
- Stirling Version 1.31
- 本読書メモおよびサンプルコードは x86 (32bit) 環境でのみ検証しています。
- 本読書メモおよびサンプルコードの内容は、CTFやセキュリティコンテストで出題される問題を解いたり、チート対策等のセキュリティ対策を行うなど、セキュリティ対策技術を向上させる目的のために調査・検証したものとなります。犯罪や不正行為を助長するものではありません。
- 本読書メモおよびサンプルコードの内容あるいはその内容から得た知識をもとに、一般的に流通するソフトウェアやサービスの利用規定に反する行為を計画・実践した場合、罪・違反・侵害・損害賠償などを問われ、罰せられる可能性があります。
- 本読書メモおよびサンプルコードの内容あるいはその内容から得た知識をもとにしたいかなる行為も、またそれにより発生したいかなるトラブルや損害についても、筆者および筆者が所属する、あるいは筆者が過去所属した会社は一切の責任を負いかねます。
- ターゲット
- crackme01.exe の「ヘルプ」→「ユーザー登録」で入力する正しいパスワードを探し出す。
- 学習の狙い
- バイナリエディタで文字列検索するやり方を学ぶ。
- 必要なソフト : バイナリエディタ。書籍では Stirling を使用。
- やり方
- crackme01.exeの「ヘルプ」→「ユーザー登録」で適当な文字列を入れると「BAD PASS」というタイトルで「パスワードが誤っています」というメッセージボックスが表示される。
- そこで、crackme01.exe をバイナリエディタで開き、「BAD PASS」文字列で検索する。
- crackme01.exeは特に文字列リソースや文字列定義を分散したり暗号化していないため、"BAD PASS"付近であっさりと正しいパスワード文字列っぽいのが発見できる。
- exe中の文字列が文字化けしていなければ(crackme01.exeの場合はShift_JIS)、「パスワード」でもそのまま検索できる。
TIPS:
- Stirling が表示に使う文字コードを変更するには、「設定」→「キャラクターセット」→ 文字コード名で切り替えられる。
- ターゲット
- crackme01.exe でシリアル番号入力箇所をクラックし、どんな文字列を入力しても、正しいシリアル番号を入力したときの動作に書き換えること。
- 学習の狙い
- OllyDbg をインストールして日本語化する。日本語文字列を表示できるようにする。
- OllyDbg の基本的な使い方の習得と、書き換えた実行ファイルの保存方法の習得。
- 必要なソフト : OllyDbg (日本語化パッチで日本語文字列認識 + メニュー日本語化をする)
- OllyDbg の入手 : 32bit版の OllyDbg 1.10 をDLする。
- http://www.ollydbg.de/ -> "Download" から "Download OllyDbg 1.10" リンクをクリックしてDLする。
- zipファイルを展開し、まずはollydbg.exeを実行して動作確認する。
- 注意 : 管理者権限で起動すること
- 初回起動時に、Win10の場合、PSAPI.DLL, DBGHELP.DLL についてOllyDbg添付のファイルはバージョンが古く、削除するか確認のダイアログボックスが表示される。
- (この時点で WinDbgを入れていなければ DBGHELP.DLL については聞かれないかも)
- → 「はい」でも良いし、一旦「いいえ」にして当該ファイルを "_old" などにリネームしてから再度実行しても良い。
- どちらにしても、OllyDbg添付の PSAPI.DLL, DBGHELP.DLL はバージョンが古いため、無い方が良い。
- 日本語化プラグインの入手
- https://hp.vector.co.jp/authors/VA028184/#TUTORIAL -> 「OllyDbg1.10用日本語化パッチ」リンクをクリック。
- (本メモ記述時点では
OllyDbg110J3a.lzh
をDL) - lzhファイルを展開して、
Ver1.10J.txt
記載の手順で日本語化する。 - OLLYDBG.EXEが更新されたあと、一度設定ファイル(
ollydbg.ini
)を削除して設定をクリアしてから、日本語フォント設定を行う。- まず「オプション」→「環境設定」の「フォント」タブを開き、「フォント」プルダウンから Font 5 ~ 7 を選択する。その後「変更」ボタンからフォントを変更する。
- ここの「名変更」は「Font 7」などのラベルを変更するので、適当に修正する。
- 続いて適当なEXEを開き、CPUウインドウやメモリウインドウなど、 ウインドウ毎に 右クリック →「環境設定」→「全てのフォント」→フォント名、という形で設定していく。
- つまり OllyDbg では各ウインドウ毎にどのフォント設定を使うのか別々に管理しているため、一通りざっとウインドウを開いて、それぞれ設定が必要ということ。
- OllyDbg を管理者権限で実行し、crackme01.exe を開く。
- 「解析」→「実行(F9)」する。
- crackme01の「ヘルプ」→「ユーザー登録」で適当な文字列を入れ、「BAD PASS」というタイトルで「パスワードが誤っています」というメッセージボックスが表示された状態にして、そこで OllyDbg 側で「解析」→「一時停止(F12)」を実行する。
- 本読書メモでの作業環境では、 win32u.dll の中のRETNで止まった。レジスタからESPを確認し、ESPのアドレスを右クリック→「スタック画面へ」でスタックを確認する。
- ここでスタック画面をアドレスの大きい方向へスクロールしていく。これはスタックを遡っていくことに相当する。
- かなり下まで遡っていくと、crackme01.exe内の戻り先アドレスが出てくるので、そこにブレークポイントを設定する。
- あるいはOllyDbgの「表示」→「コールスタック」ウインドウを使うと crackme01.exe の呼び出し元までコールスタックがきれいに表示されるので、そこで一気に辿れる。
- さらにショートカットするなら、「コールスタック」の「コール元」右クリックで「リターンまで実行」すれば一気にそこまで実行できる。(ブレークポイントを設定する必要すらない)
- ブレークを設定するなり「リターンまで実行」を選択するなりしたあと、crackme01の画面に戻りメッセージボックスで「OK」ボタンをクリック -> OllyDbg が
MessageBoxA
からRETURNした直後で止まる。 - 止まったところを上に遡っていくと、
lstrcmpA
を呼び出しているところがある。ここでlstrcmpA
の後にTEST EAX, EAX
->PUSH 0
->JNZ SHORT crackme01.0040113D
の流れがある。 - ここで、JNZをNOPに修正すれば、そのまま正常なシリアル番号登録のフローに進む。
- CPUウインドウからJNZの命令を右クリックして「逆アセ修正(Space)」で簡易アセンブルウインドウが表示されたら NOP に変更する。(間違えたらAlt + Backspaceで変更取り消しできる)
- 一点注意が必要なのは、JNZをNOPに変えてそのまま進んだ場合、本来なら
TEST EAX, EAX
で演算結果がゼロになった状態をプログラマは想定している。 - よって、ここでEAXをゼロクリアするために、
TEST EAX,EAX
をXOR EAX,EAX
に書き換える。(詳細はTESTやXOR命令のリファレンス参照)
- 動作確認できたら、CPUウインドウのDisassmebler領域で右クリック→「実行ファイルへコピー」→「全ての変更箇所」で全てコピーを選ぶ。すると変更された状態のディスアセンブルされたファイルウインドウが表示されるので、右クリックして「ファイル保存」で別の名前で保存する。
crackme01.exe, crackme02.exe を題材に、OllyDbgの色々な機能を使ってみる。
ターゲット : MessageBoxA
のような外部DLLのAPI呼び出しにブレークポイントを設定してみる。
- OllyDbg を管理者権限で実行し、crackme01.exe を開く。
- メッセージボックスが表示されたところで一時停止する。
- コールスタックから crackme01 の呼び出し元まで進める。
- その付近の
MessageBoxA
の呼び出しを見つけて、右クリック→「検索」→「ラベル名」でラベル名を開く。 - インポートテーブルが表示される。"MessageBox"とキー入力するとそのままインポートテーブルを検索できる。
USER32.MessageBoxA
が見つかったら、右クリックして「インポート関数の逆アセンブラ表示」をしてみると、実体コードのDisassemblerが表示される。- また、右クリックして「インポート関数の一覧表示(Enter)」すると「参照データ」が表示され、APIを呼び出している箇所を逆引きできる。
- ここで右クリック→「インポート関数にブレークポイントをセットT」すると、実体コードの先頭にブレークポイントが設定される。これにより、その外部API呼び出し全てでブレークポイントが設定できたことになる。(実際試してみること)
ターゲット : トレース実行の使い方を確認する。
- OllyDbg を管理者権限で実行し、crackme01.exe を開き、
MessageBoxA
のインポート関数にブレークポイントをセットし、適当なシリアルコードを入力してMessageBoxA
でブレークさせる。 - コールスタックから crackme01 の戻り先の次の命令にブレークポイントを設置し、「解析」→「トレース実行(F12)」を実施。
- crackme01側のメッセージボックスで「OK」をクリックして閉じると、OllyDbg側でcrackme01に戻ってきたところでブレークする。
- 「表示」→「ライントレース」を開くと、
MessageBoxA
から crackme01 に戻ってくるところまでのトレースが表示されるので、中身を見てみたりして楽しむこと。 - 「解析」→「ライントレース実行終了」を選択すると、ライントレースが終わり、トレースログもクリアされる。
ターゲット : 各種参照機能を使えるようになる。
- OllyDbg を管理者権限で実行し、crackme01.exe を開き、適当なシリアルコードを入力してメッセージボックスが表示したところで一時停止→コールスタックから crackme01 の呼び出し元まで戻る。
- Disassembler画面を少し上に戻り、メッセージボックス表示の文字列をPUSHしているところを探す。メモリダンプ画面にフォーカスを写し、Ctrl-Gでそのアドレスに移動する。
- アドレスに移動できたら、右クリック→「参照を検索」で、実際にPUSHしているところが「参照データ」ウインドウに出てくることを確認する。
- Disassembler画面に移り、
MessageBoxA
をCALLしているところでENTERし、MessageBoxA
の実装コードに移動することを確認する。- その後
-
(マイナス, ハイフン) キーを押すと、戻ることを確認する。 - 右クリック→「移動元にセット」でEIPが変更され、いくつかDisassembler画面上で移動したあとで
*
を押すと移動元に戻ることを確認する。
- その後
ターゲット : メモリ Read/Write 時にソフトブレークする方法を学ぶ。
- OllyDbg を管理者権限で実行し、crackme01.exe を開き、適当なシリアルコードを入力してメッセージボックスが表示したところで一時停止→コールスタックから crackme01 の呼び出し元まで戻る。
- Disassembler画面を少し上に戻り、メッセージボックス表示の文字列をPUSHしているところを探す。メモリダンプ画面にフォーカスを写し、Ctrl-Gでそのアドレスに移動する。
- 右クリック→「ブレークポイント」→「メモリアクセス」をクリックする。実行を再開し、対象のメモリにアクセスしたところでブレークすることを確認する。
- 確認できたら、メモリダンプ画面にフォーカスを移して右クリック→「ブレークポイント」→「メモリブレークポイント解除」で解除されることを確認する。
- 何回か試してみて、一度に一箇所しか設定できないことを確認する。
ターゲット : ハードウェアブレークポイントの使い方を学ぶ。
- OllyDbg を管理者権限で実行し、crackme01.exe を開き、適当なシリアルコードを入力してメッセージボックスが表示したところで一時停止→コールスタックから crackme01 の呼び出し元まで戻る。
- Disassembler画面を少し遡り、
lstrcmpA
をCALLしているところを右クリック→「ブレークポイント」→「ハードウェアブレークポイント」をクリックする。 - 実行を再開し、もう一度適当なシリアルコードを入力→ハードウェアブレークポイントを設定した箇所でブレークし、そこからステップ実行などできることを確認する。
- 「解析」→「ハードウェアブレークポイント」で、現在のハードウェアブレークポイントが表示され、Disassembler画面を表示したり解除できることを確認する。
- 「BAD PASS」文字列の先頭でBYTEでハードウェアブレークポイントの読み出しを設定してみて、実際にメモリ読み出しタイミングでブレークすることを確認する。
ターゲット : xj10n.dll による日本語文字列参照の抽出を試す。
(OllyDbg 日本語化プラグイン導入作業で xj10n.dll が導入済みの状況を想定)
- OllyDbg を管理者権限で実行し、crackme02.exe を開き、実行し、「使用期限切れ」というタイトルで「2004年5月1日を過ぎると使えません」というメッセージボックスが表示されるのを確認したら、「OK」ボタンをクリックして終了する。
- 終了後、OllyDbgに戻りコールスタックウインドウからcrackme02モジュール内の呼び出し元をクリックし、Disassembler画面で crackme02 のコード領域を表示する。
- Dissassembler画面で右クリック → 「参照」→「全ての参照文字列」を選択すると、crackme02 で参照されている全ての参照文字列が表示されることを確認する。
- 注意 : 「参照」→「全ての参照文字列」で表示されるのは、右クリックした箇所のモジュール内となる。そのため、外部DLLモジュールのDisassembleが表示された箇所で参照文字列を表示したら、そのDLLモジュール内での参照文字列一覧が表示される。
- そこから「使用期限切れ」などの文字列を参照している箇所に移動し、使用期限切れのMessageBox表示をしているコードを探す。
- その箇所の少し上に遡ると
RETN
→PUSH 0
→PUSH (「使用期限切れ」アドレス)
の並びが出てくる。つまりこのPUSH 0
にジャンプしている箇所があると想像できるので、PUSH 0
命令を右クリック→「参照を検索」→「選択コマンドへの参照」をするとそこにJA/JNBしている箇所が出てくるので、その箇所(とその少し手前のコード)を見てみる。 - すると
GetLocalTime
→ いくつか比較処理して JA/JNB しているコードが見つかる。GetLocalTime
のCALLにブレークポイントを貼ってみてステップ実行してみる。 GetLocalTime
は引数にSYSTEMTIME
構造体へのポインタを渡す。前後のコードを見てみると、スタック上に構造体領域を確保してそのポインタをECXレジスタで渡している様子。呼び出した後、構造体のWORD wYear
をAXに取り出してからCMP AX,7D4
しているところがある。7D4
を10進に戻すと 2004 となるので、恐らくまず年で比較しているものと推測できる。- さらにその下を見てみると、
CMP WORD PTR SS:[ESP+6], 5
というコードに続いて 先ほどのPUSH 0
への JNB 命令も見える。これが恐らく5/1を超えたら、の条件分岐と思われる。 - 詳細は実際に手を動かしてコードを見てもらうとして、結論から書くと
CMP AX, 7D4
の即値部分を 9999 年のHEX値 0x270F に変更すれば良い。 - その結果、日時チェックをPASSして、crackme01と同じウインドウが表示されればOK.
- crackme02.exeで、時刻取得APIについてIAT Hijackを行い時刻をmockingしてみる。
- crackme02.exeでは
GetLocalTime()
を呼び出し、2004年5月1日を過ぎるとメッセージボックスを表示して終了してしまう。- それより前なら、crackme01 のウインドウを表示する。
- そこで、
GetLocalTime()
呼び出し自体をHOOKしてみる。 GetLocalTime()
は kernel32.dll で提供されていて、SYSTEMTIME構造体への long pointerを引数に受け取り、そこに現在時刻状を埋めて返す。
※書籍では「期限切れを書き換えちゃうゾ 後編」にあたる。
- 年の部分をかならず2000年にして返す
GetLocalTime()
を kernel3_.c にMyGetLocalTime()
として作成する。- ポイント1 : オリジナルの
GetLocalTime()
はLoadLibrary() + GetProcAddress()
で手動で取得する。(このCソース自身がGetLocalTime
を exportsする予定なので、implicit linkは使わない) - ポイント2 :
MyGetLocalTime()
のシグネチャをオリジナルにあわせ、呼び出し規約は Win32 API で使われている__stdcall
を明示する。
- ポイント1 : オリジナルの
dumpbin /imports crackme02.exe
を実行し、crackme02.exe が kernel32.dll からインポートしているシンボル名の一覧を抽出する。- ちなみに、
dumpbin /exports kernel32.dll
して、kernel32.dll のEXPORTSから作ろうとしたら、300個近くがうまくリンクできず、原因も調査できなかったためギブアップ。 - よって、crackme02.exe の IMPORTS から kernel32.dll を参照する必要最低限のシンボルを抜き出して、そちらからForwarding用のDEFを作成する。
- ちなみに、
- 抽出したシンボル名を
(シンボル名) = kernel32.(シンボル名)
となるよう整形する。つまりオリジナルへの転送設定だけにしておく。 GetLocalTime
のみGetLocalTime = MyGetLocalTime
としておく。 すなわち、 kernel3_.c 中のMyGetLocalTime()
を呼び出すように設定する。- 以下のヘッダを先頭に挿入し、
kernel3_.def
として保存する。
LIBRARY kernel3_.dll
EXPORTS
- ビルド :
cl /W4 /Od /nologo /LD kernel3_.c kernel32.lib /link /DEF:kernel3_.def
- crackme02.exe をコピーし、コピーした方をバイナリエディタで開き、"KERNEL32" 文字列を検索して "KERNEL3_" に変更する。
- kernel3_.dll を同じフォルダにコピーして実行。
kernel32.dll の GetLocalTime()
は、実際には kernelbase.GetLocalTime()
への絶対間接参照のJMP命令になっている。
そこで、ジャンプ命令およびジャンプ先アドレスをカスタマイズした MyGetLocalTime()
に変更する。
- まずはテスト用に 自分のプロセス内で書き換えるテストコード, hook_self.cを作成。
- 参考 : hook_rensyu.c
- ビルド
cl hook_self.c
-> 動作確認OK. - JMP書き換えをDLLに分離 : hook.c -> ビルド :
cl /LD hook.c
- 実際にロードしてみる hook_dll_test.c 作成、ビルド :
cl hook_dll_test.c
-> 動作確認OK. - "crackme02.exe" のプロセスを探して "hook.dll" を "CreateRemoteThread() -> LoadLibrary()" する inject.c を作成、ビルド :
cl inject.c
- crackme02.exe は実行してすぐ
GetLocalTime()
を呼びに行く。そのため、今回はOllyDbgで一旦エントリポイントで止めて、そこで inject.exe を実行・・・しようとしたが、OllyDbgが掴んでいるためかOpenProcess()
に失敗。- 「うさみみハリケーン」ならアタッチしたプロセス中に任意のDLLをロード/アンロードできる。しかも実行時にエントリーポイントで一時停止もできるので、こちらを試す。
- 「うさみみハリケーン」で "ファイル" -> "ファイル名を指定して実行" 、この時に「エントリーポイントで一時停止」オプションにチェックを入れて crackme02.exe を開く。
- 「プロセスをエントリーポイントで停止しました」メッセージボックスが出たらOKをクリックし、そこから改めて "ファイル" -> "プロセスを選択" で crackme02.exe を選択。
- "デバッグ" -> "DLLのロードとアンロード" を選択して "DLL のロード" で hook.dll を参照して "ロード実行"、"終了"でDLLロード/アンロードのダイアログウインドウを閉じる。
- "ファイル" -> "プロセスを再開" し、"crackme01" の画面が表示されれば成功。
- PEフォーマットやアセンブラについてある程度予備知識がないと追いつくのが難しいが、逆に、ある程度の予備知識があればOllyDbgなどのチュートリアルとしてよくまとまっていると感じた。
- 64bitアーキテクチャ全盛の現代、OllyDbg2 で64bit対応も進んでおり、どのようにツールが進化したのか時間があれば調査してみたい。