仮想私事の原理式

この世はワタクシゴトのからみ合い

【勉強メモ】解析少女美咲ちゃん (やねう解析チーム)

感想

今まで部分部分は読んではいたけど、全部を通して読んでいなかったので、今更ながら通読。 2004年発刊で紙媒体は既に絶版。内容は古くなってしまったけど、うちの職場ではマルウェア(プログラム)解析の良き入門書として、最初に勧められる。かなり分かりやすく、ハッキング入門、デバッガ入門書としてベストなレベル感であり、正直萌え系にする必要は全くなかった本と評されることも多いが、あの萌え要素のお陰で心理的なハードルが下げられていると思う。「兄」の質問(突っ込み)が、いい感じに初心者の疑問点や理解が怪しい点をカバーしており、「兄、けっこう鋭いじゃん」と思ったりする。

1 文字列を探しちゃえ!!

  • バイナリファイルに埋め込まれている平文文字列は、プログラム解析の手がかりになる。
  • バイナリから文字列を探すツール
  • マルチバイト文字列(ASCII、Shift-JIS等)とUnicode(ワイド文字。UTF-16。2byte文字) の違い
    • Windows内はUnicodeがデフォルトなので注意。PEヘッダはASCIIだけど…。

2 条件分岐を書き換えちゃえ!

  • プログラムは実行時にハードディスク(ストレージ)からメモリにロードされ、メモリ上で実行される
  • デバッガ(Ollydbg等)はメモリの中を見て、実行中のプログラムを解析する
    • メモリ上の位置や中身を調べたり、命令を一つずつ実行させたり、命令を書き換えたり…
  • メモリ上の命令は原則として命令のアドレスが増える方向に順次実行されていく(上から下)。
  • JMP(ジャンプ)系命令やCALL命令を使えば、離れたアドレスへ戻ったり進めたりできる。
    • Jで始まる命令は何らかの条件分岐でジャンプする命令(JMP、JZ、JNZ等)。
    • JZとJEはマシンコード同じ。
  • クラックの基本は、条件分岐を書き換えること。
  • CALL命令の場合、ジャンプ元へ戻ってくる。戻る先は飛ぶ時にスタックに積んでいる。
    • スタックに積んだ戻り先アドレスは、RET(Return)命令で使われる。
  • CMP命令は「引き算したつもりになる」命令
    • CMP a,b で a-b=0ならばゼロフラグ(ZF)をセット(1)。ただしa、bの値は実際には変化しない

3 マシンコードを解読しちゃえ!

レジスタ

CPU内にある記憶領域。x86では主なものが10個。各サイズは32bit(4byte)。 EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP, EIP, EFLAG(他にも、セグメントレジスタ等) ※ ちなみに64bit(x86-64)の主なレジスタが以下の17個あり、各サイズは64bit(8byte)。 RAX~RDX, RSI, RDI, RBP, RSP, R8~R15, RFLAG  

■ 補数表現

先頭の1bitがセットされている場合(1byteなら0x80~0xFF)は、マイナスの数として扱う。 桁合わせは、通常は頭に0を付けるが、補数表現では頭にFを付けて桁を揃える(F2 ⇒ FFFFFFF2)。

■ TEST命令

「AND演算をしたつもりになる」命令(CMP a, 0 と結果は同じ) TEST EAX, EAX ではEAXがゼロでない限りZF=0となるので、JNZと組み合わせるなど。

■ " DWORD PTR DS:[408378] " の意味
  • DWORD PTR ⇒ 「DWORD(4byteデータ)のポインタ(先頭アドレス)とみなす」
  • DS: ⇒「データセグメントの~」
  • [408378] ⇒「メモリアドレス0x408378にあるデータ」([, ]で囲われた値は基本的にアドレス扱い)

よって、「データセグメントのメモリアドレス0x408378から始まる4byte分(DWORDサイズ)のデータ(をリトルインディアンで)」となる。

■ マシンコードを書き換え
  • 書き換え前後でマシンコードのバイト数が変わらないようにする。
  • 基本的に上書きなので、後ろの必要な命令まで潰してしまう。
  • 挿入もできるが、プログラムサイズが変わってサイズチェックがあると検知されたり…色々面倒

4 デバッガで追いかけちゃえ!

サンプルプログラム「crackme01.exe」を実際にデバッガ(Ollydbg)で見ながら、パスワード保護をクラックしていく。本書内ではバージョン1.10だが、自分は2.01を使った。 大まかな流れとしては、

  1. パスワード不一致と判断された後の命令を見つける(本書内では不一致の旨のメッセージボックス)
  2. そこから命令をさかのぼり、パスワードチェックをしている処理を見つける。
  3. パスワード不一致側に進まないように命令を書き換える。

ざっくり書くと簡単だけど、条件分岐を潰すだけで良い場合もあれば、その他のチェック機構や、正常動作に必要な処理は実行させなければならない等、そう単純でもない。

■ Ollydbgの使い方 
メニュー ショートカット 説明
実行(Run) F9 プログラムを実行する
一時停止(Pause) F12 プログラムを一時停止する
ステップ実行
(Step over)
F8 命令を一つ実行する(CALLされた関数には入らない)
詳細ステップ実行
(Step into)
F7 命令を一つ実行する(CALLされた関数に入っていく)
ブレークポイント F2 (ソフトウェア)ブレークポイントをセットする
※ INT 3(0xCC) 命令(デバッガ割込)に書き換え
再スタート(Restart) Ctrl + F2 プログラムをはじめから再実行する
アドレス移動 Ctrl + g 指定したアドレス位置へカーソル移動
※プログラムの処理がジャンプするわけではない
■ SYSENTERの問題

Windowsは処理内容に応じてユーザモードとカーネルモードを切り替える必要がある。 カーネルモード(Ring0 *1)への移行する方法は以下のとおり。

OS 切替方法 備考
Windows NT/2000 int 2E (割込みゲート) intは整数のことではなくINT命令(Interrupt)
interl80386から存在。遅い
Windows XP/2003(x86) SYSENTER PentiumIIで採用。早い。
SYSENTER命令の後、SYSEXIT命令で呼出元に復帰
Windows XP/2003(x64) SYSCALL AMDK6で採用した方法
Windows 95/98/ME CALL (コールゲート) interl80386からあり遅い

これらの命令を使うと、処理がメモリ上のカーネル空間に飛ばされる。

SYSENTER命令の後、「SYSEXIT命令で呼び出し元に復帰」とあるが、実はSYSENTER命令では INT 2E命令やSYSCALL命令と異なり、呼び出し元のアドレスをスタックやレジスタに保存しない。戻り先をカーネルに知らせることなく処理が開始する。よって対応してないデバッガでは、SYSENTERの戻りを追うことができない(Ollydbgも)。

SYSEXIT命令でユーザプログラムに復帰するには、SYSEXIT実行前にEDXに戻り先、ECXにユーザプログラムのスタックポインタを設定する必要がある。戻り先をカーネルに通知する方法はいくつかある(例えば事前に決められた固定アドレスに復帰させる等。最近はセキュリティ的に採用されていないが)。

とりあえず、WinXPデバッグするのは避けた方が無難みたい。

5 APIを見張っちゃえ!

使っているAPIが分かれば、APIの呼び出しに対してブレークポイントを仕掛けられる。

■ 呼び出し規約 cdecl、stdcall
cdecl

関数の呼び出し元が、引数分のスタックの後始末をする規約。C言語の標準。 ( printf関数のような可変引数の場合は、引数の総数を知ってるのは呼び出し側だけ)

stdcall

呼び出された関数側が引数分のスタックの後始末をする規約。Windows APIの標準。 (ちなみにWindows CEはcdecl規約らしい)

  • CALLで呼ばれた関数の戻り値はEAXに入る。
  • 「RET 4」のようにRET命令に値を与えると、RET後にESPに値(ここでは4)を加算する。
    • stdcallで引数分スタックの後始末をする際に使う
    • なので、「RET 8」命令を見たら、「引数2個のstdcallな関数かも」と推測できる。
  • 関数への引数は、関数定義での右側(末尾側)からスタックに積む
    • 関数が使うときは、ESP+4で必ず一つ目の引数にアクセスできる
  • cdeclやstdcallでは、呼び出し先でEDI、ESI、EBX、EBPレジスタの値を潰してはいけない
    • 呼び出し先でこの4つのレジスタを使うならば、事前にスタックに退避し、出るときに復元する
  • スタックは上に積み上げていく方式だが、プログラム上では上から順に使うとは限らない。
    • [ESP-8]や[EBP+4]等のように、ESPやEBPから間接的にスタック上データを参照することも多い。
    • その場合、POPやPUSHで値が変わるESPより、関数内ではあまり動かないEBP間接の方が適切。
■ Ollydbgの使い方
メニュー ショートカット 説明
ラベル名 (Names in ~) Ctrl + n 現在のモジュール内で呼び出しているAPIの一覧を取得
参照の一覧 Ctrl + r API名を右クリ⇒「参照の一覧」(又はCtrl + r)
APIブレークポイント ラベル名一覧で「インポート関数にブレークポイントをセット」
APIが呼出し対してブレーク

6 バックトレース

■ レース実行(Ctrl+F12):

実行した命令を記録しながら一気にステップ実行してくれる。 実行(F9)とは違い、ステップ実行(F7又はF8)を連続で行った時と同じ動作になる。 ステップ実行で追えないSYSENTER命令は、トレース実行でも追うことができない。

トレース実行を行った後、ラントレースでトレース実行した箇所を確認できる(トレースログ)  「View」→「Run trace」

7 期限切れを書き換えちゃうゾ 前編

■ 参照文字列
  • 文字列を使うと、文字列へのアクセスに対してメモリブレークポイントを仕掛けられる。
  • ollydbgはデフォルトでは参照文字列をASII文字列として見てくれない。
  • 参照文字列はプログラム解析において大きなヒントになるので、偽装化(難読化)しておく。
  • 「参照文字列」は、 Code Section(.text)からData Section(.data、.rdata)の文字列を直接参照してるもの

文字列を直接参照させなければ「参照文字列」に載らないが、単純な定数ずらし(配列アドレス + 1 のような)程度では、Cコンパイラの「定数畳み込み」という最適化処理によって直接参照と変わらなくなってしまう。 実行時まで定まらない関数(定数を返すけど処理内に乱数を組み込んだ関数)を使う必要がある。

■ メモリブレークポイント
  • メモリのアクセス(読み書き)があったときにブレークが掛かる。
  • 設定できるのは一か所のみ。
  • 実行が遅くなる。
  • 再実行したときにクリアされてしまう(再実行のたびにセットし直す必要あり)
■ ハードウェアブレークポイント
  • CPUのハードウェアコンパレータにブレークポイントを記録
  • 4か所まで設定可能。
  • 実行速度は遅くならない
  • メモリアクセスに対しても設定可能(メモリブレークポイントのように使える)
  • 解析対象を変更しないので、アンチデバッグに検出されにくい ※ ソフトウェアブレークポイントはブレーク位置を「INT 3(0xCC)」命令に書き換えている。

8 期限切れを書き換えちゃうゾ 中編

チェックサム

誤り検出符号の一種。データ列を整数値の列とみなして和を出し、ある定数で割った余り(mod)を検査用データとする。 ソフトウェアデバッグではブレークポイントを「int 3(0xCC)」に変えるので、チェックサムでばれる。 ハードウェアブレークポイントならば、コードを変えないので検知されない。

■ コールスタック

Call命令であるサブルーチンが呼ばれると、スタックには呼び出し元のアドレスが積まれる。 コールスタックは、call命令時に積まれたスタックを抽出・一覧化したもので、デバッガの機能。 メモリ上に「コールスタック」というスタックが別に存在するわけではない(と思う)。

コールスタックの項目 説明
積まれたスタックのアドレス
積まれたスタックのデータ 戻り先アドレス
CALLされた関数 (Procedure) どの関数を呼び出したか分かる
CALLした関数 (Called from) どの関数から呼ばれたか分かる

コールスタックを見れば、関数の呼び出し履歴や親子関係、戻り先アドレスが分かる。 コールスタックを見るには、プログラムを一時停止(Pause)すること。

■ WinAPIのASCII向け関数(MessageBoxAなど)の扱い

Windows NT系では内部的(カーネル的?)にはUnicode(ワイド文字)を使用しているので、ASCII版のAPI(末尾A)は、最終的にワイド文字版(末尾W)に処理をたらい回ししている。

■ バックトレースへの対策
条件分岐とジャンプの位置を離す

一般には、認証失敗処理へのジャンプ元の近くに、認証成否の条件分岐があると考えられる。なので、ジャンプ元と条件分岐を離すと辿りにくくなる。 しかし、コード内のどこかで失敗処理のアドレスを使ってしまえば、「参照を検索」機能で辿られる。 よって、アドレスを素の状態で扱わず、分解するなどして隠すと見つかりにくい。

const DWORD NG_FuncDW = (DWORD)(&NG_Func);
const WORD NG_FuncH = (WORD)(NG_FuncDW >> 16);   // 上位16bit
const WORD NG_FuncL = (WORD)(NG_FuncDW & 0xFFFF);  // 下位16bit
 (…中略…)
WORD nextProcH, nextProcL;
if( 失敗条件成立){
    nectProcH = NG_FuncH;
    nextProcL = NG_FuncL;
}else{
       (略)
}
((BOOL(*)())( (((DWORD)nextProcH)<<16) + nextProcL ))(); // ジャンプ時に合体

ただし、書き方によってはコンパイラが最適化せずに(または最適化して?)マシンコード上ではNG_Funcのアドレスがそのまま残る可能性がある。

スタック上の呼び出し痕跡を無くす(CALLを使わない)

CALLで関数に飛ぶとスタック上に痕跡が残る(戻り先アドレスとかコールスタックとか)。 アセンブリ言語レベル(インラインアセンブラ等)でCALLをJMPにしてしまえば、痕跡を残さずに済む。 ただし戻り先アドレスが記憶されず戻れないので、その処理内でプログラムを終了(exit(0);)させる。 ※ 64bit版ではインラインアセンブラが使えなくなっているので注意。

9 期限切れを書き換えちゃうゾ 後編

■ トレースログの書き出し (ver2.01)
  1. 「Trace」→「Trace over(トレース実行)」又は「Trace into(詳細トレース実行)」
  2. 「View」→「Run Trace」で表示 して内容確認
  3. 「Run Trace」ウィンドウ上で右クリック → 「Log to file(ログ保存)」

動作が異なる場合のトレースログをDiffなどで比較することで、相違展を発見できる。

■ インポート関数のフッキング(API Hooking)
  • CPUウィンドウ → 右クリ → 「Search for」→「Names」でAPIのDLLを確認できる。
  • インポート関数の場所(アドレス)は、PEファイルのインポートセクションに書かれている。
  • インポートセクションに書かれているアドレスを書き換えれば、好きな処理に飛ばせる。
  • ツール「APIHijack」を使うと、インポート関数をフッキングするプログラムを容易に作れる。
■ DLLインジェクション

WindowsでDLLファイルを探す場合、次の順序で探す 1. プリインストールDLL(kernel32.dll、user32.dll等)は特別扱いでOS依存 2. 実行中のプロセスの実行形式モジュールがあるフォルダ(実行ファイルがあるフォルダ?) 3. カレントフォルダ 4. Windowsシステムフォルダ(GetSystemDirectory関数で取得) 5. Windowsディレクトリ(GetWindowsDirectory関数で取得) 6. 環境変数PATHに記述されたフォルダ

kernel32.dllだと最優先されるので、偽DLLの名前を変え、解析対象内の「kernel32.dll」文字列も偽DLL名に変えてしまう(「_ernel32.dll」など)。あとは解析対象と同じフォルダに偽DLLを置いておけばよい。

10・11・12 1種スーパークラッカー試験 前・中・後編 

1年間修業をした兄が戻り、1種スーパークラッカー試験に挑む。兄の成長ぶりにビビる。 兄が問題(認証をクラックしていく)を解いていく過程、考え方は勉強になる。が、こんな短時間でバシバシ解いていける気は全くしない。

  • 認証のタイプからWinAPIを推測する(例:ダイアログ文字取得 ⇒ GetWindowTextA?GetDlgItem?)
  • 認証機能の仕組みの理解が必要(動作を知らないとAPIを推測できない)
  • DLLがAPIをexportするには名前でなく順序(序数)指定の場合もあり、その場合は名前一覧に出ない
  • dongleに書込み(WriteByte等)するAPIの引数を下手に変更すると、dongle破損させる恐れあり。
  • LoadLibrary、GetProcAddressで動的にDLLを読み込み(インポート関数一覧に載らない)

13 ファーファと美咲の課外授業

Q&A形式のテクニック説明。ループの解析とか、パックされたプログラムとか。

*1:Ring Protection(リング・プロテクション)はコンピュータ・アーキテクチャ内の権限を複数の階層構造で構成する。Ring0が最も高い(信頼)。リング間には特別な「ゲート」があり、外側から内側のリングのリソースに予め決められた方法でアクセスが可能。任意の使用は許可しない。ゲートによるリング間の正しいアクセスにより、あるリングのプログラムが別のリングのプログラム用のリソースを悪用することを防ぐ