Question & Suggestion
- 以下の解説は、PCゲーム解析等プログラム解析の、初心者の方を対象として執筆しました
- 以下の解説は、特に記載のない限り32ビットアプリケーションを対象としています
- 当ページと併せて、デバッガ関連の解説である「OllyDbg Q&A」も参照されることをお勧めします
- 以下の回答は、匿名希望のリバースエンジニア有志とうさぴょん、Lucaで行っています
| デバッガ・逆アセンブラ・プロセスメモリエディタ・ヘキサエディタ関連 | その他 |
ご質問・ご意見一覧
デバッガ・逆アセンブラ・プロセスメモリエディタ・ヘキサエディタ関連
- Windows Vistaや7/8/10でOllyDbgなどのプログラム解析ツールは動作しますか?
- 新規購入したパソコンでプログラム解析ツールが実行できません
- プログラム解析環境を構築したい
- 逆アセンブラやデバッガを使ったゲーム解析を行うためには、どの様な知識から身に付ければよいでしょうか?
- プログラム解析以前のコンピュータ関連基礎知識を身につけたい
- プログラム解析ツールの操作を簡単に行いたい
- 初心者はどのツールを使うべきですか?
- やはりプログラミングの勉強をしておいた方がゲームの解析に役立つのでしょうか?
- ファイルパッチコードの形式について教えてください
- 常用している解析ツールをセキュリティソフトがマルウェアと検出しました
- API関数の書式や引数に指定する値等をすばやく探し出す方法はありませんか?
- 逆アセンブルコードリスト上で文字列参照箇所をすばやく見つけるには?
- PE(Portable Executable)ヘッダの詳細情報を簡単に知りたい。
- コード無効化のメモリパッチへNOP命令を用いるべきではないと聞いたことがありますが...
- 『PeRdr』のレジストリへの登録について、レジストリをいじるのはちょっと怖いです。
- ヘキサエディタとバイナリエディタの違いを教えて下さい。
- MOV命令の意味の言い方が人によって違うのですが...
- うさみみハリケーン用のドライバは開発しないのですか?
- 某PCゲームなのですが、改造ツールを起動するとゲームが強制終了します。
- 某PCゲームの2重起動(多重起動)制限を解除したいのですが、どうアプローチすれば良いか分かりません。
- 改造済の実行ファイルを配布したいのですが。
- プロセスメモリエディタの検索で判明したパラメータのアドレスが変なメモリ領域にあります。
- 「環境依存型変動アドレス」について教えてください
- 相互修復型パラメータの書き換え方法を教えてください。
- プログラムの参照文字列を解析の端緒とする場合の注意点を教えて下さい。
- ゲームのシナリオ表示等、文字列を画面に「描画」する処理箇所を特定するためのアプローチ方法が分かりません。
- タスク切り替えが出来ないゲームの対処法を知りたい。
- フルスクリーンのゲームをウィンドウモードでプレイする方法を教えて下さい。
- PrintScreenキーでスナップショットが取れないゲームの仕組みを教えてください。
- ゲームが自己プロセスを隠蔽することは可能ですか?
- ゲームの当たり判定解析のアプローチが分かりません
- 「逆アセンブルコードの再利用」について教えてください
- 日本語版のOSでは起動させないゲームの仕組みを教えてください
- PCゲーム解析関連情報の管理はどのようにするべきですか
- ゲーム実行PCの固有情報を鍵として暗号化されるセーブデータを別のPCで使用したい
- アプリケーションの処理実行速度を変更するソフトウェアの仕組みを教えて下さい
- ゲーム上でランダムに設定されるデータ(出現アイテムの種類など)を固定化したい
- 「倍直」とは何ですか
- ゲームなどで実行時に作成される「一時ファイル」を残したい
- ASLRに影響されないEXEファイル改造を行いたい
- プログラムがユーザーの入力を検出する仕組みを知りたい
- 64ビットアプリケーションの解析について知りたい
- ホットパッチ型APIフックがうまくいきません
- プログラムが認識するフォルダのパスを変更したい
- エントリーポイント以前に実行される解析対策について知りたい
- プロセスとプロセスでやり取りする方法を知りたい
- IAT書き換え型APIフックの注意点を知りたい
- 64ビット環境で.NETアプリケーションが解析できません
- ポインタを解析したい
- アプリケーションがユーザーインターフェース上に表示する文字列を取得したい
△デバッガ・逆アセンブラ・プロセスメモリエディタ・ヘキサエディタ関連
Windows Vistaや7/8/10でOllyDbgなどのプログラム解析ツールは動作しますか?
(注)この質問と回答は、もともとWindows XPからVistaへの移行期のものであり、その後Windows 7/8/10向けに若干の加筆を行っています。基本的にWindows Vista/7/8/10では、Windows XP SP2で正常動作するたいていのプログラム解析・改造ツールは動作可能と見られます。ただし、処理をネイティブAPIやカーネルに強く依存するといった特殊なツールではまったく動作しないものもあります。動作上致命的な問題が生じるケースについては、次の質問の回答も参照してください。
Windows Vista以降での、プログラム解析ツール導入・実行時の注意点は下記。
●ユーザーアカウント制御(UAC)と管理者権限
扱いに注意が必要なのは、デバッガやプロセスメモリエディタなどプロセスへの操作を行うツールです。Windows Vista以降ではUAC(User Access Control:ユーザーアカウント制御)というセキュリティ関連機能により、管理者としてログインしていても通常のプログラム実行の権限は標準ユーザー扱いとなります。そのため、エクスプローラ上でEXEファイルをダブルクリックするような通常の起動では、自動的に権限のフィルタリングが行われ、デバッガとして十分な実行権限が与えられません。システムで実行されているプロセスの一覧取得も制限され、一部のプロセスしか認識・列挙できなくなります。ここでもし解析対象プロセスも標準ユーザーの権限で実行されているものならば、デバッガやプロセスメモリエディタによる解析は、レジストリの操作やプロセス関連情報取得などに制限はあるものの一応問題なく行えます。しかし、この場合はなにぶんデバッガなどに本来必要な権限がないだけに、解析にあたり何らかの不具合が発生してもおかしくない状況といえます。
Windows Vista以降では、UACにより、管理者としてプログラムを実行する際には、その都度確認のダイアログが表示されとても煩雑です。このことを併せて勘案すれば、デバッガやプロセスメモリエディタに関しては、まず通常どおり(標準ユーザーとして)実行してプログラム解析を行い、なんらかの問題が発生したら管理者として実行するという方法が、利便性と安全性の面からみた妥協案になると考えられます。また、Windows Vistaでは、UACの制御を簡易化し、権限昇格確認のダイアログ表示を抑制するツール『TweakUAC』を利用するという方法もあります。
Windows Vista以降では、管理者の権限でプログラム解析ツールを実行するためには以下の方法が使用できます。
- エクスプローラ上でプログラム解析ツールのEXEファイルを右クリックし、表示されるポップアップメニューから「管理者として実行」で実行させる
- Vista専用のマニフェストファイルを使用する(使用法など詳細はマニフェストファイル同梱テキストにあります)
- エクスプローラ上でプログラム解析ツールのEXEファイルかそのショートカットを右クリックして、ポップアップメニューの「プロパティ」→「互換性」タブ→「管理者としてこのプログラムを実行する」チェックボックスを有効にする(マニフェスト使用よりも若干権限が低くなります)
- ランチャーをタスクスケジューラに登録する
・以下操作手順により、ランチャーから起動されたプログラム解析ツールはUACの警告表示なしに管理者権限で実行されます
1.Windowsキーを押しながらRキーで[ファイル名を指定して実行]を表示し「taskschd.msc」を入力して実行
2.タスクスケジューラ画面右側にある[タスクの作成]をクリック
3.[全般]タブで適当な名前を指定し、[最上位の特権で実行する]にチェックを入れる
4.[トリガー]タブで[新規]をクリックし、[タスクの開始]に[ログオン時]を選択後[OK]
5.[操作]タブで[新規]をクリックし、[プログラム/スクリプト]にランチャーのパスを指定後[OK]
6.[タスクの作成]ダイアログ画面下部の[OK]をクリックして完了
7.必要に応じてランチャーの処理優先度を修正(下記参照)
8.もしも不具合が生じたら、ランチャーの設定等を使ってランチャーを遅延起動させてみる
参考:タスク スケジューラの操作方法
上とは別に、管理者権限のコマンドプロンプトからタスクスケジューラに登録する方法もあります。
・コマンドプロンプト(管理者)からの登録例
設定内容:タスクの作成
タスク名:NekoLaunch
ランチャーのパス:C:\APP\Neko\Launcher.exe
開始:ログオン時
遅延起動:1分30秒後
特権:最上位の特権で実行
既存同名タスクの上書き:あり(意図しない上書きが生じないよう注意してください)
以下を管理者権限の「コマンドプロンプト(管理者)」で実行します。Windows 8/10ではWindowsキー+Xキーから、7ではスタートメニューの[すべてのプログラム]→[アクセサリ]→[コマンドプロンプト]を右クリックして[管理者として実行]でコマンドプロンプト(管理者)を呼び出します。
schtasks /create /tn NekoLaunch /tr "C:\APP\Neko\Launcher.exe" /sc onlogon /delay 0001:30 /rl highest /F
ランチャーのパスに引数(コマンドラインオプション)を付加する場合は、下記のようにランチャーのパスを囲みます。もし引数が何らかのパスならば引数部分を「\"」で囲みます。
/tr "'C:\APP\Neko\Launcher.exe' /Usagi 42"
作成したタスクを直接実行するならば、リンク先が「schtasks.exe /run /tn NekoLaunch」という形式のショートカットを作成します。ショートカットは右クリックから「プロパティ」で、リンク先を指定したり、「実行時の大きさ」を「最小化」に設定することができます。
基本的に、タスクスケジューラから起動されたプロセス(Windows上の実行単位)は、実行処理における「I/O優先度」と「メモリ優先度」が通常よりも低く設定されてしまいます。これらの優先度をタスクスケジューラ側で修正するならば、まずタスクスケジューラを起動して対象ソフトウェアを管理者権限で起動するタスクを作成後に、「タスクスケジューラ ライブラリ」に表示される同タスクを選択して右クリックからエクスポートを行い、同タスクは削除します。次に、エクスポートで出力されたXMLファイルをテキストエディタで開いて優先度設定を書き換えてから、「タスクスケジューラ ライブラリ」文字列部を右クリックから「タスクのインポート」で書き換え済みXMLファイルを読み込んで対処します。優先度の書き換えは、エクスポートで出力されたXMLファイル内の「Settings」以下にある「Priority」の設定値を7から4に書き換えます(6だとメモリ優先度が低くなる)。なお、『うさみみハリケーン』同梱のボタン型ランチャー「NekoLaunch」では、タスクスケジューラからの起動に対応するため、起動時に自分自身の「実行処理優先度」・「I/O優先度」・「メモリ優先度」を「通常」に設定します。
- UAC(ユーザーアカウント制御)の設定を無効にする(セキュリティの面からお奨めしません)
ちなみに、Windows 7以降ではUACの警告表示についての設定を簡単に変更可能です。コントロール パネル→[システムとセキュリティ]→[アクションセンター]→[ユーザー アカウント制御設定の変更] で設定画面が表示されます。あるいは、7でスタートメニュー下部「プログラムとファイルの検索」箇所に以下の実行可能ファイル名文字列を貼り付け、表示されるリンクをクリックします。Windows 8/10では画面左下隅を右クリックか[Windowsキー]+[Xキー]で表示されるメニューから、または[Windowsキー]+[Rキー]で表示される、[ファイル名を指定して実行]で以下を実行します。
UserAccountControlSettings.exe
さらに、レジストリを変更することでより細かい設定変更が可能になります。
参考:UAC設定をレジストリから制御し、スムーズなセキュリティ設定を実現する
OllyDbgでは、ジャスト・イン・タイム デバッガとしてOllyDbgを登録したり、エクスプローラが表示するポップアップメニューにOllyDbgを登録する際には、レジストリの書き込み処理を成功させるために管理者として実行する必要があります。この登録処理は権限の不足により失敗してもエラーメッセージは表示されません。
Windows Vista以降での「OllyDbg」や「うさみみハリケーン」のインストール先フォルダは、既定の「Program Files」フォルダ内とは別の場所に新規作成することをお奨めします。これは、両ソフトで各種設定保存に使用するINIファイルが、Vistaで実装されたUACのファイル/レジストリ仮想化機能(VirtualStore)の影響を受けないようにするためです。これにより、従来どおり関連ファイル一式がインストール先フォルダ内のみに集約され、関連ファイルが他のフォルダに分散することを回避できます。
VirtualStoreにより、Program FilesフォルダやWINDOWSフォルダ内で行われたファイル作成・読み書きアクセスは、VirtualStore専用フォルダにリダイレクトされます。このフォルダは、Vistaのスタートメニュー下部「検索の開始」箇所、7では「プログラムとファイルの検索」箇所に以下のいずれかの文字列を貼り付けることで、当該フォルダをエクスプローラで開くリンクが表示されます。Windows 8/10では画面左下隅を右クリックか[Windowsキー]+[Xキー]で表示されるメニューから、あるいは[Windowsキー]+[Rキー]の、[ファイル名を指定して実行]で以下を実行します。
%USERPROFILE%\AppData\Local\VirtualStore
%LocalAppData%\VirtualStore
なお、うさみみハリケーンでは、メニューの[その他]→[特殊フォルダを開く]で、VirtualStore専用フォルダを含む各特殊フォルダを簡単に開くことができます。
●ユーザーインターフェイス特権の分離(UIPI)とWindowメッセージ
解析ツールがキーフックによる各種操作などで、内部的にメッセージフックを用いる場合には注意が必要です。この場合、解析ツールから解析対象アプリケーションを起動することなどにより、解析する側とされる側の実行権限を同じレベルに合わせないと、メッセージフックが正常に動作しません。ドラッグ・アンド・ドロップが正常に機能しないケースも権限レベルの不一致が原因として考えられます。ちなみに、同様の制限が一部のプロセス間通信にも適用されます。もし、上記のような問題がおきたときは、プログラム解析ツール、解析対象アプリケーション「両方」を管理者権限で起動させてみて下さい。
上記の問題は、Windows Vistaで導入されたセキュリティ関連機能「ユーザーインターフェイス特権の分離」(UIPI)によるものです。上記のように、標準ユーザー権限から管理者権限へといった、プロセスの整合性レベル(Integrity level)が低い方から高い方へのWindowメッセージを用いた操作が制限されます。プログラミング上の解決策としては、ChangeWindowMessageFilterEx関数で対処可能です。たとえば解析ツール側でドラッグ・アンド・ドロップを許可するならば、同関数でWM_DROPFILES、WM_COPYDATAおよび0x49(非文書化メッセージ定義値)のフィルタ通過を許可(MSGFLT_ALLOW)します。
●Windows 10のフォント仕様変更
Windows 10の2015年11月のアップデート(ビルド10586)では、フォントに関する仕様が変更されています。これにより、例えばCreateFont関数呼び出し時に、引数の指定内容によっては、その指定した条件を満たすフォントが作成されなくなりました(関数の戻り値ではフォント作成成功となる)。このため、バイナリエディタなどに見られる等幅フォントを作成・使用するユーザーインターフェイスにおいて、等幅ではないフォントが作成されてしまい表示が崩れてしまうケースも生じています。また、フォントサイズが極小になるといった、その他の文字列表示における不具合も、このフォントの仕様変更が原因である可能性があります。このWindowsの仕様変更には、ソースコードの一部変更で対処可能ですので、該当ケースではまずプログラム解析ツールの作者に相談されることをお勧めします。
なお、上記とは別の問題として、高解像度環境で文字列表示に不具合を生じるケースがあります。これは、市販PCによっては、文字列表示サイズに関連するスケーリング(拡大表示)の値があらかじめ標準の100%よりも高い数値に設定されており、特にダイアログ上での文字列表示において、文字のはみ出しやぼやけといった不具合を招くというケースです。特定アプリケーションでこのスケーリングを無効にするならば、エクスプローラ上で対象EXEファイルの右クリックから、[プロパティ]→[互換性]タブで[高DPI設定では画面のスケーリングを無効にする]にチェックを入れます。また、Windows 10のCreators Update(ビルド15063)以降ならば、上記[互換性]タブの[高DPI設定の変更]ボタン押し下げ後、[高いDPIスケールの動作を上書きします。拡大縮小の実行元:]をチェックしてから[システム (拡張)]の選択により、適切な文字列拡大とぼやけ改善が可能です。 他には、対象EXEファイル用のマニフェストで「dpiAware」の指定を行うことによりスケーリングを制御することも可能です。SetProcessDpiAwareness関数等による、スケーリングの外部からの動的な制御手法は、対象プロセスをエントリーポイントまでに停止させた状態以外で使用するならば、安全性に問題があります。
●<参考>当サイト管理人の動作確認環境と動作確認時実行手順
・Windows Vista Home Premium 32bit クリーンインストール
・UAC(ユーザーアカウント制御)は有効
・インストール先フォルダは「C:\APP\UsaMimi」や「C:\APP\Olly」
・起動時はエクスプローラ上でEXEファイル右クリックから[管理者として実行]
新規購入したパソコンでプログラム解析ツールが実行できません
(注)この質問と回答は、もともとWindows XPからVistaへの移行期のものであり、その後Windows 7/8/10向けに若干の加筆を行っています。パソコンの買い替え等に伴い、実行環境がWindows XPからVistaや7以降に変更された場合、XP上では問題なく動作する一部のプログラム解析ツール(以下「解析ツール」)が実行できないケースがあります。以下に想定される原因を列挙しました。
- DEP
OSに実装されているDEP(データ実行防止)機能により、プログラムの処理上でDEPを想定していない一部の解析ツールは、実行不能となります。ただし、OSのDEP初期設定では、システムに関わる重要なプロセスのみがDEPの対象として設定されています。そのため、設定を自分で変更し、全てのプロセスをDEPの対象にした場合に、表題のような実行不能になるケースが生じます。しかしながら、一般に販売されているパソコンの中には、一部の大手メーカー製や一部のショップ製パソコンで、出荷時にDEPの設定を変更しているケースもあります。なお、パソコンに不具合が生じた際に、家族・親戚・友人・パソコン修理業者などにパソコンを診てもらった結果、気づかないうちにDEPの設定が変更されていたというケースも実在します。
- OSバージョンチェック
- API関数の仕様変更
- 64ビット版のWindows 7
- セキュリティソフト・オンラインゲーム
- (参考)ヘルプが表示されない
通常、DEPによりプロセスの実行が中断された場合は、OSがそのことをユーザーに警告します。しかし、Windows Vista Home Premiumで確認した限りでは、一切の警告なしにプロセスの実行が中断され強制終了するケースもあります。
もし、DEPが解析ツール実行不能の原因と推測される場合は、DEPの設定を確認し、解析ツールのEXEファイルを対象から除外してみてください。 Vista以降でのDEPの設定は、以下の手順で行います。
1.Windows Vistaでは、スタートメニューから、[コントロールパネル]→[システムとメンテナンス]→[システム]→[システムの詳細設定]をクリックします。Windows 7では、スタートメニューから、[コントロールパネル]→[システムとセキュリティ]→[システム]→[システムの詳細設定]をクリックします。
あるいは、Vistaのスタートメニュー下部「検索の開始」箇所、7では「プログラムとファイルの検索」箇所に以下の実行可能ファイル名文字列を貼り付け、表示されるリンクをクリックします。Windows 8/10では画面左下隅を右クリックか[Windowsキー]+[Xキー]で表示されるメニューから、または[Windowsキー]+[Rキー]で表示される、[ファイル名を指定して実行]で以下を実行します。
SystemPropertiesAdvanced.exe
2.管理者としてのアクセス許可の後、[パフォーマンス]欄の[設定]ボタンをクリックします。
3.[データ実行防止]タブをクリックし、オプション[次に選択するのものを除くすべてのプログラムおよびサービスについてDEPを有効にする]が選択されているならば、[追加]ボタンで、解析ツールの実行可能ファイルを除外対象に追加します。設定変更内容を反映させるため、必要に応じて再起動させます。
一般的なソフトウェアならば、OSに実装されている「互換モード」により対処可能なケースもあります。しかし、特にプロセスを対象とする解析ツールでは、一部のプロセス操作用API関数でOSによって仕様が異なるため、「互換モード」適用では対処できないケースがあります。
望ましい対処策として、作者に対して状況と当該解析ツールの必要性を説明し、最新OSへの対応を打診してみることをお勧めします。
なお、上とは逆に、プログラム解析ツール等がバージョンアップした際に、それまで正常に動作していたWindows XPなど古いOS上で起動不能になるケースもあります。これは作者がコンパイラを最新のものに変更したり、新しいWindows OSで実装されたAPI関数をそのまま使用するといった原因によります。この場合、使用API関数に問題がなければ、『うさみみハリケーン』同梱のPEエディタ『UMPE』で、PEヘッダ内のバージョン項目「OS」と「Subsystem」両方を古いOSに合わせて書き換える(Windows XPならば5.1)ことで、起動可能になるケースもあります。
実例として、拙作の汎用プロセスメモリエディタ兼デバッガ『うさみみハリケーン』では、Vista対応にあたり、複数のプロセス関連処理箇所でソースコードを書き替えました。
このようなケースでは、作者に最新OSへの対応を希望することで、最も適切な対処が行われると考えられます。
なお、解析ツールの作者側としては、上記のケースとは逆に、Windows 10といった新しいOSでの正常動作をもって、Windows XPなどの古いOSで正常に動作すると判断すべきではないことに、注意が必要です。特にネイティブAPIを使用している場合は、OSによって対応する引数の範囲が異なるケースを考慮してください(NtQueryInformationProcess関数の引数ProcessInformationClassなど)。
関連して、Windows OSに含まれる、DbgHelp.dllがエクスポートしているMiniDump系のAPI関数では、内部処理に用いられる構造体がXP SP2とVista以降で異なるものがあります。そのため、この構造体の変更を想定していない一部の解析ツールにおいて、XP SP2では正常に動作するが、Vista以降ではクラッシュするというケースもあります。このような場合は、作者の動作確認環境に含まれるDbgHelp.dllを、ツールに同梱して再配布することで対処可能です。DbgHelp.dllは実行環境におけるバージョン差異に対処するために、再配布が許可されているDLLです。また、DbgHelp.dllは「Known DLLs」ではないので、実行ファイルと同じフォルダに導入したものが優先的にロードされます。
DbgHelp.dllを最新のものにすれば、処理速度の向上や取得できる情報の増加というメリットが増える場合もありますが、解析ツールの作者が想定していない新しいバージョンのDbgHelp.dllでは、上記のようにこのDLLの仕様により不具合を招くケースもあります。他の例として、たとえばWindows 9x対応のためIMAGEHLP.dllとDbgHelp.dllの両方のAPI関数の呼び出しを処理中で混在させている解析ツールで、DbgHelp.dllが新しすぎると不具合を生じることがあります(注:Windows Vista以降でのこれらDLLのファイル名は「dbghelp.dll」、「imagehlp.dll」)。
なお、64ビット版のWindows 7/8では32ビット版と異なり、32ビット版アプリケーションのプロセスメモリ上に読み込まれるGDI32.dll、kernel32.dllといった一部のシステムモジュールは、ReadProcessMemory関数を用いてプロセスメモリ上でのモジュールエリア全体を一度に読み込むことができない仕様となっています。このため、32ビット版Windowsが想定使用環境のプログラム解析ツールでは、プロセスメモリ上からの情報取得時や逆アセンブル時などに、モジュール全体が読めないことでエラーとみなすケースがあります。このことは、実際のゲーム解析といったプログラム解析を行う上で深刻な問題とまではいえないものの、プログラム解析を制約する要素となりえます。
また、一部セキュリティソフトの標準設定、あるいはセキュリティソフトが実装する「ゲームモード」といった特殊なモードにより、解析ツールのダウンロード時や、その配布ファイルの解凍時に、一切の警告無しに解析ツール本体の実行ファイルが削除されるケースにも注意が必要です。これにより、配布ファイルを解凍すると、「実行ファイルが見つからない」という事態が発生します。「ゲームモード」は本来ユーザーが自分自身で設定するものですが、初心者の方の中には、「ゲームモード」がどのようなものか知らないまま設定している方も少なくないようです。
たいていのウイルス対策ソフトでは、ウイルススキャンの対象から除外するフォルダを設定可能です。そのため、安全性が明らかであるプログラム解析ツールは、あらかじめフォルダを作成しスキャン対象から除外に設定しておいてから、そのフォルダ内にダウンロードとインストールを行えば、上記のようなウイルス対策ソフトが原因で生じる問題を回避できます。
Windows Vista以降では、既存のHLP形式のヘルプファイルを表示するには、Microsoftが配布する専用のソフトウェアをダウンロードする必要があります。ただし、この提供される「WinHlp32.exe」では、開くことのできないHLPファイルもあります。
プログラム解析環境を構築したい
一般論としてプログラム解析環境に用いるOSは、プログラム解析環境における、プログラム解析への制約の少なさという面でみれば、Windows 10や8よりも7、7やVistaよりもXP、さらに64ビット版よりも32ビット版が望ましいということになります。このような古いWindows OS上ならば、そのOSの時代に公開された解析関連ツールや解析資料といった、蓄積された過去の多くの資産を十分に活用できるという利点もあります。ただし、解析対象プログラムがカーネルモードでのAPIフックといったルートキット同様の解析対策を実装しているケースでは、署名なしのドライバへの制約が増加した64ビット版の方がスムーズに解析できることもあります。
また、一般的なPCゲームやアプリケーションならばPC実機で解析を行い、マルウェアや「挙動に問題がある」アプリケーションの解析には「VMware Workstation Pro」(有償)や「VMware Workstation Player」(個人使用無償)などで構築した仮想PCを用います。この仮想PCにWindowsをインストールしさらに必要な各種解析ツールを導入しておきます。仮想PCならば、問題発生時への対処として、スナップショット機能で問題発生前の状態へ戻したり、仮想PCの本体であるファイルのバックアップから復旧することが可能です。なお、仮想PC検出・対策機能を実装しているマルウェアも実在しますので、仮想PCという解析環境を過信しないでください。関連して、仮想PCは、挙動が不明なプログラム解析ツールの動作確認にも役立ちます。
PC実機を用いる場合は、できれば解析専用にPC1台を割り当てて、たとえ解析対象プログラムのバグやロジックボムなどでシステムに不具合が生じたり起動不可となっても、システム対応のバックアップソフトなどを使って問題なく復旧できるようにしておくと良いでしょう。
PCゲーム解析環境に関して、OSに独自設定が施され色々なソフトウェアがプレインストールされているメーカー製PCをそのまま使用すると、PCゲーム解析およびその結果を反映した改造後の動作確認に支障が生じる場合もあります。この場合は、仮想PCを構築後、Windowsをクリーンインストールしてさらに必要な各種解析ツールを導入した解析環境の使用をお勧めします。
プログラム解析ツールの導入や使用については、当Q&Aの他の質問と回答を参考にしてください。導入する解析ツールの選択に関しては、仮想PCを用いた解析環境の構築を容易にするツール「FLARE VM」の解説にある、インストールされるツール群のリストが参考になります。ただし、このリストには日本製の各種解析ツールは含まれていません。
プログラム解析環境のハード面としては、プログラム解析時のストレスを極力軽減させるために、「必要十分なスペックのPC」、「大画面のモニター」および「人間工学に基づく形状のマウス・キーボード」の使用をお勧めします。加えて、リバースエンジニアやプログラマの方々から高い評価を得ているワークチェア『アーロンチェア』(新モデルあり)も、実際にプログラム開発・解析時に使用している者として強くお勧めします。なお、『アーロンチェア』の上位モデルといえる、PCでの長時間作業を想定して設計された『エンボディチェア』もあるのですが、こちらは筆者に長期間使用した経験がないため紹介に止めます。
余談ですが、一部のリバースエンジニアの方々は、解析環境の「構成要素」として、猫とか「Lonely Planet」とかチョコレートとか楽器とかの、思考回路をリフレッシュさせる何かが必須だと主張されています。
逆アセンブラやデバッガを使ったゲーム解析を行うためには、どの様な知識から身に付ければよいでしょうか?
逆アセンブラやデバッガを使う前に、まずヘキサエディタ(バイナリエディタ)やプロセスメモリエディタ等の基本的な改造ツールの操作を身に付けて下さい。それからゲーム解析チュートリアルに加えて、インテルのアーキテクチャマニュアル、Windows SDK等を揃えてアセンブリ言語とWindowsAPIの基本事項(レジスタ、条件ジャンプ等各種命令、各種API関数他)を理解し、その上で逆アセンブラやデバッガを用いたゲーム解析を始めれば良いかと思います。なお、現在では、API関数等の仕様の詳細はネット上での検索でおおむね事足りるため、Windows SDKは必須とはいえません。また、上に挙げた情報ソースは、面倒でも疑問点があれば何度でも参照して正しい知識を身に付けて下さい。また、基本事項をより理解するために、上記基本的な改造ツールやデバッガのヘルプには十分に目を通し、さらに必要に応じてネット上で検索サイトや用語集サイトを利用したり、当サイトで紹介している解析参考書籍を参照することをお奨めします。
なお、知識が不十分である事柄について他の方に教える場合は、「自分も詳しくは無いのですが」等の一文を加えて知識が不十分であることを明確にしておけば、よほどの勉強不足で無い限り、もし内容に予期しない間違いがあっても非難されることは無いと思います。
過去の実例を見る限り、アセンブリ言語やWindowsAPIに関する基本的な知識を身に付けないまま逆アセンブラやデバッガを使ったゲーム解析に挑んだ方々が、解析の方向性を定められず無意味な解析に時間を浪費し、最終的に挫折してしまうケースが少なくありませんでした(もちろん私のケースを多々含みます)。
しかし、上記のアセンブリ言語とWindowsAPIの基本事項に加えて、解析対象のゲームに対して「自分がどのような解析・改造を行いたいか」を明確にした上で、その解析に必要な正しい知識をチュートリアル等を通じて身に付けてから解析に臨めば、大抵のゲーム解析は特に迷うこともなくスムーズに解析できるはずです。もちろん逆に、現在の自分のスキルでは対処できないケースという判断も的確かつ迅速に行えます。
必要な知識が欠如していると、「闇雲に書き換え」等、あまりにも非効率的な解析を行うことになります。このような解析は、私を含めた複数のゲーム解析関連の方の経験から言えば、ゲーム解析スキル向上には殆ど役に立ちませんので、十分にご注意下さい。
以上は簡略化した「一般的な」回答です。リバースエンジニアそれぞれ進む方向は異なります。が、「自分は何がしたいか、そのために何が必要か」を自問し続ければ、迷うことは少ないと思います。
プログラム解析以前のコンピュータ関連基礎知識を身につけたい
当サイトで紹介しているプログラム解析の参考書籍でもこのような基礎知識は解説しています。しかし、それとは別に、後々プログラム解析にスムーズにリンクすることを踏まえて、私や他のリバースエンジニアの方々が超初心者の方に勧めている解説書は以下が挙げられます。・プログラムはなぜ動くのか
・はじめて読む8086
なお、『はじめて読む8086』と同じ筆者による『はじめて読む486』『はじめて読むPentium マシン語入門編』も出版されており、できればこの3冊を読み比べて、自分が求める内容に一番近いものを選択されることをお勧めします。
上記の解説書に記述されている情報は、ネット上で入手できない訳ではありません。実際、この様な解説書に頼らず、ネット上で資料を収集して学ばれた方もいます。しかし、超初心者の方が短時間で体系的にコンピュータ関連基礎知識を身につけるには、定評のある上記の解説書を用いるのが適切と考えられます。 ただし、これらの解説書に書いてあることだけにとらわれず、疑問に感じた点はネット上で調べる等、自分で知識を拡げていくという自己努力の姿勢も大切です。「読んだ」だけで満足しないよう注意して下さい。
プログラム解析ツールの操作を簡単に行いたい
一般的なプログラム解析ツールは、ユーザーが簡単に操作できるようにするために、色々な工夫をこらしています。基本的に、このような操作簡易化のための仕組みは、プログラム解析ツールに同梱されているヘルプで解説されています。しかし、たとえば、プログラム解析ツールの作者が、ユーザーには基本的なツール操作やプログラミングの知識があるはずと考えるケースでは、ヘルプでの説明が省略されていることもあります。以下では、特に解析初心者の方が知っておくと役立つ、操作簡易化のための仕組みを列挙しました。以下の『うさみみハリケーン』に関する記述については、同梱ヘルプやオンラインヘルプに詳細な解説がありますので、参照されることをお勧めします。
●ファイルやプロセスの指定方法
解析対象となるファイルあるいはプロセスの指定・オープンには、たいてい、その手順を簡易化する仕組みが設けられます。ファイルならば、メニューに「最近使ったファイル」が表示され、メニューから簡単に指定できるケースがあります。また、コマンドラインオプションに対象ファイルを指定して解析ツールを起動させることで、自動的に指定・オープン処理を行うものもあります。この機能は、エクスプローラ上の右クリックで表示される、ポップアップメニューに解析ツールを登録すると特に役立ちます。
プロセスならば、メインモジュール(EXEファイル)のパスかファイル名を指定することで、プロセスの選択・オープン処理を自動化するものがあります。この機能は、同じゲームを何度も起動して解析するといったケースに役立ちます。『うさみみハリケーン』では、コマンドラインオプションに「C:\APP\UsaMimi\UsaMimi.exe /UsaTest2.EXE」といった形式で解析対象プロセスのEXEファイル名を指定することで、『うさみみハリケーン』起動時にそのプロセスを自動で選択・オープンします(参考)。この場合、オープンしている対象プロセスが終了・再起動しても、「Ctrl+O」キー押し下げだけでその再起動したプロセスを選択・オープンします。このようなコマンドラインオプションの指定は、ショートカットかランチャーで指定するのが簡単です。
●16進数の指定方法
プログラム解析ツールによっては、10進数での数値指定とは別に、16進数での数値指定もできるようにしています。指定方式にはいくつかのパターンがありますが、「0x12345678」というように「0x」をつけるケースが多いといえます。この場合、汎用のプログラム解析チュートリアルで解説しているような、Windows付属の「電卓」を使用した16進数への変換は不要です。なお、「0x」での16進数の入力に対応しているならば、さらに、入力した数値の最初がゼロで、以降を1から7で入力する8進数の入力にも対応しているケースがあります。
『うさみみハリケーン』でのプロセスメモリ検索機能・範囲検索機能・置換機能・一括書き込み機能などでは、「0x」で16進数指定が可能です。また、『うさみみハリケーン』には、テキスト入力ベースの進数変換・式入力計算ソフト「UMEC」を同梱しています。
●バイナリデータの指定方法
一部のプログラム解析ツールには、検索などでバイナリデータを指定する際に、「シーケンス」や「マスク」という方法を使用可能なものもあります。シーケンスとは「1 0x1234 86 120 0xABCDEF00」というように、指定された複数のデータを連結して一つのデータとして扱う方法です。『うさみみハリケーン』のプロセスメモリ検索機能・範囲検索機能(64Bit Mode)・置換機能・一括書き込み機能・10進入出力機能他はシーケンスに対応しています。マスクは指定するバイナリデータの一部を無視して処理を行う方法で、「ワイルドカード」と表記されることもあります。マスクの使用例としては、検索対象のバイナリデータに「7FFF****05*0」と指定して、「*」部分は無視して検索を行うケースが挙げられます。マスクの指定には「*」の他に「?」もよく使われます。『うさみみハリケーン』では、範囲検索(64Bit Mode)やリージョン検索でバイト列を検索する際に、マスクを「?」で指定することができます。
●各種クリックへの機能割り当て
プログラム解析ツールでは、リストやバイナリデータ表示画面などで選択した項目に対して、左クリックでの選択以外に各種クリックが使用可能なケースもあります。このようなケースではたいてい、選択項目に対して、ダブルクリック、右クリックあるいは、ホイールボタンクリックに、その選択項目に応じた処理を行うための機能を割り当てています。そのため、画面上で何かを選択したら、まず右クリックなど各種クリックを試してみることをお勧めします。また、リスト上で項目選択後に、ダブルクリックにならない間隔で同じ項目を選択すると、項目表示部分の文字列を編集できるようになるケースもあります(リストビューの「ラベル編集」)。この場合、項目文字列の右クリックにより、項目文字列を簡単にコピーすることが可能です。
初心者はどのツールを使うべきですか?
ツールの選択基準としては、解析者のスキル・解析対象・アプローチ手法・ツールの実装機能・その他の要素があり、一概にどれを使うべきとはいえません。そのため、ここに列挙した選択基準等を参考に、自分にあうかどうか色々なツールを解析の実践で試されることをお奨めします。このことは、自分なりの解析スタイルを模索するということにおいても、とても重要な要素といえます。サイト管理者としては、当サイトで紹介しているツールを使って頂ければ幸いです。
なお、ひとつのプログラム解析ツールにこだわらず、複数のプログラム解析ツールをうまく組み合わせて併用し、プログラム解析の工数を減らすという創意工夫も大切です。気になったツールを見つけたら、まずは仮想PC上で試してみることをお勧めします。あるプログラム解析ツールが自分に合うか否かというのは、ネット上の評価に依存せず、自分自身で判断しましょう。
やはりプログラミングの勉強をしておいた方がゲームの解析に役立つのでしょうか?
アセンブリ言語やC/C++言語のプログラミングの知識が、ゲーム解析などのプログラム解析に役立つのは事実ですが、必須とまでは言えません。個人的にはアセンブリ言語やC/C++言語で、ダイアログベースのゲーム改造ツールを製作可能な程度のスキルは身に付けた方が良いかと思います。アセンブリ言語でのプログラミング例は、当Webサイトのチュートリアル欄で公開している「改造初心者向け練習用プログラム No1」と「PE2HTML」のソースコードを参照して下さい。また、チュートリアル欄では、各種開発言語での改造ツールのソースコード配布サイトも紹介しています。さらに、プログラム解析の入門書『デバッガによるx86プログラム解析入門 【x64対応版】』では、アセンブリ言語でのプログラミング例や、C言語でのデバッガ製作例を解説しています。
アセンブリ言語でのプログラミングには『MASM32』が導入や学習に適しています。C/C++言語については、無償で配布されている『Visual Studio Express Editions』や『Visual Studio Community』で簡単なアプリケーション製作から始めてみることをお勧めします。すでにネット上に多くの参考となる解説サイトがあります。これらの開発環境の入手先は当Webサイトのツール欄で紹介しています。
なお、アセンブリ言語に関する基礎知識とアセンブリ言語によるプログラミングの参考書としては、『アセンブリ言語の教科書』をお奨めします。アセンブリ言語を使用して自分でレジスタ・スタック・API関数などをどう使うか考えながらプログラムを組み、さらにそのプログラムの実際の処理をデバッガで解析することは、プログラム解析において大切な「処理の流れを理解する」力をつける上で役立ちます。特に超初心者の方は、PCゲームやマルウェアなどで解析を実践するよりも先に、処理内容を把握しており解析しやすい自作プログラムで解析の練習を行っておくと、後々実践的なプログラム解析へのステップアップがスムーズになります。
近年では、一部のプログラム解析ツールが機能拡張のためにPythonを使用するケースが増えています。そのため、アセンブリ言語やC/C++言語の基礎を十分理解した後に、Pythonにも触れてみることをお勧めします。解析される側という点では、LuaやSquirrelなど組み込み言語が、一部処理のソースコードから動的にプログラムコードを生成し実行可能といった利便性により、PCゲーム等の開発に使用されるケースが散見されます。
なお、Python等による、プログラム解析ツールの機能追加用スクリプトを製作するよりは、C/C++言語を使って自作ツールの形で同機能を実現する方が、解析初心者の方にとってよりプログラム解析のスキル向上に繋がると考えられます。特にファイルやプロセスメモリのやや複雑な読み込みや書き込みといった処理は、プログラム解析・改造ツール自作の格好の題材といえます。
ファイルパッチコードの形式について教えてください
バイナリファイルのパッチコードについては、コマンドプロンプトでのfcコマンド(/bオプション)の出力形式をベースにした差分形式と、単純にオフセットと書き込むバイナリデータを指定したり、書き換え対象オフセットの範囲とその範囲を埋めるバイナリデータを指定するといった(ゲーム用)改造コード形式が一般的です。なお、この差分形式は、アンダーグラウンドでメジャーなバイナリファイル書き換えソフトの名称からFireFlower形式とも呼ばれています。また、この改造コード形式は、アドレスとバイナリデータを指定してプロセスメモリの書き換え用改造コードにも使われています。オフセットとアドレスを混同しないよう注意して下さい。
差分形式の場合は、「*」の後がコメント、「FILENAME 」の後に書き換え対象ファイル名、オフセットは8桁で設定するのが一般的です(書き換えソフトによって異なるケースあり)。下記の場合、3行目ではオフセット0x2054のバイナリデータが「74」ならば「EB」に書き換えることを意味します。
*Ver7.4用 FILENAME Target.exe 00002054: 74 EB 000051AC: 55 C3改造コード形式の場合は以下の形式が一般的といえます。1行目ではオフセット0x010000から5バイトを「E703646401」で書き替えることを意味します。2行目はオフセット0x0100から0x01FFを指定バイナリデータ「E703」を繰り返し書き込んで埋めるという意味です。いずれも、差分形式のような書き換え前データの照合は行いません。
010000-E703646401 0100>01FF-E703上記差分形式及び改造コード形式のバイナリファイル用パッチコードの実行と、差分形式パッチコードの作成については、『スペシャルねこまんま57号』の「簡易バイナリファイル書き換え」機能 (メニューの[ファイル]→[簡易バイナリファイル書き換え] ) が対応していますので、ご利用頂ければ幸いです。『スペシャルねこまんま57号』ではファイルマッピングを使用しており、書き換え対象ファイル全体をメモリ上に読み込みませんので、巨大なバイナリファイルでもフリーズすることなく高速に書き換えを行うことが可能です。
常用している解析ツールをセキュリティソフトがマルウェアと検出しました
そのセキュリティソフト開発元、あるいはアンチマルウェアエンジン提供元に対して、「False Positive Report」(誤検出の報告)を出しましょう。ほとんどのセキュリティソフトは誤検出レポートを受け付けるシステム(Webページなど)を用意しています。たいていは作者側でもこの誤検出レポートは送信しているのですが、すべてのセキュリティソフトまではカバーできないのが実情です。また、セキュリティソフトによっては、購入者以外の誤検出レポートを一切受け付けないため、Virustotalなどで誤検出の発生が判明しても、作者側では対応できないこともあります。なお、このレポート提出時には、誤検出された実行ファイルのみではなく、挙動確認ができるように「付属DLLも含めた実行ファイル一式」を送るようにしたほうが、よりスムーズに対処されると考えられます。
また、この誤検出レポートへの対応はある程度自動化されているとはいえ、このレポートを人が読む場合を考えて、当該セキュリティソフトを激しく非難するような、好ましくない表現はなるべく控えましょう。
『うさみみハリケーン』が頻繁にセキュリティソフトから「マルウェアと疑わしい(Suspicious)」ソフトウェアだと誤検出されるように、プログラム解析ツールが誤検出の対象となることは珍しいことではありません。このような誤検出の場合、検出名としてはたいてい「Suspicious」や「Generic」・「Gen」といった、マルウェアや亜種ウイルスと疑わしい(マルウェアそのものではない)ことを示す名前がつけられます。
<参考>誤検出が明らかな解析ツールのインストール・使用方法
まず対象となる解析ツールのインストール用フォルダを作成します。そのフォルダをセキュリティソフトの設定で「スキャン除外対象」に設定してから、そのフォルダ内へ解析ツール配布ファイルの(再)ダウンロードと内容の解凍またはインストールを行います。これにより、たいていのセキュリティソフト使用時で、誤検出を回避して対象解析ツールをインストール・使用することが可能になります。
API関数の書式や引数に指定する値等をすばやく探し出す方法はありませんか?
かつてはWindows SDKでの関数名検索が有用とされていましたが、現在では、API関数等の仕様の詳細はネット上での検索でも簡単に情報を得ることができます。なお、API関数については、その書式から使用例のC言語ソースまで掲載しているプログラミング系解説サイトが多々あり、プログラム解析を学ぶ上でも参考になります。引数については、Windows SDKで目的の関数に用いるインクルードファイルと引数に指定する定数名を確認した上で、同インクルードファイルをテキストエディタで開いて定数名を検索すれば定数の実際の値を得られます。このインクルードファイルは、Windows SDKだけでなく各種コンパイラにも付属しています。実際に頻用するインクルードファイルは、「Kernel32.DLL」等に対応する「winbase.h」、「User32.DLL」に対応する「winuser.h」および「ntdll.dll」に対応する「WinNT.h」等です。なお、アセンブラでのコーディング用に、主要な関数が用いる定数名と実際の値を定義している汎用インクルードファイルも公開されていますので、それを参照するという選択肢もあります。
必要なインクルードファイルのみをWindows SDKのISOファイルから抽出する場合は、WinRARなどでISOファイルの内容を展開し、[Setup]フォルダ→[WinSDKBuild](x64は[WinSDKBuild_amd64])フォルダ内のcabファイルから抽出します。
逆アセンブルコードリスト上で文字列参照箇所をすばやく見つけるには?
『OllyDbg』にはプログラムが参照する文字列一覧をアドレス付きで表示する機能や、対象文字列先頭アドレスを指定して参照コード一覧を表示する機能があります(「OllyDbg Q&A」参照)。また、『うさみみハリケーン』が実装している逆アセンブラでも、『OllyDbg』の様に、参照文字列一覧を表示しワンタッチで逆アセンブルコードリスト上での参照箇所を確認可能です。しかし、この様な参照文字列抽出は文字列認識アルゴリズムとの関係上完璧に行われない場合もありえるため、「DATA」または「.data」などのデータセクションを『OllyDbg』や『うさみみハリケーン』上でダンプして、その内容を視認しておくことをお奨めします。『うさみみハリケーン』には、プロセスメモリ上の任意の指定範囲から、格納されている文字列を一括抽出する機能も実装しています (メニューの[編集]→[指定メモリエリアから文字列を抽出])。
なお、対象実行ファイルが『Delphi』または『C++ Builder』でコンパイルされている場合は、コードセクション内(「CODE」や「text」)にダイアログの設定フォームに関する文字列等が含まれます。特に『Delphi』では警告メッセージ文字列等もコードセクション内に含めることが可能です。しかし、現状では、特にPCゲームの実行ファイルが『Delphi』または『C++ Builder』でコンパイルされているケースはごくまれですので、参考程度に考えておいてください。
また、「DATA」セクション等に格納された、プログラムが認識する文字列は、値00h(ANSI)や値0000h(Unicode)を文字列終端としていることに注意が必要です。
PE(Portable Executable)ヘッダの詳細情報を簡単に知りたい。
まずPEヘッダについて書かれた資料を読んでおくことをお奨めします。たとえばMicrosoftの資料「PE Format」が参考になります。さらに、現在ではネット上でPEヘッダについての良質な解説ページも複数見つかりますので、必要に応じて検索サイトを利用することをお勧めします。そして、『うさみみハリケーン』付属のPEエディタ『UMPE』を使用することで、PEヘッダの詳細情報の表示や編集が可能です。
『OllyDbg』でのデバッグ中ならば、PEヘッダ詳細情報は、まずメニューの「View」(表示)から「Memory」(メモリ)でメモリマップを表示します。この時点で「CODE」や「DATA」等の各セクションの簡単な情報を得ることができます。さらに、メモリマップの一番上の列(カラム)にある「Contains」項目が、「PE header」となっている列を選択・右クリックから「Dump」でPEヘッダの詳細情報が表示されます。
コード無効化のメモリパッチへNOP命令を用いるべきではないと聞いたことがありますが...
結論からいえば、コード無効化のメモリパッチへNOP命令を用いても構いません。ただし、NOP命令は「何もしない」のではなく、実際には「(E)AXレジスタと(E)AXレジスタの内容を入れ替える」という処理を行っており、当然ながら処理に相応の時間を要します。そのため、メモリパッチでコードを無効化したい範囲があれば、ジャンプ命令で無効化したい範囲の先頭と終端を結ぶバイパス書き換えで対処するのが「美しいコーディング」といえます。この際、メモリパッチ箇所の視認性や可読性を高めるためのパディング(不要箇所への詰め物)に、「実行されないコードとして」NOP命令を用いるのは問題ありません。なお、コードを無効化したい範囲あるいはコード書き換え後の余剰範囲が4バイト以下程度であれば、NOP命令による対象範囲上書きでも適切といえます。ちなみに、質問にあるNOP命令使用への否定的な意見は、決して誤りではありません。過去に海外で発売された一部のゲームは、改造対策として「連続NOP処理検出機能」を実装していたためです。ただし、現在では改造対策に「連続NOP処理検出機能」よりも優れた多くの方法が編み出されており、今後「連続NOP処理検出機能」が即時ゲーム終了フラグや時限起動型の改造トラップ用フラグ設定に用いられる可能性は低いと考えられます。そのため、個人的には「コード無効化のメモリパッチへNOP命令を用いても問題ない」と判断しました。
なお、一部のコンパイラは「Code Align」機能により、コーディングに関係なくコンパイルされた実行ファイルにはNOP命令が多用される仕様となっています。
『PeRdr』のレジストリへの登録について、レジストリをいじるのはちょっと怖いです。
(注)この質問と回答はすでに古いものですが、エクスプローラーの右クリックメニューにプログラム解析ツールを登録する際の参考になるため残しています。追記:
エクスプローラー用コンテキストメニュー(右クリックメニュー)に任意のプログラム解析ツール等を登録するならば、「拡メ」など専用のソフトウェアを用いれば簡単に行うことができます。
当Webサイトで配布している『PeRdr』用REGファイルを使えば、手動でレジストリを書き替える必要はありません。また、このREGファイルにより変更される箇所は拡張子との関連付けを設定しており、Windowsの中核であるシステムには関与していないため、たとえ同梱REGファイルを使わず手動で少々間違えて書き替えても、深刻な状況を招くことはまずありません。
また、レジストリの書き換えに不安のある方は、あらかじめ書き換え箇所の部分的なバックアップを作成することをお奨めします。例えば、上記REGファイルではレジストリの「HKEY_CLASSES_ROOT\exefile\shell\」以下を書き替える訳ですから、レジストリエディタでこの「shell」の部分を選択して、メニューから「レジストリ」→「レジストリファイルの書き出し」でレジストリ該当部分のバックアップが作成できます。これにより、「HKEY_CLASSES_ROOT\exefile\shell\」以下の必要以外の部分を間違って書き替えても、上記REGファイル同様に、バックアップのREGファイルをエクスプローラ上でダブルクリックすればレジストリの該当箇所がバックアップで上書きされます。
なお、手動またはアプリケーションによるレジストリ書き換え不具合への対処には、Windowsの機能である「復元ポイント」を作成するという選択肢もあります。
ヘキサエディタとバイナリエディタの違いを教えて下さい。
正確な表記か否かです。正式名称:ヘキサデシマルエディタ
略称:ヘキサエディタ
テキストファイルエディタとの対比用:バイナリファイルエディタ
誤り:バイナリエディタ
いわゆる「バイナリ」という単語には「2進法の」という本来の意味と、それから派生した、「(データが)0と1で構成される」という2つの意味があります。上記を見て頂ければ分かると思いますが、「ヘキサエディタ」との対比という点で「バイナリエディタ」とは前者の意味であり、「2進法即ち0と1のみを用いた書き換えによるファイル編集ソフト」を指すことになります。また、「バイナリ」を「0と1で構成される」の意味で用いる場合は「バイナリデータ」、更に「バイナリデータ」で構成されるテキスト形式以外のファイルは、上記の通り「バイナリファイル」と表記すれば「バイナリ」という単語の意味の正しい使い分けができますので、この様な表記が適切と言われています。
実際のところ、日本では上記の2つの意味を混同した、ヘキサエディタを「バイナリエディタ」とする表記が定着しており、その表記が問題にされることは現在では殆どありませんし、私は「ヘキサエディタ」という表記を強要するつもりもありません。個人的には、日常使うに当たっては特に「バイナリエディタ」でも構わないと考えています。しかし、英語のネイティブスピーカーであるリバースエンジニアに対しては、「バイナリエディタ」は一応意味は通じるものの「変な英語」に聞こえているようです。 当然ながら海外では「Hex editor」あるいは「Binary file editor」と表記するのが一般的です(リバースエンジニアのサイトでは特に)。ただ、海外においても「言葉の乱れ・揺らぎ」というものはあります。
ちなみに、海外のソフトで本物の「バイナリエディタ」(0と1のみを用いた書き換えによるファイル編集ソフト)があります。
関連して、一部の日本製のバイナリエディタは、オフセットのことを「アドレス」と表示しています。これは、バイナリエディタは汎用性が高く、プログラム開発・解析と接点の無い一般人の使用も多いため、英単語の「正確な意味」よりも「イメージしやすさ・示すものの伝わりやすさ」を優先した結果と推測されます。
MOV命令の意味の言い方が人によって違うのですが...
MOV命令は、本来の意味はデータの「転送」ですが、その使い方によって表記が異なります。1.メモリからメモリへ→「転送」
2.レジスタからレジスタへ→「転送」
3.メモリからレジスタへ→「ロード」
4.レジスタからメモリへ→「ストア」
なお、ここでの「転送」は転送元データが消去される訳ではないので要注意です。意味としては「コピー」と考えた方が良いでしょう。このような誤解を避けるために「代入」と表記しているケースもあります。
(注)上記1のメモリ間転送は、MOV命令を2回用いてロードとストアを連続で行うことを意味します。メモリ間での直接転送は、アドレッシングモード(レジスタ・セグメントレジスタ・メモリ・データのデータ転送可能組み合わせ)に含まれていません。
mov ax,word ptr [00450000]
mov word ptr [0045048C],ax
メモリ間転送の転送対象メモリサイズが4バイトならば、スタックを利用するという選択肢もあります。
push dword ptr [00450000]
pop dword ptr [0045048C]
同転送対象がメモリブロックならば、ストリング操作命令とリピートプリフィックス命令を組み合わせます。ここでは処理の前後でレジスタの内容を変化させないように、pushad及びpopad命令で汎用レジスタの退避と復旧も行っています。
pushad
cld
mov ecx,20h
mov esi,00450000h
mov edi,0045048Ch
rep movsd
popad
うさみみハリケーン用のドライバは開発しないのですか?
一部のマルウェア(およびオンラインゲーム)の解析対策は、現在では、たいていrootkitの技術を使用してカーネルモードで行っており、ユーザーモードで動作するプロセスメモリエディタやデバッガ単体では対処できません。そのため、プロセスメモリエディタやデバッガといったプログラム解析ツールと、自分で製作したドライバを併用することで解析対策に対処している方が少なくありません。ドライバ併用により、プログラム解析ツールのプロセス隠蔽や、プロセスのオープン処理やプロセスメモリ読み書き処理のカーネルモード化、各種カーネルオブジェクトの操作などが可能になります。
このようなドライバは、公開すれば迅速に対策されてしまうことや、バグがあるとユーザーのシステムに深刻な影響を与えかねないといった理由から、ドライバ自体は公開されず、ドライバ自作のヒントとなる技術情報のみ提供されるというパターンが見受けられます。なお、「セキュリティツール」として、このようなドライバを自作する上で参考となるソースコードを配布しているサイトもあります。
現実的な問題として、もしプログラム解析ツールに上記のようなドライバを同梱した場合は、即座に解析・改造対策を施される可能性や、特にカーネルモードで解析対策が実行されている状態では、プログラム解析ツール側のドライバが安全に動作しない可能性があることから、ドライバの開発は見送っているのが現状です。
さらに、プロセス隠蔽やAPIのフックと処理のフィルタリングを行うドライバは、高確率でセキュリティソフトからマルウェアと認定されます。また、システムの深部を弄るドライバを使用するソフトウェアは、たとえドライバ使用が明示的であっても、好まない方が多いという実情もあります。
某PCゲームなのですが、改造ツールを起動するとゲームが強制終了します。
このケースは、「スペシャルねこまんま57号」を含む国産の改造ツール9種類を、ウィンドウタイトルあるいはウィンドウクラス名で探索し、該当ウィンドウが存在した場合は即座に終了するという、改造対策の典型的なケースでした。このようなケースでは、大抵FindWindow関数か、EnumWindows関数とGetWindowText関数・GetClassName関数併用等の手法が用いられます。「スペシャルねこまんま57号」の場合、このようなケースではウィンドウタイトルの変更で対処します(クラス名はダイアログ標準なのでウィンドウタイトル変更だけで対処可能)。近年、デバッガ検出・メモリ改ざん検出・実行ファイルの改ざん検出・独自のパッカー使用等、改造対策の多様化が進んでいます(「OllyDbg Q&A 参照」)。 また、改造ツールを検出すると、自分ではなく改造ツールを強制終了させるゲームも現れましたので注意が必要です。
某PCゲームの2重起動(多重起動)制限を解除したいのですが、どうアプローチすれば良いか分かりません。
一般的な2重起動制限解除アプローチとしては、CreateMutex関数呼び出し後にGetLastError関数戻り値を定数ERROR_ALREADY_EXISTS(B7h)と比較しているコードを探し出し、その後に続く条件ジャンプを書き換えます。なお、2重起動の余地がある不完全な手法ですが、OpenMutex関数を使用するケースもあります。特定名のミューテックスをこの関数でオープンして、戻り値がゼロでなければ2重起動と判断します。
他の2重起動検出手法としては、自分が既に起動していることを、自分と同じウィンドウタイトルやウィンドウクラス名を持つウィンドウの存在を探索した結果で判断するケースもあります。この場合は、FindWindow関数か、EnumWindows関数とGetWindowText関数・GetClassName関数併用等の手法が用いられます。
さらに、SetProp関数でウィンドウのプロパティリストに識別用の文字列を設定しておき、EnumWindows関数等でウィンドウを列挙しながらGetProp関数でその文字列を持つウィンドウが存在するか確認することも可能です。GetProp関数の戻り値がゼロでなければ2重起動と判断します。
また、CreateFileMapping関数で特定名のファイルマッピングオブジェクトを作成し、GetLastError関数の戻り値を定数ERROR_ALREADY_EXISTS(B7h)と比較することで2重起動を検出可能です。
当方では未確認ですが、CreateEvent関数を使用したケースもあるようです。2重起動検出手法はCreateFileMapping関数のケースと同じになります。
基本的に、どうアプローチすれば良いか分からない場合は、思いつくキーワードで検索することから始めると良いでしょう(このケースでは「2重起動防止」等)。
改造済の実行ファイルを配布したいのですが。
日本以外の東アジア諸国や東南アジア諸国のゲーム解析系サイトでは、不正コピー対策処理を無効化した改造済ゲーム実行ファイルが普通に配布されています。しかし、日本では著作権法と不正競争防止法に抵触すると見られるため、配布してはいけません。現在の不正競争防止法では、技術的制限手段の効果を妨げてデジタルデータを改変するプログラム等の譲渡や提供は違法行為となります。そのため、技術的制限手段を回避しつつセーブデータを改変する「プロテクト破り」の改造ツールはもちろん、同様の処理を行うファイルパッチコードやプロセスメモリ用パッチコード形式という文字列としての実行ファイル改造情報も、配布は行わないでください。
プロセスメモリエディタの検索で判明したパラメータのアドレスが変なメモリ領域にあります。
このケースでは、プロセスメモリ上のゲームのEXEモジュールに属するメモリエリアではなく、EXEモジュールが使用する、ゲームと共にインストールされたDLLモジュールに属するメモリエリアでパラメータを管理していました。このようなケースでは、DLLファイルの拡張子を変更していることもありますので、解析時には必ず対象プロセスのモジュールリストを確認してください。また、このゲーム専用のDLLファイルが読み込まれるアドレスは、環境により異なることがありますので注意が必要です。『うさみみハリケーン』や『スペシャルねこまんま57号』の改造コードを用いた、DLLモジュールに属するアドレスの書き換えについては、ヘルプの「改造コードの活用例」を参照願います。
なお、ゲームのパラメータなどを格納するメモリエリアは、VirtualAlloc関数による確保時に第3引数に定数MEM_TOP_DOWN(0x100000)を論理和で追加設定することにより、そのアドレスは7FF*****といった可能な限り高いアドレスに確保することができます。この高アドレスのメモリエリアは、プロセスメモリエディタでの通常の指定検索範囲に入らない可能性が生じるため注意が必要です。逆に言えば、プログラムコードを書き換えてVirtualAlloc関数呼び出し時に常にこのフラグを追加すれば、大きなサイズのメモリエリア作成時には必ず高いアドレスとなり、その高いアドレスにゲームで使用するパラメータなどのデータが集中すれば、プロセスメモリエディタでの検索が容易になるケースもあると考えられます。
「環境依存型変動アドレス」について教えてください
●アドレス変動の仕組み
例えば、PCゲームのパラメータ等のアドレスが、プロセスメモリ上に『***.exe』といった解析対象アプリケーションのメインモジュールがロードされたエリアであるモジュールエリア内(基本的にアドレス0x400000~のメモリエリア)にあるならば、そのアドレスは実行環境等に依存しない固定アドレスとなります。これは、メインモジュールのロード処理は実行環境に影響を受けず、また、メインモジュール内のデータ格納用セクションには、基本的にプログラム側で使用することやサイズ上限が『確定している』データを格納するためです(注:Windows Vista以降では、EXEモジュールをランダムなアドレスにロードする機能ASLRが実装されています。ASLRの解説と、プログラム解析におけるASLRへの対処については、この質問と回答を参照してください)。
しかし、解析対象アプリケーションが起動後に自分で動的に確保したメモリブロックおよび、そのメモリブロックに格納されたパラメータ等のアドレスは固定とはなりません。これは動的に行うメモリ確保では、状況に応じて確保されるメモリブロックのアドレスが変化するためです。 動的なメモリブロック確保について、ヒープ(プログラムが実行中に動的にメモリを割り当てるためにOSが用意している、あるいはアプリケーションが自分でHeapCreate関数を使用して作成したメモリ領域)から動的に必要なサイズのメモリブロックを確保するHeapAlloc,GlobalAlloc, LocalAlloc関数のうち、GlobalAlloc, LocalAlloc関数は速度面等の理由により使用可能ではあるが現在では推奨されていません。ちなみに、WindowsNT系OS上でGlobalAlloc関数を使用して確保されたメモリブロックのアドレスは、WindowsOSにおけるメインモジュールの開始アドレス0x00400000よりも低いアドレスになります。 また、プログラミング手法として大きなメモリブロックを確保する場合には、WindowsOSのバージョンに関わらずパフォーマンス面で適しているVirtualAlloc関数の使用が推奨されています。VirtualAlloc関数はヒープからの確保ではなく、簡単に言えばプロセスの仮想アドレス空間内のページ(システムがメモリ管理に用いるメモリ単位で、x86コンピュータでのサイズは0x1000バイト)に使用可能属性を設定して割り当てるAPI関数です。なお、1つのアプリケーションだが実際には2つ以上のプロセスが同時に実行されるケースでは、CreateFileMapping関数で第1引数に定数INVALID_HANDLE_VALUE(0xFFFFFFFF)を指定して作成したファイルマッピングオブジェクトを、プロセス間の共有メモリとして使用することもあります。 これらのメモリ確保用API関数を用いて確保されたメモリブロック上のアドレスは、基本的にOSその他実行環境によって変化する環境依存型変動アドレスとなります。使用OSが同じ場合にプロセスヒープ(OSが各プロセス用に作成する既定のヒープで、スタックとは別物)から確保されたメモリブロックのアドレスが同一になるケースもありますが、確実とはいえません。なお、解析対象アプリケーションのプログラマがメモリ確保の設定やタイミング等を変化するように設計すれば、実行環境に依存するアドレス変動に加え、アプリケーション起動ごとのアドレス変動も生じさせることが可能です。また、起動ごとのアドレス変動は、常駐ソフト等の環境要因によっても起こりえます。おおむね、大きなメモリブロックの確保が必要になるPCゲームに加え、家庭用ゲーム機のエミュレータでもパラメータ等のアドレスは環境依存型変動アドレスになるといえます。 さらに、パラメータ等の管理を解析対象アプリケーション専用のDLLがそのモジュールエリア内で行う場合、このようなDLLがプロセスメモリ上にロードされるアドレスは実行環境によって必ずしもWindowsOSにおける基本的なDLLロード先アドレスである0x10000000とはならないため、この場合も環境依存型変動アドレスとなります。このような解析対象アプリケーション専用のDLLは、解析対象アプリケーションがLoadLibrary関数を使用して動的にロードすることもできますが、『***.exe』といったメインモジュールからリンク(実行ファイルの設定で実行に必須のDLLとして指定)することで、アプリケーションの起動時に自動的にプロセスメモリ上にロードされるようにすることもできます。また、プログラムの処理としてはGetModuleHandle関数使用により、プロセスメモリ上で特定のDLLがロードされたアドレスを取得するのは容易です。 アドレス変動に関しては、解析対象アプリケーションが、データを格納するための必要最小限のサイズのメモリブロックを確保し、必要に応じてより大きなサイズのメモリブロックを確保してデータを格納し直すことがアドレス変動の原因となるケースもあります。このようなケースではHeapReAlloc関数かメモリブロック再確保用の自作関数を用い、格納するデータの増加に応じてより大きなサイズのメモリブロックを確保し、元のメモリブロックの内容を新しく確保したメモリブロックにコピーしたうえで元のメモリブロックを解放します。 HeapReAlloc関数はメモリ確保対象ヒープ内の使用状況や再確保するメモリブロックのサイズに応じて、元のメモリブロックを拡張あるいは、別途メモリブロックを確保してそこに元のメモリブロックの内容をコピーします。元のメモリブロックを拡張する場合はアドレス変動は生じません。 環境依存型変動アドレスは、プログラム側でそのアドレスにアクセスする以上、対象アドレス(群)の基準となるアドレスをポインタ(アドレスを指定するために特定アドレスに格納された値を使うやり方)として格納する必要があります。また、プログラム側では確保したメモリブロックが不要になった時に解放するために指定するパラメータとして、確保したメモリブロックの先頭アドレスを保持しなければなりません。そのため、データを格納するために確保したメモリブロックの先頭アドレスを格納するポインタは、基本的にEXEモジュールのモジュールエリア内の固定アドレスあるいは、DLLモジュールのモジュールエリア内でDLLロード先アドレスからみて相対値が固定のアドレスに配置することになります。なお、ヒープから確保されたメモリブロックの先頭アドレスはページ境界(0x1000単位)上にはならず、0x00831E90といった半端なアドレスとなります。一方、VirtualAlloc関数で割り当てられたページの先頭アドレスは64KB境界(0x10000単位)上となります。 |
●アプローチ
これら環境依存型変動アドレスの解析においては、パラメータ等のアドレスにデバッガで読み書きまたは書き込みブレークポイントを設定してブレークさせ、ブレーク箇所周辺の逆アセンブルコードリストを読解してプログラムの処理上どのようにアドレスを管理しアクセスしているかを把握するというアプローチが基本となります。この際、逆アセンブルコードリスト上で処理をさかのぼって観察し、特にパラメータ等のアドレスを指定するためにレジスタに格納される値の出所に注目して下さい。また、パラメータ等のアドレスが含まれるメモリブロックの、API関数による確保処理とその後のデータ格納処理を追いかけていくというアプローチもあります。
システム情報表示ツールなどが表示する、解析対象プロセスに属するヒープの一覧をアプローチの参考にする場合は、そのヒープ一覧はあくまである瞬間でのスナップショットであり永続的な情報ではないこと、各ヒープ内の確保済みメモリブロックにはプロセスのメインモジュールだけではなくシステムDLL他別のモジュールが確保したものも含まれること及び、基本的にヒープ内のメモリブロックのアドレスは環境に依存する変動アドレスであることに十分注意して下さい。 環境依存型変動アドレスを改造コードで指定する際には、アドレスの直接指定ができないため、ポインタを利用することになります。解析対象アプリケーション側が複数のポインタを連ねてデータのアドレスを管理しアクセスしているケースでは、改造コードは多重ポインタ対応のものを使用することで対処します。 また、多重ポインタ解析の手間を省くために、解析対象アプリケーションの改造対象パラメータ等操作処理箇所をデバッガで特定後、その処理箇所でパラサイトルーチンへ繋がるようコードを書き替えて、パラサイトルーチンの処理として固定アドレスにポインタを自作する方法もあります。この自作ポインタはプロセスメモリ上で解析対象アプリケーションのメインモジュールのモジュールエリア内にある未使用領域に配置します。 ポインタに関する解析結果の公開にあたっては、予期せぬアドレス変動要因を考慮して、公開前にOSを再起動させて解析対象アプリケーションも再起動させたうえで、ポインタを用いた改造コード等の動作を再確認されることをお奨めします。 このようなポインタの解析を含む、環境依存型変動アドレス解析のスマートなアプローチについては、DANAさんのサイト『DANAの部屋』の実践改造講座が大いに参考になります。 |
●構造体
PCゲームにおけるキャラクター情報等、メモリブロックを確保して複雑なデータを格納する場合は、そのデータは色々な要素を格納できる構造体(複数のデータを一つに集めたデータ構造)の形式をとるケースが多いといえます。構造体の中にさらに別の構造体を格納することも可能です。基本的に、構造体を使用する場合のプログラムの処理としては、複数のポインタを連携させた効率的なデータ管理・アクセス手法が使われます。
<参考>構造体を用いたデータ格納例 以下は構造体の要素を大幅に省略して簡略化したモデルです。さらに、プロセスメモリ上へのデータの格納方法はプログラマのコーディングによって変化するため、以下はあくまで1つの例に過ぎないと考えて下さい。 [構造体のヘッダ] 正義の味方のキャラ数(0x2バイト) 悪の組織のキャラ数(0x2バイト) ---他の色々なデータは省略--- [正義の味方] キャラ1名前(0x10バイト) HP(0x4バイト) MP(0x4バイト) 所持金(0x4バイト) 所持アイテム数(0x2バイト) 所持アイテム1レベル(0x2バイト) 所持アイテム2レベル(0x2バイト) ---その他の要素が続く--- キャラ2名前 HP MP 所持金 所持アイテム数 所持アイテム1レベル 所持アイテム2レベル ---その他の要素が続く--- 以下キャラ3以降に続く [悪の組織の一員] キャラ1名前 HP MP 所持金 所持アイテム数 所持アイテム1レベル 所持アイテム2レベル ---その他の要素が続く--- 以下キャラ2以降に続く解析対象アプリケーションが、ソースコードのメンテナンス性向上等を目的として構造体の形式でデータを格納している場合、プログラム上でのデータの管理とアクセスは、アドレスの直接指定ではなくポインタを使用することで処理の効率化を図ります。上の例で各キャラクターのデータのサイズが0x100ならば、例えば正義の味方キャラ2の所持金データは、構造体の先頭アドレスである「正義の味方のキャラ数」のアドレスからみて、+(0x2+0x2) +(0x100) +(0x10+0x4+0x4)のアドレスに格納されています。ここで、正義の味方データの先頭アドレスとなる「正義の味方キャラ1名前」のアドレスがポインタとして別の場所に格納されていれば、「正義の味方キャラ2の所持金」のアドレスはプログラム上の処理では「ポインタの格納値+0x100*1+0x18」として効率的に処理され、同様に他のキャラの各要素へアクセスすることも容易になります。 逆アセンブルコードリスト上でのこのようなポインタに関するアドレスの演算処理は、「LEA EDX,[ECX*4+00425180] 」といった形で特にLEA命令やレジスタを使って処理を高速化することが可能であり、また、対象アドレス格納値の取得や変更にはMOV命令で「MOV EAX, DWORD PTR[EBX+ECX*4+1470]」、「MOV DWORD PTR[EBX+ECX*2+1470], EAX」といった効率的な処理を行うことも可能です。 構造体のデータにアクセスするためのポインタは構造体に含めることも、全く別のメモリエリアに配置することもできます。また、上の例のように構造体でデータを管理する場合は、一般的に各ポインタを連携させて使用するため、プログラムの処理の効率上、使用する複数のポインタを特定のメモリエリアに固めて格納していることもあります。 |
相互修復型パラメータの書き換え方法を教えてください。
この「相互修復型パラメータ」とは、ひとつのパラメータをプロセスメモリ上で複数箇所に格納し、高頻度でその複数のパラメータをチェックして、複数箇所のパラメータのうちひとつがプロセスメモリエディタで書き換えられた時に、他の箇所に格納されたパラメータを使って元の値に修復するケースを指します。いわゆるメモリ改ざん検出・防御手法のひとつです。このようなケースでは大抵の場合、複数箇所のパラメータを、複数の改造コードを一括実行してほぼ同時に書き換えれば対処可能です。しかし、それで対処できない場合は、デバッガでパラメータ格納アドレスへ書き込みメモリアクセスのブレークポイントを設定することで、相互修復ルーチンを特定し無効化する必要があります。また、『うさみみハリケーン』のプロセス停止・再開機能や、『スペシャルねこまんま57号』のデバッガで対象プロセスにアタッチして「実行一時停止」機能を用いる、対象プロセスの全スレッドを停止させた状態で複数箇所のパラメータを書き換える手法も有効です。
プログラムの参照文字列を解析の端緒とする場合の注意点を教えて下さい。
まず、プログラムが実行される際に参照する文字列は、必ずしもANSI(日本語ならシフトJIS)やUnicode形式の文字列として実行ファイル中に予め用意される訳ではない点に注意が必要です。つまり、プログラム実行中に必要に応じて文字列をスタックあるいはデータセクション内などに動的に生成して参照し、さらに不要になれば消去することも可能です。そのため、OllyDbg等で表示されるプログラムの参照文字列が「総ての」参照文字列とは限りません。たいていの場合、プログラムが参照する予め用意された文字列はデータセクション内に格納されていますが、Delphi等コンパイラによってはコードセクション内に参照文字列が格納されることもあります。また、API関数は関数名の接尾辞が「A(ANSI)」か「W(Wide:Unicode)」かで、参照される文字列がANSIかUnicode形式かを判断することが可能です。
なお、参照する文字列をキャラクターコード(文字コード)に演算を施すことで暗号化した上で予め用意しておき、必要時に復号化することも可能です。この方法は、CD/DVDチェック時のオリジナルCD/DVD認証用ファイル名や、多重起動防止用ミューテックス名等に使用されることがあります。キャラクターコード表は『うさみみハリケーン』のヘルプなどで見ることが出来ます。
さらに、ゲームの解析においては、パラメータの数値を文字列に変換する際に使用されることがあるwsprintf関数等についても、Windows SDKあるいはネット上の資料等を参照して理解しておくことを強くお勧めします。例えばwsprintf関数の第2引数に使用可能な文字列「%d」は指定した符号付き10進整数を文字列に、「%u」は指定した符号無し整数を文字列に変換する書式指定文字列です。具体例としては、パラメータ表示用文字列作成のために、「体力:%d, お金:%u」という書式指定文字列をwsprintf関数の第2引数として指定し、第3、第4引数には体力とお金の値を指定します。この場合、第3、第4引数に指定した値の出所がどこにあるか、逆アセンブルコードリスト上で辿っていくことがポイントになります。なお、数値を文字列に変換する方法は他にもあり、必ずしもwsprintf関数あるいは書式指定文字列を使う訳ではないので注意して下さい。
ゲームのシナリオ表示等、文字列を画面に「描画」する処理箇所を特定するためのアプローチ方法が分かりません。
デバッガで逆アセンブルコードリスト上のテキスト描画用API関数呼び出し箇所を把握し、同箇所の実行にブレークポイントを設定して実際の描画処理を追いかけます。代表的なテキスト描画用API関数は以下。DrawText
DrawTextEx
GrayString
TextOut(使用されることが多い)
ExtTextOut
TabbedTextOut
なお、PCゲームではGetGlyphOutline関数を使うケースもあります。
また、表示済み文字列のメモリサーチを行い、その文字列格納アドレス周辺のバイナリデータを視認して、「これから描画されるはずのテキスト」が発見された場合は、そのテキスト内のアドレスの読み込みにブレークポイントを設定してブレークさせるという方法もあります。
参考までに、PCゲームなどのプログラムの処理中で参照される文字列を監視・出力したいならば、以下のAPI関数群でAPIフックを行い、処理される文字・文字列をAPIフック用DLL内からOutputDebugString関数で出力するやり方があります。出力したデバッグ文字列は、『うさみみハリケーン』の「デバッグ文字列出力を監視」機能などのモニターを使って取得します。ただし、これで全ての参照文字列が取得できるとは限りません。
WideCharToMultiByte
MultiByteToWideChar
GetGlyphOutline
GetCharABCWidths
GetTextExtentPoint32
TextOut
ExtTextOut
DrawText
DrawTextEx
wsprintf
wvsprintf
lstrlen
lstrcpy
lstrcat
StringCbCatなどlstr*関数の互換関数
_mbsincや_mbslenなどのCランタイムライブラリが提供する文字列関連関数
LoadString
タスク切り替えが出来ないゲームの対処法を知りたい。
まず、汎用のタスク切り替え手法による対処法は、拙作『うさみみハリケーン』のヘルプの「基礎用語解説」→「フルスクリーン」で、特定キーの押し下げや『スペシャルねこまんま57号』のキーフック等を用いた手法を解説していますのでご覧下さい。次に、解析による対処法を考える場合には、「タスク切り替えが出来ない」という状況をよく考察する必要があります。例えば、タスク切り替えが出来ないゲームはDirectXを使用したゲームで散見されますが、MicrosoftはDirectX 9を用いたアプリケーションでのタスク切り替えの無効化を禁止しています。そのため、キーフック他によるタスク切り替えの無効化ではなく、タスク切り替えを検出してアプリケーション強制終了等の処理を行うケースが少なくありません。
<参考>Alt + Tab キーおよびその他のタスク切り替えを無効にするにはどうすればよいですか。
さらに、解析によって対処する場合の注意点として、タスク切り替え検出ルーチンを探し出すというアプローチは工数が嵩む可能性が挙げられます。これは、タスク切り替えの検出手法が、仮想デバイスドライバを使用したり、メインウィンドウに送られてくるWM_ACTIVATE・WM_ACTIVATEAPP等のWindowメッセージで自分のウィンドウがアクティブか否か確認する方法や、GetForegroundWindow関数等のAPI関数を用いて自分のウィンドウが最前面にあるかを高頻度で確認する方法等、色々な手法があるためです。そのため、タスク切り替えに伴う強制終了等のアクションを手掛かりに、デバッガを用いてアプローチする手法が効率的となるケースもあると考えられます。
その他の対処法としては、タスク切り替えが出来ないゲームのプロセスを停止・再開等制御することで、タスク切り替えを可能にするソフトウェア『AAT(Anti Alt-Tab)』を使用するという選択肢もあります(当方では動作未確認)。 また、DirectX汎用窓化ツール『DXWnd』に実装されている、タスク切り替えをゲームに通知させない機能を利用する対処法もあります。
フルスクリーンのゲームをウィンドウモードでプレイする方法を教えて下さい。
まず最初に、対象ゲームが開発者用のウィンドウモードを実装している可能性を考慮して下さい。この場合、ウィンドウモードのトリガーは、iniファイルやレジストリの「Window=1」といったキーの値や、「xxx.exe -win」等のコマンドラインオプションであることが少なくありません。また、「Alt+Enter」他の特定キーを起動時に押すことでウィンドウモードになるケースもあります。これらは、対象ゲームの参照文字列、起動時のiniファイルあるいはレジストリ読み込み関数(GetPrivateProfileInt関数,RegQueryValueEx関数他)の呼び出し、またはキー入力認識関数(GetAsyncKeyState関数等)の呼び出しを元にアプローチを行います。次に、対象ゲームがインポートするDLL等から判断してDirectX 9(または8)を用いているならば、ウィンドウ作成およびDirectX初期化ルーチンにパラサイトルーチンを使用して処理を変更する方法があります。最も基本的なアプローチとしては、まずCreateWindow関数呼び出しの時点で第3引数に適切なウィンドウスタイル、第6,第7引数にウィンドウサイズを指定させます。それからd3d9.dll(d3d8.dll)のDirect3DCreate9(Direct3DCreate8)関数呼び出し後のD3DPRESENT_PARAMETERS構造体初期化部分でメンバ変数WindowedをTRUE(値1)に設定し、さらに同構造体のメンバ変数BackBufferFormatを、予めGetAdapterDisplayMode関数で取得されたD3DDISPLAYMODE構造体のメンバ変数Formatの値か、D3DFMT_UNKNOWN(値0)に設定します。なお、古いゲームでddraw.dllをロードしてDirectDrawを使用しているならば、DirectDrawCreate関数呼び出し直下のディスプレイとの協調設定部で、SetCooperativeLevel関数の第3引数をDDSCL_NORMAL(値8)に変更する必要があります。以上が最も基本的なアプローチですが、これで総てのゲームでウィンドウモード化が可能になる訳ではなく、他に何箇所も変更しなくてはならないケースも多々あります。
参考:D3DPRESENT_PARAMETERS構造体初期化例(ウィンドウモード) 00401000 /$ 55 PUSH EBP 00401001 |. 8BEC MOV EBP,ESP 00401003 |. 83EC 44 SUB ESP,44 00401006 |. 6A 78 PUSH 78 00401008 |. E8 BD020000 CALL <JMP.&d3d8.Direct3DCreate8> ;D3Dオブジェクト作成 0040100D |. A3 00784000 MOV DWORD PTR DS:[407800],EAX ;D3Dオブジェクトのポインタ 00401012 |. 833D 00784000 00 CMP DWORD PTR DS:[407800],0 00401019 |. 75 07 JNZ SHORT dxtest.00401022 0040101B |. B8 05400080 MOV EAX,80004005 00401020 |. EB 78 JMP SHORT dxtest.0040109A 00401022 |> 8D45 F0 LEA EAX,DWORD PTR SS:[EBP-10] ;D3DDISPLAYMODE構造体のアドレス 00401025 |. 50 PUSH EAX 00401026 |. 6A 00 PUSH 0 00401028 |. 8B0D 00784000 MOV ECX,DWORD PTR DS:[407800] 0040102E |. 8B11 MOV EDX,DWORD PTR DS:[ECX] 00401030 |. A1 00784000 MOV EAX,DWORD PTR DS:[407800] 00401035 |. 50 PUSH EAX 00401036 |. FF52 20 CALL DWORD PTR DS:[EDX+20] ;GetAdapterDisplayMode関数 00401039 |. 85C0 TEST EAX,EAX 0040103B |. 7D 07 JGE SHORT dxtest.00401044 0040103D |. B8 05400080 MOV EAX,80004005 00401042 |. EB 56 JMP SHORT dxtest.0040109A 00401044 |> 6A 34 PUSH 34 00401046 |. 6A 00 PUSH 0 00401048 |. 8D4D BC LEA ECX,DWORD PTR SS:[EBP-44] ;D3DPRESENT_PARAMETERS構造体のアドレス 0040104B |. 51 PUSH ECX 0040104C |. E8 7F020000 CALL dxtest.004012D0 ;ZeroMemory関数 00401051 |. 83C4 0C ADD ESP,0C 00401054 |. C745 D8 01000000 MOV DWORD PTR SS:[EBP-28],1 ;D3DPRESENT_PARAMETERS構造体のメンバ変数Windowed 0040105B |. C745 D0 01000000 MOV DWORD PTR SS:[EBP-30],1 00401062 |. 8B55 FC MOV EDX,DWORD PTR SS:[EBP-4] ;D3DDISPLAYMODE構造体のメンバ変数Format 00401065 |. 8955 C4 MOV DWORD PTR SS:[EBP-3C],EDX ;D3DPRESENT_PARAMETERS構造体のメンバ変数BackBufferFormat 00401068 |. 68 04784000 PUSH dxtest.00407804 0040106D |. 8D45 BC LEA EAX,DWORD PTR SS:[EBP-44] ;D3DPRESENT_PARAMETERS構造体のアドレス 00401070 |. 50 PUSH EAX 00401071 |. 6A 20 PUSH 20 00401073 |. 8B4D 08 MOV ECX,DWORD PTR SS:[EBP+8] 00401076 |. 51 PUSH ECX 00401077 |. 6A 01 PUSH 1 00401079 |. 6A 00 PUSH 0 0040107B |. 8B15 00784000 MOV EDX,DWORD PTR DS:[407800] 00401081 |. 8B02 MOV EAX,DWORD PTR DS:[EDX] 00401083 |. 8B0D 00784000 MOV ECX,DWORD PTR DS:[407800] 00401089 |. 51 PUSH ECX 0040108A |. FF50 3C CALL DWORD PTR DS:[EAX+3C] ;CreateDevice関数 0040108D |. 85C0 TEST EAX,EAX 0040108F |. 7D 07 JGE SHORT dxtest.00401098 00401091 |. B8 05400080 MOV EAX,80004005 00401096 |. EB 02 JMP SHORT dxtest.0040109A 00401098 |> 33C0 XOR EAX,EAX 0040109A |> 8BE5 MOV ESP,EBP 0040109C |. 5D POP EBP 0040109D \. C3 RETNまた、上記DirectX初期化ルーチンからd3d9.dll(d3d8.dll)内にステップインして、D3DPRESENT_PARAMETERS構造体のメンバ変数判定部を特定した上で、このDLLを直接書き換える方法もありますが、これは対象ゲーム以外のDirectXを使用するアプリケーションに影響を及ぼしかねないという問題があります。
このように、パラサイトルーチンやDirectX関連DLL書き換えによるアプローチは汎用性等に問題があるため、近年では対象ゲームの使用DLLが行う処理を仲介DLLでフックして、CreateDevice関数処理部等を自作したものに置き換えるアプローチを取るケースが増えています。DirectX初期化処理のフック手法は日本や海外のゲーム専門リバースエンジニアの間で改良が進められ、以下のツールのような、多くのゲームでウィンドウモード化を可能にする汎用性の高い手法が公開されています。
現在DirectX使用アプリケーションのウィンドウモード化機能を持つソフトウェアとしては、『DXWnd』や『Window Mode Patch for Game』などがあります。これらのソフトにより、多くのゲームでウィンドウモード化が実現可能です。また、対象ゲームによっては、『VMware Workstation Player』や『Oracle VM VirtualBox』などの仮想マシン上で実行させることによる、擬似的なウィンドウモード化が可能なケースもあります。仮想マシン構築ソフトにはDirect3D対応のものもあり、大抵のゲームは仮想マシン上で実行可能と考えられます。
PrintScreenキーでスナップショットが取れないゲームの仕組みを教えてください。
スナップショットが作成できないのは、PrintScreenキー押し下げを監視して、キー押し下げのタイミングでクリップボードの内容をクリアしているためと考えられます。キーフックを用いてPrintScreenキー押し下げを監視する場合は、キー入力のウィンドウメッセージを取得して、このキーの仮想キーコードの値(2Ch)と比較した上で処理を分岐させることになります。また、クリップボードの内容のクリア処理は、OpenClipboard関数呼び出し後に第1引数を08h(CF_DIB)にしてGetClipboardData関数を呼び出してクリップボードの内容が画像データかどうか確認し、画像データの場合はEmptyClipboard関数を使ってクリップボードのデータをクリアします。
なお、クリップボードの内容のクリア処理は、対象ゲームのEXEファイルではなく、対象ゲーム専用のロードされたDLLファイル(拡張子変更可能)内のコードで実行されている可能性が高いことに注意が必要です。このようなケースに限らず、ゲームの解析においては必ずプロセスのモジュールリストを確認しておくことをお奨めします。
ゲームが自己プロセスを隠蔽(いんぺい)することは可能ですか?
いわゆるプロセスメモリエディタやOS付属のタスクマネージャが表示するプロセスリストにリストアップされないよう、アプリケーションが自己プロセスを隠蔽する手法は実在します。このプロセス隠蔽にはたいてい、カーネルモードで各種操作を行うrootkitの技術が使用されています。自己プロセスを隠蔽する例としては、一部のマルウェアや、商用プロテクトが施されたオンラインゲームが挙げられます。しかし、マルウェア同様にシステムの深部を弄るプロセス隠蔽は、セキュリティ上の問題を招きかねないため、オンラインゲームではない、一般的なPCゲームに実装されるケースは少ないといえます。システム深部にタッチせずに、デバッガやプロセスメモリエディタがゲームのプロセスを認識できなくする手法もありますが、たいていデバッガなどから無効化可能なため、あえて実装するケースはまれと考えられます。ただ、ゲームのプログラマはほとんど無意味な解析対策だと分かっていても、社長やディレクターなどの上司が実装を命令することがあると聞きます。
その他のケースとしては、プロセスの隠蔽ではありませんが、「game.exe」といったゲーム開始用の実行ファイルが、別ファイルとなっているゲーム本体の実行ファイル(拡張子はEXEとは限らない)の実行及び改ざん検出用のローダーであるケースが挙げられます。この場合、大抵「game.exe」は本体の実行ファイルを実行後に終了するため、プロセスリストに「game.exe」はリストアップされません。このようなケースでは、ゲーム本体の実行ファイルに改造対策が施されている可能性を考慮して下さい。
なお、『うさみみハリケーン』のキーフック機能を使えば、プロセスリストには表示されないプロセスを、ウィンドウ情報を元に特定してプロセスリストに追加することが可能です。また、『うさみみハリケーン』には、プロセスID総当りでプロセスを列挙したり、システムが保持するパフォーマンスデータを基にプロセスを列挙するといった、特殊なプロセス列挙機能を備えており、解析対策としてのプロセス隠蔽に対処できるケースがあります。
ゲームの当たり判定解析のアプローチが分かりません
当たり判定の処理にはいろいろな手法があるため、汎用性が高い定型的なアプローチはありません。まず、API関数を用いた当たり判定としては、2つの矩形の交差を判断するIntersectRect関数の使用が挙げられます。また、補助的な関数として、指定の点が指定の矩形の境界の内側にあることを判断するPtInRect関数が挙げられます。
実際のゲームプログラミングとしては、API関数を使うのではなく、自機の座標と、敵弾の座標との演算により矩形の重なりを判定するというケースが多いようです。この場合、当たり判定処理直前にSetRect関数が使用されることがあります。
当たり判定が座標の演算による比較で行われるならば、自機座標の格納アドレスを変動サーチで探し出し、メモリアクセスへのブレークポイント設定で処理を追いかけることになります。この際、自機や敵機の座標が浮動小数点数で格納されている可能性と、座標は暗号化されており増減で絞り込まない方がよい可能性を考慮して下さい。なお、座標の格納アドレスへのブレークポイントは、無用なブレークを避けるためにスレッドを指定して設定する方が効率的と考えられます(『うさみみハリケーン』ではスレッドを指定してブレークポイント設定可)。
座標の更新のトリガーとなるWindowメッセージは、矢印キー押し下げ時のWM_KEYDOWN(0x0100)あるいは、WM_MOUSEMOVEやWM_RBUTTONDOWN(lparam)などが考えられます。たとえば、↑キーが押されたことを検出するためには、ウィンドウプロシージャならばVK_UP(0x26)、DirectInputならばDIK_UP(0xC8)といった、定数を用いた演算処理が行われます。もし、この演算処理を見つけることができれば、その処理の直後にある、座標格納アドレスへアクセスし座標を書き換える処理の解析により、比較的簡単に座標の格納アドレスを見つけることができます。
なお、当たり判定に伴う結果である自機数やHP等の減算処理のコードを特定し、それを無効化するという代替アプローチもあります。
「逆アセンブルコードの再利用」について教えてください
たとえば、暗号化されたセーブデータを復号化する目的で、セーブデータ復号化ツールを製作する場合、ゲームのプログラムに実装されているセーブデータ復号化処理をそのまま復号化ツールに流用できれば、コーディングが容易になります。(注:ゲームのプログラムを書き換えて暗号化しないセーブデータを出力させるというアプローチもあります)実際の処理としては、ゲームのプログラムの復号化処理箇所をデバッガで特定し、その逆アセンブルコードリストを整形すれば、C言語コンパイラ等が実装しているインラインアセンブラを用いることで、復号化ツールのソースコードにほとんどそのまま流用することが可能になります。
この場合、処理の流れは以下のようになります。
1.デバッガでセーブデータ読み込み時の復号化処理箇所を特定
2.特定した復号化処理箇所の逆アセンブルコードリストをテキストエディタにコピー
3.テキストエディタ上でマクロ等を用いて逆アセンブルコードリストを整形
4.3.を復号化ツールのソースコードにインラインアセンブラの形式で挿入
5.復号化ツールをコンパイルして動作を確認
この手法は、暗号化されたゲームの画像データ用Susie Pluginの製作等にも有用です。ただし、この様な手法で製作したSusie Pluginの公開が原因で、ソフトハウス側とトラブルになったケースもあります。
この様な逆アセンブルコードの再利用には、逆アセンブルコードリスト上での処理の流れと、スタックやレジスタに格納されている値との関係について理解する必要があります。特に目的の処理箇所が関数の場合、スタックフレーム内に格納されている引数やローカル変数等がどのように使われているかに注目してください。なお、スタックへのデータ出し入れは、最後に入れたデータが最初に取り出される、FILO(First In, Last Out、先入れ後出し)方式で行われます。
●<参考:関数とスタックフレーム> ;関数呼び出し部 push 2 ;引数2 push 1 ;引数1 call function add esp, 8 ;引数分スタック修正 ;関数本体(関数の処理は省略) push ebp ;ebpレジスタを新しいフレームポインタとして使うため、元の値を退避 mov ebp, esp ;スタックポインタespレジスタの値をebpレジスタに転送 sub esp, 8 ;ローカル変数(ここではDWORDx2)の設定 ;スタックフレームの内容 ;dword ptr [ebp+0C] = 2(引数2) ;dword ptr [ebp+8] = 1(引数1) ;dword ptr [ebp+4] = 関数のリターンアドレス ;dword ptr [ebp] = ebpレジスタの元の値(前のフレームポインタ) ;dword ptr [ebp-4] = ローカル変数1 ;dword ptr [ebp-8] = ローカル変数2 (関数の処理) mov esp, ebp ;スタックポインタ復帰 pop ebp ;フレームポインタ復帰 ret ;関数呼び出し元に戻る ●<参考:x86 スタックフレームのレイアウト> ・関数パラメータ ・関数のリターン アドレス ・フレーム ポインタ ・例外ハンドラ フレーム ・ローカルに宣言した変数やバッファ ・呼び出し側のレジスタの保存 (必ず上記全項目が格納される訳ではない) ●<参考:インラインアセンブラ使用例> _asm{ ;引数の設定(レジスタ渡しにすることも可能) push 2 ;引数2 push 1 ;引数1 push 0 ;ダミー ;元の逆アセンブルコードリストをなるべくそのまま使用する push ebp mov ebp, esp ;スタックポインタespレジスタの値をebpレジスタに転送 sub esp, 8 ;ローカル変数(ここではDWORDx2)の設定 ;dword ptr [ebp+0C] = 2(引数2) ;dword ptr [ebp+8] = 1(引数1) ;dword ptr [ebp-4] = ローカル変数1 ;dword ptr [ebp-8] = ローカル変数2 (関数の処理) ;API関数の呼び出しは関数ポインタを用意しておいてcall ;ジャンプ元やジャンプ先には、クロスリファレンス情報を元にラベルを指定 mov esp, ebp ;スタックポインタ復帰 pop ebp ;ebpレジスタ復帰 add esp, 0C ;引数とダミー分のスタック修正 }なお、逆アセンブルコードリストを読解した上で再利用するだけではなく、逆アセンブルコードリストを自分が得意とする開発言語のコードに置き換える練習をしておくこともお勧めします。
日本語版のOSでは起動させないゲームの仕組みを教えてください
一部の海外のゲームでは、デバッグ環境がない等の理由により、起動時にOSの使用言語を調べて、製作環境と異なる言語のOS上では起動させないケースがあります。このようなケースでは、OSの使用言語をGetSystemDefaultLangID関数を用いて言語IDで判断することが多いようです。ただし、この手法ではコントロールパネルで「地域と言語のオプション」を変更した場合に正確な使用言語が判別できないため、「user.exe 」等のシステムファイルのバージョン情報にある使用言語をGetFileVersionInfo関数とVerQueryValue関数で取得するケースもあります。 なお、かつてMicrosoftは上記「user.exe」のバージョン情報による言語ID取得を例示していました。しかし、Windows Vista以降では、Windowsの日本語版であっても「USER.EXE」の使用言語は「英語(米国)」になっていますので、これからは「USER32.DLL」といった別のシステムファイルがチェックされると考えられます。
他にはレジストリのロケール情報を取得してOSの使用言語を判断する方法もありますが、正確に言語IDが取得できない場合があるため、この手法を用いることはほとんどないようです。
<参考:言語ID例>
0x0000 Language Neutral
0x0404 Chinese (Taiwan)
0x0804 Chinese (PRC)
0x0409 English (United States)
0x0809 English (United Kingdom)
0x040c French (Standard)
0x0407 German (Standard)
0x0411 Japanese
0x0412 Korean
0x0419 Russian
PCゲームに限らず、一部のマルウェアにおいても、英語といった特定言語環境でのみマルウェアとして正常動作するものがあり、それらは上記のような言語環境判別手法を使用したり、あるいはプロセスメモリ上にあるTEB構造体のメンバ「CurrentLocale」を参照していると考えられます。
PCゲーム解析関連情報の管理はどのようにするべきですか
デバッガ等を用いたPCゲームの解析に伴い得られた、特許取得済み商用プロテクトに関わる情報や、PCゲーム解析の参考資料として入手したウイルスのソースコードなどは、当然ながら外部に公開すべきではありません。このような情報を公開せず、参考資料として自分のパソコンに保存しておくことは問題ないと思われます。ウイルスのソースコードは、不正指令電磁的記録取得・保管罪の定義において「不正な指令を記述した電磁的記録その他の記録」に該当するものの、学術的研究を目的としてウイルスのソースコードを取得し、自分のパソコンにのみ保管することは、同罪の成立要件を満たしていないため支障はないといえます。この場合、ソースコードを保管するパソコンには、同ソースコードに対応するコンパイラまたはアセンブラはインストールしないのが賢明です。
参考:いわゆるコンピュータ・ウイルスに関する罪について(法務省の見解)
ただし、このような情報の管理にあたっては、パソコンの盗難等による、予期せぬパソコン内の保存データの流出に対し、セキュリティ上の対策を講じておくべきでしょう。
具体的には、PCゲーム解析に伴い得られた情報全ての暗号化が挙げられます。昨今では色々なファイル/フォルダ暗号化ソフトが公開されていますが、利便性の問題から、ファイル操作時の暗号化と復号化を自動で行う暗号化仮想ドライブ構築ソフトがお勧めです。
私の場合はフリーウェアの『TrueCrypt』(日本語ランゲージパックあり)を使用しています。AES、Serpent、Twofish等の暗号化アルゴリズムに対応しており、ユーザーが設定する暗号化仮想ドライブマウント用パスワードがよほど短くない限り、このソフトで情報漏洩は防止できると考えられます。ただし、暗号化仮想ドライブマウント中のトロイ等による情報漏洩まではこのソフトで対処できる訳ではないので、過信は禁物です。また、『TrueCrypt』はすでに開発が終了し、かつ脆弱性が発見されているため、互換性がある『VeraCrypt』あるいは類似ソフトの使用も検討した方が良いと思われます。
なお、パソコンの盗難時等にファイル復元ソフトを使用されるケースに対処するため、『TrueCrypt』等の使用に加えて『Eraser』などのファイル完全削除ソフトの併用をお勧めします。
余談ですが、私の失敗経験からいえば、情報を収集しプログラム解析を学ぶより先に、情報を取捨選択し、重要度といった自分なりの指標を元に、情報を整理する技術を学ぶべきです。少なくとも、収集した膨大な資料などの情報の中から、必要な情報をすぐに取り出せるようにする工夫は必須だと思います。
ゲーム実行PCの固有情報を鍵として暗号化されるセーブデータを別のPCで使用したい
これには色々なパターンがあり一概に対処法は述べられません。まず、暗号化と復号化の仕組みを解析して理解するところから始めると良いと思われます。アプローチとしては、 セーブデータ書き込み時の暗号化や読み込み時の復号化のためにどのようなデータ(鍵)を参照しているか。そのデータはすでにセーブデータあるいは別のファイルやレジストリに保存されているものか、それともセーブデータ読み書き時に動的に生成しているものか。どのような要因でセーブデータ読み込みエラーを出すように設計されているか。コードを書き換えて暗号化と復号化処理をスキップさせることは可能か。などを解析していくと解決の糸口がつかめることもあります。
ただし、復号化に必要なデータを、セーブデータ読み込み時にPC固有情報を元に動的に生成している場合など、別のPC上だけでの対処が困難なことも少なくありません。この場合は、元のPC上と別のPC上でゲームのインストール時/初回起動時/セーブデータ読み書き時などに生成される暗号化・復号化用データを同じものにさせる必要があります。
具体的には、ゲーム側が暗号化・復号化用データを生成するために、Windowsのユーザー名や各種ハードウェア情報といった、PC固有の情報を取得するコードを、そのまま書き換えたり、ラッパーDLLやAPIフックで改変して環境に関わらず同じ情報を取得させ、どのPC上でも同じ暗号化・復号化用データを生成させることで対処しますが、「PC固有の情報」が広範に渡るだけに必ずしも対処が容易とはいえません。
なお、 ラッパーDLLやAPIフックによる、API関数呼び出し時に特定の動作を行わせたり特定の値を返させたりする対処法は、上記のケース以外にも色々応用可能ですので、有効な1手法として覚えておくことをお勧めします。
アプリケーションの処理実行速度を変更するソフトウェアの仕組みを教えて下さい
『Speed Hack Tool』などとも呼ばれるこの種のソフトウェアが、アプリケーションの処理実行速度を変更する基本的な仕組みは以下と推測されます。あくまで推測であり、必ずしも以下の方法を使用しているとは限らないことに留意願います。『うさみみハリケーン』が実装している「実行速度調整」機能では、以下のようなIATの書き換えではなく、API関数のエントリーを逆アセンブラで解析してAPIフックを行います。
- 自作のアプリケーションから、対象プロセスをCreateProcess関数のProcess Creation Flags->CREATE_SUSPENDEDを使って、エントリーポイント実行前にメインスレッドを停止させた状態で起動します。ただし、この状態でのプロセスメモリ読み書き以外のプロセス操作は、メインスレッド再開後にアクセス違反による対象プロセスの強制終了を招くことがあります。これに対処するためには、まず停止状態で起動後すぐにエントリーポイントから2バイトを自分自身にジャンプするニーモニック"EBFE"に書き換えてからResumeThread関数でメインスレッドを再開させます。次に、エントリーポイントで無限ループに陥らせた状態のままSuspendThread関数でメインスレッドを再度停止させ、さらにエントリーポイントから2バイトをオリジナルのバイナリデータで書き戻し、FlushInstructionCache関数を実行します。これにより、メインスレッドをエントリーポイントで停止させることが可能になります。また、単純に起動後すぐに全スレッドをSuspendThreadで停止させることもできます。なお、対象プロセスのエントリーポイントのアドレスは、あらかじめ対象プロセスの実行ファイルのPEヘッダを参照することで特定しておきます。
- CreateRemoteThread関数とVirtualAllocEx関数・WriteProcessMemory関数を使って、対象プロセスが実行する処理としてLoadLibrary関数を呼び出し、対象プロセス内に自作DLLをロードさせます。この自作DLLロード(DllMain関数内でfdwReason==DLL_PROCESS_ATTACH)をトリガーとして何らかの処理を自作DLL側に行わせることも可能です。さらに、処理実行速度変更に関する設定内容をプロセスメモリ上の自作DLLモジュールエリア内か、メモリマップトファイルといった共有メモリなどに書き込みます。
- 対象プロセスのメインモジュールのモジュールエリアで、PEヘッダの情報からIAT(インポートアドレステーブル)のアドレスを特定します。それからIATを解析し、IAT内に設定されているSetTimer関数、QueryPerformanceCounter関数、(必要に応じて)timeGetTime関数、GetTickCount関数などの開始アドレスを、ロード済み自作DLL内のダミー関数の開始アドレスに書き換えて、SetTimer関数などのAPI関数呼び出し処理が必ずダミー関数を経由するようにします。IAT解析時は、パッカー使用など対象プロセスのIATが異常な場合の対処 (IAT再構築技術を参考にした独自のIATサーチなど) が必須となります。
- ResumeThread関数を使って、停止している対象プロセスの全スレッドの実行を再開させます。すでに自作DLLを対象プロセス内にロードさせているため、この時点で自分自身のプロセスを終了させることも可能です。
- 以降はダミー関数の処理により、SetTimer関数などのAPI関数呼び出し時の引数や戻り値を変更することで、それらを元に対象アプリケーションが設定する各種処理の実行速度も変更されます。ダミー関数ではQueryPerformanceCounter関数などが最初に呼ばれた際の戻り値を保存しておき、次に呼ばれた際には、最初の戻り値と新しい戻り値との差を元に、その差を定数倍などして修正した値を戻り値として対象アプリケーション側に渡します。
上記アプリケーションの処理実行速度変更を既に起動済みのプロセスに対しても行えるようにするには、WM_TIMERメッセージへの対応などから、GetMessage関数・DispatchMessage関数・PeekMessage関数等のメッセージループ関連API関数もダミー関数で処理させる必要があると考えられます。また、上記の方法はWindowsNT系OSでのみ可能であり、これをWindows9x系OSにも対応させるにはさらに複雑な操作が必要です。 上記の方法では、対象プロセスがLoadlibrary関数かGetModuleHandle関数呼び出し後にGetProcAddress関数を用いて取得した関数アドレスを使用するケースには対応していませんが、アプリケーションの処理実行速度設定に関連するAPI関数に関してそのような処理を行うことはまずありません。
なお、このIATを直接書き換える方法は、対象プロセスの特定API関数呼び出しに併せて自作コードを実行させる方法として有用です。特に、コードセクションには一切変更(改ざん)を加えていない点に注目して下さい。また、対象プロセスへの自作DLLロード及び自作コードの実行については、以下のページが参考になります。
別のプロセスにコードを割り込ませる3つの方法
プロセスメモリ上にロードされたモジュールにおける、PEヘッダやセクション及びIATなどが、API関数呼び出しなどプロセスの処理実行に関してどのような役割を果たしているかを理解することは、プログラム解析において重要な要素の一つといえます。この理解を深めるために、プロセスメモリ上の各モジュールのPEヘッダ情報及びインポート・エクスポート関数一覧を表示するソフトウェアの自作や、公開されているパッカーやアンパッカーのソースコードの読解をお勧めします。
上記の処理実行速度変更は基本的にAPIフックで実現可能ですが、API関数の内部処理を自前でエミュレートして、システムが保持する時間関連情報を直接参照するケースには対応できません。
Windows Vista以降では、アドレス0x7FFE0000以降のKUSER_SHARED_DATA構造体先頭と+320hに時間関連の値が格納されています。この値は『うさみみハリケーン』を使って確認することができます。このメモリエリアは、ユーザーモードからも参照可能なカーネルモード用のメモリエリアといえるものであり、この時間関連の値はユーザーモードのプロセスから書き換えることはできません。
//0x7FFE0000以降 ULONG TickCountLowDeprecated; ULONG TickCountMultiplier; KSYSTEM_TIME InterruptTime; KSYSTEM_TIME SystemTime; KSYSTEM_TIME TimeZoneBias; //中略 //0x7FFE0320 union { KSYSTEM_TIME TickCount; UINT64 TickCountQuad; }; //以下略 //KSYSTEM_TIME構造体の定義 typedef struct _KSYSTEM_TIME { ULONG LowPart; LONG High1Time; LONG High2Time; } KSYSTEM_TIME, *PKSYSTEM_TIME;以下の時間関連API関数は内部処理でKUSER_SHARED_DATA構造体を参照しています。
GetTickCount
GetSystemTime
GetSystemTimeAsFileTime
GetLocalTime
解析対象アプリケーションが、GetTickCount関数等の代わりに同関数の内部処理をエミュレーションして、上記の固定アドレスを使って経過時間取得を行っているならば、同処理箇所にパラサイトルーチンを打ち込んで取得する値を操作あるいは、本来呼び出されるAPI関数に繋げるというアプローチが想定されます。ドライバを使用してカーネルモードでのカーネルオブジェクト操作で対処するというアプローチもあります。
ゲーム上でランダムに設定されるデータ(出現アイテムの種類など)を固定化したい
プログラムではこの様なランダムにデータを設定する場合、擬似乱数(あらかじめ用意された演算等のアルゴリズムにより生成した乱数)を使用します。C言語では標準ライブラリ関数として擬似乱数を生成するrand関数がありますが、生成した擬似乱数の重複など精度面ほかで問題があるため、近年では「Mersenne Twister」等の優れた疑似乱数生成アルゴリズムが奨められています。なお、一般的にrand関数の処理は、線形合同法と呼ばれる単純な計算式で行われています。以下はその計算例であり、定数0x343FDなどは定型的に使用されます。もし、解析対象アプリケーションがMSVCR90.dllなどmsvcr**.dllという、MicrosoftのCランタイムライブラリを使用しているならば、同DLLが提供し、以下の計算処理を行うrand関数が呼び出される可能性があります。一方、rand関数の改良版といえる新しいrand_s関数は、内部処理で後述のSystemFunction036関数を使用します。
MOV ECX, [EAX+14] IMUL ECX, ECX,343FD ADD ECX, 269EC3 MOV [EAX+14], ECX MOV EAX, ECX SHR EAX, 10 AND EAX, 7FFF RETNまた、API関数を用いた擬似乱数の生成には、Advapi32.dllが提供するCryptGenRandom関数やSystemFunction036関数(「RtlGenRandom関数」と表記されることあり)が使用可能です。
基本的に擬似乱数は「種」と呼ばれる任意の数値に特定の処理を施して生成します。そのため、たいていの擬似乱数生成アルゴリズムにおいて、プログラム側が設定する種の数値さえ固定化できれば、生成される擬似乱数は常に同じ値になります。
PCゲームにおける擬似乱数を生成するための種の設定には、特定のAPI関数によって得られた、常に変化し種として適切な「現在時刻」や「システムの実行経過時間に連動する値」が使用されるケースが少なくありません。用いられる可能性があるAPI関数のリストは下記。なお、これらのAPI関数の戻り値は、各種処理におけるタイミングを調整するための処理時間制御に用いられることもありますが、種を取得する場合は戻り値を他の数値と大小比較する必要が無い点に注目してください。また、一回の擬似乱数生成で複数の擬似乱数からなる配列を作成するアルゴリズムならば、擬似乱数が必要となる毎に種を取得する必要がありません。
timeGetTime
GetTickCount
GetTickCount64(Vistaで実装された)
QueryPerformanceCounter
NtQueryPerformanceCounter(ネイティブAPI)
GetSystemTimeAsFileTime
NtQuerySystemTime(ネイティブAPI)
GetSystemTimes(XP SP1で実装された)
timeGetSystemTime(timeGetTimeよりオーバーヘッド大)
GetProcessTimes(プロセスの実行時間を取得、9x/Me不可)
アプローチとしては、これらのAPI関数を用いて種を取得・変更する処理を無効化することになります。もしも生成された擬似乱数とその前に生成した擬似乱数の重複をチェックし、重複時には擬似乱数を再生成する処理があるならば、必ずその処理も無効化しないと無限ループに陥ります。
他のアプローチとしては、サブルーチンとなっている擬似乱数生成処理そのものの無効化や、生成した擬似乱数を任意の数値範囲内にするための演算処理箇所を書き換えて、生成した擬似乱数を固定値に置き換える手法も考えられます。
参考までに、もしMersenne Twister(普及しているMT19937バージョンの32ビットマシン用)を用いて擬似乱数を生成している場合は、逆アセンブルコードリスト上に同アルゴリズムが使用する定数0x9908B0DFや調律用定数0x9D2C5680および0xEFC60000が見つかります(プログラマの意図的操作がなければ)。
種の設定に関連して、現在時刻の取得には以下のAPI関数も使用可能です。ただし、これらのAPI関数は取得する時刻情報の形式が暦型であるため、種の設定よりは、セーブデータに保存時刻情報を書き込むケースや、定期型CD/DVDチェックおよび、イベント出現等でプレイする月/日/時/分/秒/曜日などをトリガーとする場合の条件判定などに用いられる可能性が高いといえます。
GetLocalTime
GetSystemTime
なお、Visual Studio 2008 付属のソースによれば、C言語の標準ライブラリ関数のうち、プログラム実行開始からの消費したプロセッサ時間を取得するclock関数や、現在時刻を取得するtime(_time32/_time64)関数は、内部処理でGetSystemTimeAsFileTime関数を使用しています。ランタイムライブラリや、ソフトウェア同梱DLLを経由して時刻情報を取得可能なことに留意してください。
さらに、インラインアセンブラを用いてRDTSC命令(オペコード:0F31)を実行することで、システムの実行経過時間に連動するプロセッサのタイムスタンプ・カウンタを取得して種とすることも可能です。インテルPentiumで実装されたこの命令を実行すると、EDX:EAXの形式で64ビットのタイムスタンプ・カウンタが同レジスタにロードされます。
「倍直」とは何ですか
「倍直」はクラッキング関連用語です。意味としては、「シェアウェアの実行可能ファイルをバイナリエディタでオープンすることで、ユーザー登録用シリアルコードが直接視認可能なケース」です。倍直のケースが生じる原因は、ソースコード上でのプログラムの処理において、シリアルコードの固定文字列をそのまま参照することです。これにより、コンパイラが生成する実行可能ファイルのデータセクションには、プログラムが使用するデータとして、その固定文字列が最初から用意されることになります。もし、シリアルコード文字列をプログラムの処理として動的に生成するならば、倍直にはなりえません。
近年では、このようなクラッキング手法も広く知られており、プログラミング初心者が製作したシェアウェアであっても、たいていは倍直対策を施します。現在では、解析対策に注力しない一部のシェアウェアを除けば、倍直はまれなケースだといえます。ちなみに、1990年代半ばでは、あるPC誌の付属CDに収録されたシェアウェアの約3割が、倍直ということもありました。
倍直に関して、たとえシェアウェアの不正使用が目的でなくとも、安易にシリアルコードと推測される文字列でユーザー登録を試さないでください。過去に実在したケースとして、あるシェアウェアでは偽のシリアルコードを倍直になるよう用意しておき、そのシリアルコードでユーザー登録を行うと、システム破壊を試みるロジックボムを発動させました。システム破壊とまではいかなくとも、個人情報が送信されるといった可能性は考慮すべきでしょう。
ゲームなどで実行時に作成される「一時ファイル」を残したい
アプリケーションによっては、実行時の任意のタイミングで「一時ファイル(テンポラリファイル)」を作成し、プロセス終了時までに同ファイルを削除するケースがあります。ゲームが作成するこのような一時ファイルには、暗号化されていたデータを復号したものといった、解析の手がかりになりえるデータが格納されることもあります。このような、プログラムで使用する一時ファイルの作成には、CreateFile関数が使用可能です。第6引数で、一時ファイル作成に関連するフラグを論理和で指定します。
●C言語でのCreateFile関数使用例 CreateFile(FileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE,//一時ファイル指定 NULL ); ●逆アセンブルコードリスト例 PUSH 0 PUSH 4000100 PUSH 2 PUSH 0 PUSH 0 PUSH C0000000 PUSH EAX CALL DWORD PTR DS:[<&KERNEL32.CreateFileA>] 0x04000100 = FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSEこの場合は、第6引数を定数FILE_ATTRIBUTE_NORMAL(0x00000080)に書き換えることで対処可能です。なお、一時ファイルの作成場所は、必ずしもゲーム等アプリケーションのインストール先フォルダとは限りません。GetTempPath関数を使用して、一時ファイル専用のフォルダに格納するケースも想定されますので注意が必要です。
さらに、CreateFile関数の引数指定ではなく、DeleteFile関数を使用して対象ファイルを削除している可能性も考慮してください。
もし、一時ファイルをOS再起動時に削除するならば、MoveFileEx関数が使用されます。この関数の第2引数がゼロ(NULL)で、第3引数が定数MOVEFILE_DELAY_UNTIL_REBOOT(0x00000004)ならば、第1引数でパスを指定されたファイルや空フォルダがOS再起動時に削除されます。この場合は、関数呼び出しを無効化することで一時ファイルの削除を回避可能です。
ASLRに影響されないEXEファイル改造を行いたい
Windows Vista以降では、EXEファイルやDLLファイルといったモジュールがプロセスメモリ上に読み込まれるアドレスをランダム化する、セキュリティ上の脆弱性を緩和する機能ASLR(Address Space Layout Randomization)が実装されています。参考:Windows Vista カーネルの内部 : 第 3 部
Microsoftはかつて、このASLRやDEPをシステムレベルあるいはアプリケーションレベルで適用可能な脆弱性緩和ツール『EMET』(Enhanced Mitigation Experience Toolkit)を配布していました。EMETを使用することで、解析対象アプリケーションや解析ツールなどを指定して、ASLR、DEPその他のセキュリティ対策適用時の挙動を、簡単に確認することが可能でした。
本来、ASLRが適用されるEXEモジュールは、ASLR対応のコンパイラでコンパイルされ、PEヘッダ内の「DllCharacteristics」で「IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(以下DYNAMIC_BASE)」フラグがオンになっているモジュールに限られます。これに対し、EMETが持つASLR強制適用機能は、このフラグに左右されないでASLRを適用できるということになっていました。しかし実際には、この機能はプラグインのようなDLLファイルを主な対象としており、すべての実行ファイルにASLRを強制適用させるものではありません。Windows 7+EMET2.1で試した限りでは、PEエディタで「DYNAMIC_BASE」フラグをオフにしたASLR対応EXEファイルには、EMETを用いて実行ファイル指定で個別にASLR強制適用の設定をしても、ASLRは有効になりませんでした。
つまり、既存のASLR対応EXEファイルに、イメージベースが0x00400000になるという前提でのプログラムコードを埋め込んだり、そのようなパッカーを用いたいケースや、固定アドレスを指定してメインモジュールのプロセスメモリを書き換えたいケースでは、「DYNAMIC_BASE」フラグをオフにすれば、たとえEMET互換機能が使用されている環境でも問題なく対処できるということです。加えて、PEヘッダ内の「Characteristics」にある「IMAGE_FILE_RELOCS_STRIPPED」フラグをオンにしておくことをお勧めします。このようなPEヘッダ内のフラグの変更は、『うさみみハリケーン』に同梱しているPEエディタ『UMPE』を用いれば簡単に行うことができます。
なお、特定プロセスではなく、全てのプロセスでASLRを無効化したい場合は、以下のレジストリ操作を行うことで対処できます。
「HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\MoveImages」に値0を設定。
ちなみに、メインモジュールの先頭アドレスは、GetModuleHandle関数(引数NULL)の戻り値か、PEBの先頭アドレス+8hにある「ImageBaseAddress」の値で動的に取得可能です。
Windows 10といった新しいWindowsOSでは、プロセス単位のASLRやDEPおよびCFGなどの制約設定は、プロセスの「ポリシー」として指定されます。うさみみハリケーンは、このポリシーの状況表示に対応しています。メニューの「ファイル」→「プロセスの各種情報を表示」、あるいは起動時に表示されるプロセスリスト上で右クリックによるポップアップメニューから「プロセスの各種情報を表示」で、「実行状況」カテゴリを選択します。
プログラムがユーザーの入力を検出する仕組みを知りたい
●基本事項下記の解説を読む前に、プログラム解析の基本事項であるAPI関数、仮想キーコードおよび、プログラムがWindowメッセージを処理する仕組みを理解しておくことをお勧めします。なお、Microsoft社員による技術解説や、同社の公式解説書には、「Windowメッセージ」と「Windowsメッセージ」の両表記が混在しています。
主要仮想キーコードの一覧と、DirectInput用スキャンコードの一覧は、うさみみハリケーンのヘルプ内「基礎用語 仮想キーコード」に掲載していますので、参照されることをお勧めします。
プログラムの入力検出処理の解析は、プログラム上で固定化された入力検出対象(特定キー等)や入力時挙動の変更に有用です。また、特にゲームでの、開発者用デバッグモードや隠し要素等の出現条件探索にも役立ちます。
入力検出処理の該当箇所とおぼしき箇所が見つかったら、該当するコードをNOP命令で潰したり、条件ジャンプ命令を書き換えてから、キー・マウス入力に対する反応を確認します。これにより、対象となる処理箇所を特定していきます。
以下では逆アセンブルコードリスト上での、入力検出処理の解析について述べていますが、同処理の操作が目的ならば、APIフックやDirectInputのフックというアプローチが工数面でより有用なケースもあります。
●API関数
GetAsyncKeyState関数やGetKeyState関数により、特定キー(マウスボタンを含む)を仮想キーコードで指定して、そのキーの押し下げ状態を取得することが可能です。GetKeyboardState関数では、仮想キーコードで指定可能な256キー全ての押し下げ(上げ)状態を一度に取得します。全ての仮想キーコードに対してループ処理でGetAsyncKeyState関数を呼び出すケースもあります。
●Windowメッセージ
ウィンドウ作成時にRegisterClassEx関数で指定したウィンドウプロシージャ(Windowメッセージを処理するための、関数のようなもの)内の処理として、Windowメッセージを受け取ることでキー・マウス入力を検出します。ウィンドウプロシージャには、その引数として、メッセージの種類(定義値)および、そのメッセージに応じたパラメータが渡されます。
ウィンドウプロシージャのアドレスは、うさみみハリケーンで対象プロセスを選択後に、メニューで「ファイル」→「プロセスの各種情報を表示」でカテゴリの[ウィンドウ]を選択すると、項目[クラスプロシージャ]として表示されます。この方法でアドレスが取得できない場合は、逆アセンブルコードリスト上で、RegisterClassEx関数の呼び出し処理を観察あるいは、通常ウィンドウプロシージャの最後に配置されるDefWindowProc関数呼び出し箇所から辿るというアプローチを使用してください。
関連して、SetWindowsHookEx関数により、ウィンドウプロシージャとは別に、キー・マウス入力のWindowメッセージをフックするプロシージャを指定可能です。この場合、第1引数はキー入力のフックならばWH_KEYBOARD(定数2)かWH_KEYBOARD_LL(定数13)、マウス入力のフックならば同様にWH_MOUSE(定数7)かWH_MOUSE_LL(定数14)となります。さらに第2引数でフックプロシージャのアドレスが指定されます。なお、自分以外のアプリケーションもフック対象とするグローバルフックの場合、フックプロシージャは、各プロセスのプロセスメモリ上にロードされたフック用DLL内で実行されます。
基本的に、WM_KEYDOWN(定数0x0100)メッセージの受信時に、同メッセージに付随するパラメータ(この場合は仮想キーコード)を読み込むことで、入力されたキーを認識可能です。ただし、PRINT SCRNキーはWM_KEYUP(定数0x0101)メッセージ、F10キーや、ALTキーと他キーの同時押しはWM_SYSKEYDOWN(定数0x0104)メッセージの受信により認識可能です。
ウィンドウプロシージャ内では、他のメッセージを受信した場合と処理を切り分けるため、引数であるメッセージの値と、上記メッセージの定義値との単純比較を行うことで、上記メッセージ受信と判断し、それに応じた処理を行います。なお、コンパイラのコード最適化に伴い、必ずしも定義値そのものが逆アセンブルコードリスト上に現れるとは限らないことに注意が必要です。
WM_CHAR(定数0x0102)メッセージは、WM_KEYDOWNメッセージを加工して送信したものであり、このメッセージ受信時には、文字入力に使用されたキーの認識が可能です。たとえば、このメッセージ受信時には、「A」なら0x41、「a」なら0x61といった文字コードを取得できます。
ウィンドウ内でのマウスの入力は、WM_LBUTTONDOWN(左クリック、定数0x0201)、WM_RBUTTONDOWN(定数0x0204)および、WM_MBUTTONDOWN(定数0x0207)という、専用のWindowメッセージの受信によって入力を検出可能です。また、カーソルの移動はWM_MOUSEMOVEメッセージ(定数0x200)の受信により検出できます。
●IME(Input Method Editor)
ユーザーによるIME使用時には、生成されるWM_IME_COMPOSITIONメッセージ(定数0x010F)の受信時にImmGetCompositionString関数を実行することで、ひらがなや漢字といった変換結果の文字列を取得します。
参考までに、日本製のプロセスメモリエディタやバイナリエディタで一般的に使用されるユーザーインターフェースでは、上記の変換結果文字列取得時や、WM_IME_STARTCOMPOSITIONメッセージ(定数0x010D)の受信時に、ImmSetCompositionWindow関数で文字変換用のウィンドウをユーザー入力位置に配置します。併せて、ImmSetCompositionFont関数を使って文字変換用ウィンドウでの文字表示サイズ等を適切に調整し、文字入力におけるユーザビリティを高めています。
なお、Windowsは、既存の日本語入力システム (IME) を、高機能な「Text Services Framework」へ置き換えつつあります。この移行に伴い、従来のIMEで使われるAPI(IMM32)はエミュレートすることで、互換性を維持しています。
●DirectInput/XInput
DirectXの一部機能であるDirectInputのAPIを用いることで、キー・マウス入力や、ジョイパッド等ゲーム用デバイスでの入力を検出可能です。一般的に、この入力検出手法はゲームや各種ゲーム機用エミュレータで使用されることが多いといえます。なお、現在では、キー・マウス入力の検出にDirectInputは推奨されていません。
DirectInputでのキー入力検出処理は、プログラマ向けの解説例として、以下のように独自の関数を呼び出し、その関数内で行うパターンが多く見受けられます。以下の例では、GetInput関数呼び出しの前後にある定型的なAPI関数呼び出しが、この独自の関数を見つける手がかりとなり得ます。
//C++ソースコード記述例 //Windowメッセージ関連の処理を行っている while ( TRUE ) { if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) { if (!GetMessage(&msg, NULL, 0, 0)) break; TranslateMessage(&msg); DispatchMessage(&msg); } else { //ここでキー入力検出など、ゲームの基本処理を行う GetInput();//プログラム独自の関数(関数名は例) Sleep(1); } }上記例の他にも、SetTimer関数やtimeSetEvent関数を使用する方法で、定期的に入力検出を行うことが可能です。ただし、SetTimer関数呼び出しにより送信される、WM_TIMER(定数0x0113)メッセージは処理優先度が低く、送信間隔の時間的な精度も低くなるため、ゲームの重要な処理に使用されるケースは少ないと考えられます。
DirectInputによるキー、マウスおよびジョイパッドの入力検出処理には、DirectInputの内部的な関数(メソッド)であるGetDeviceState関数が使用可能です。逆アセンブルコードリスト上ではこの関数名は表示されないため、同関数名から目的の処理箇所を辿ることはできません。なお、キー入力検出時は、上記GetDeviceState関数呼び出し時の第2引数(入力情報を格納するバッファのサイズ)として通常0x100、マウスならばDirectInputのバージョンにより0x10または0x14、ジョイパッドならば取得する入力情報の種類により0x50または0x0110が指定されます。また、取得されたキー、マウスおよびジョイパッドの入力情報から、押されているキーやボタンを認識するための処理には、基本的に定数0x80が使用されます。
;GetDeviceState関数呼び出し例(キー入力検出時) 00401BF2 LEA ECX,DWORD PTR SS:[EBP-108] 00401BF8 PUSH ECX 00401BF9 PUSH 100 00401BFE MOV EDX,DWORD PTR DS:[410F04] 00401C04 MOV EAX,DWORD PTR DS:[EDX] 00401C06 MOV ECX,DWORD PTR DS:[410F04] 00401C0C PUSH ECX 00401C0D MOV EDX,DWORD PTR DS:[EAX+24] 00401C10 CALL EDX同様に、DirectInputの内部的な関数であるGetDeviceData関数を使用するケースもあります。この場合は第2引数はDirectInputのバージョンにより0x10または0x14となります。
上記逆アセンブルコードリストで、CALL先を指定するために使われている数値0x24は、GetDeviceState関数のアドレス取得に使われる定数です。同様にGetDeviceData関数のアドレス取得ならば、定数0x28が使用されます。これらの定数はDirectInputのバージョンに依存しません。
ここで注意すべきは、DirectInputでは、仮想キーコードではなく、スキャンコード(入力デバイスであるキーボードが生成する独自の値)により入力されたキーの種類を識別することです。たとえば、上矢印キー(↑)の押し下げは、仮想キーコードVK_UPの定義値(0x26)ではなく、DirectInputでの定義値DIK_UP(0xC8)により識別されます。
なお、WINMM.dllが提供するjoyGetPos関数やjoyGetPosEx関数でもジョイパッドの入力を検出可能です。これらのAPI関数は、DirectInputの古いバージョンに相当するもので軽快に動作します。ただし、新しいバージョンのDirectInputとの比較では、ボタン、スライダーや複数POVなどで、取得可能な入力情報に若干の制約があります。
XInputは(事実上)Xbox 360コントローラー用の新しいAPIですが、ジョイパッドの入力検出において、DirectInputとの互換性に問題があります。たとえば、ジョイパッドの入力検出にXInputのみを使用するゲームでは、DirectInput対応のジョイパッドが使用できません。ただし、この問題は、DirectInput対応のジョイパッドでの入力を、『うさみみハリケーン』のプラグイン「Joy2Write」でキー入力に変換したり、あるいは、有志により公開されている、DirectInput対応のジョイパッドをXInputに対応させるツールを使用することで対処可能です。また、現在では、DirectInput/XInputの両方に対応したジョイパッドも発売されています。
XInputを使用しているゲーム等が実行する、ジョイパッドでの入力を検出する処理の解析は、xinput1_3.dllや別バージョンDLLがエクスポートするXInputGetState関数の呼び出し箇所が手がかりとなります。なお、XInputの場合、ボタン押し下げの検出は、各ボタンに設定されている定数との論理積で行われる可能性が高いといえます。
//XInputでのボタンの定義値 XINPUT_GAMEPAD_DPAD_UP 0x0001 XINPUT_GAMEPAD_DPAD_DOWN 0x0002 XINPUT_GAMEPAD_DPAD_LEFT 0x0004 XINPUT_GAMEPAD_DPAD_RIGHT 0x0008 XINPUT_GAMEPAD_START 0x0010 XINPUT_GAMEPAD_BACK 0x0020 XINPUT_GAMEPAD_LEFT_THUMB 0x0040 XINPUT_GAMEPAD_RIGHT_THUMB 0x0080 XINPUT_GAMEPAD_LEFT_SHOULDER 0x0100 XINPUT_GAMEPAD_RIGHT_SHOULDER 0x0200 XINPUT_GAMEPAD_A 0x1000 XINPUT_GAMEPAD_B 0x2000 XINPUT_GAMEPAD_X 0x4000 XINPUT_GAMEPAD_Y 0x8000
●Raw Input
Raw Input APIにより、キーボード、マウス、ジョイパッドおよび他の入力デバイスから、入力情報をそのまま取得することができます。このAPIの特徴として、入力を行ったデバイスを特定できるという点が挙げられます。つまり、Raw Input APIを使用しているならば、たとえば複数のキーボードや複数のマウスがPCに接続されていても、キーボードやマウスのうちの一つを特定して入力を検出可能です。
基本的な仕組みとしては、まずRegisterRawInputDevices関数で入力情報を取得したいデバイスのタイプを登録します。これにより、登録したデバイスでの入力時には、同関数の第1引数(RAWINPUTDEVICE構造体内)で指定したウィンドウに、WM_INPUT(定数0x00FF)メッセージが送信されます。このメッセージ受信に合わせて、GetRawInputData関数あるいはGetRawInputBuffer関数を呼び出すことで入力情報を読み取ります。RegisterRawInputDevices関数では、RAWINPUTDEVICE構造体の配列を指定することで、複数のデバイスのタイプを一度に登録可能です。
//RAWINPUTDEVICE構造体の定義 typedef struct tagRAWINPUTDEVICE { USHORT usUsagePage; USHORT usUsage; DWORD dwFlags; HWND hwndTarget; //WM_INPUTメッセージ送信先 } RAWINPUTDEVICE, *PRAWINPUTDEVICE, *LPRAWINPUTDEVICE;RegisterRawInputDevices関数によるデバイスの登録は、RAWINPUTDEVICE構造体のメンバusUsageに以下の定義値を指定することで行います。以下はメンバusUsagePageがHID_USAGE_PAGE_GENERIC(定数1)の場合です。
//主要デバイスのタイプ
ポインタ:1
マウス:2
ジョイスティック:4
ゲームパッド:5
キーボード:6
キーパッド:7
WM_INPUTメッセージの受信時にはそのパラメータとして、入力情報が格納されたRAWINPUT構造体のハンドルを受け取ります。このハンドルを指定してGetRawInputData関数を呼び出し、RAWINPUT構造体の実データを取得します。
//RAWINPUT構造体の定義 typedef struct tagRAWINPUT { RAWINPUTHEADER header; union { //共用体:下記の構造体のいずれかが配置される RAWMOUSE mouse; RAWKEYBOARD keyboard; RAWHID hid; } data; } RAWINPUT, *PRAWINPUT; *LPRAWINPUT; //上記RAWINPUTHEADER構造体の定義 typedef struct tagRAWINPUTHEADER { DWORD dwType; //デバイスのタイプ(マウス(定数0)、キーボード(1)、その他(2)) DWORD dwSize; HANDLE hDevice; //デバイスのハンドル(デバイスの特定に使用可) WPARAM wParam; } RAWINPUTHEADER, *PRAWINPUTHEADER; //上記RAWKEYBOARD構造体の定義 typedef struct tagRAWKEYBOARD { USHORT MakeCode; USHORT Flags; USHORT Reserved; USHORT VKey; //入力されたキーの仮想キーコード、RAWINPUT構造体先頭から+16h UINT Message; ULONG ExtraInformation; } RAWKEYBOARD, *PRAWKEYBOARD, *LPRAWKEYBOARD; マウスボタンの入力を検出する場合は、以下の定義値との比較が行われます。 RI_MOUSE_LEFT_BUTTON_DOWN(0x0001) RI_MOUSE_LEFT_BUTTON_UP(0x0002) RI_MOUSE_RIGHT_BUTTON_DOWN(0x0004) RI_MOUSE_RIGHT_BUTTON_UP(0x0008) RI_MOUSE_MIDDLE_BUTTON_DOWN(0x0010) RI_MOUSE_MIDDLE_BUTTON_UP(0x0020)ジョイパッドの入力を検出する場合は、OS付属のHID.DLLが提供するHidP_GetUsages関数なども必要となり、その実装は工数がかさみます。そのため、DirectInputを用いず、あえてRaw Inputでジョイパッドの入力を検出する可能性は低いと考えられます。
●Windows タッチ(環境不備により動作未確認)
Windows 7以降は、タッチパネル等のタッチ・インターフェイス使用時に、ユーザー入力検出を行う独自の仕組み「Windows タッチ」に対応しています。
基本的には、ユーザーがズーム・パン・回転などに対応するジェスチャ操作を行うと、WM_GESTUREメッセージ(定数0x119)が自分のウィンドウに送信されます。このメッセージの受信時にGetGestureInfo関数を呼び出すと、同関数の第2引数で指定したGESTUREINFO構造体に、入力されたジェスチャの情報が格納されます。この際、同構造体の先頭+8hにはジェスチャの種類を示す以下の定数が格納されます。
//ジャスチャの種類 ジェスチャ開始:1 ジェスチャ終了:2 ズーム:3 パン:4 回転:5 マルチ タップ:6 プレス アンド タップ:7接触点の座標といった、ジェスチャではないユーザー入力情報を取得する場合は、RegisterTouchWindow関数で自分のウィンドウを登録し、WM_GESTUREメッセージではなくWM_TOUCHメッセージ(定数0x240)が送信されるようにします。WM_TOUCHメッセージ受信時にGetTouchInputInfo関数を呼び出すと、同関数の第3引数で指定したTOUCHINPUT構造体(配列で複数指定可)に、接触点の座標などの情報が格納されます。この際、同構造体の先頭にはタッチ入力のX座標、+4hにはY座標が格納されます。この座標値を100で割ればピクセルの値になります。また、同構造体先頭+10hには、接触点の移動(定数0x0001)・作成(定数0x0002)・削除(定数0x0004)といった操作を示す、組み合わせ可能なフラグが格納されます。
●補足
上記では、API関数やWindowメッセージを用いた基本的な入力検出処理について述べています。実際のゲームやエミュレータにおいては、入力検出処理を汎用のライブラリに任せるケースもあります。その例としては、入力検出処理等の実行に、クロスプラットフォームの「SDL」(Simple DirectMedia Layer) を使用するケースが挙げられます。
各種USBデバイスといった、HID(Human Input Devices)のプログラムでの操作には、上記HID.DLLが提供するAPI関数が使用可能です。ただし、キーボードやマウスといった一般的なデバイスは、上記のように入力を検出する仕組みが用意されています。そのため、このAPI関数はたいてい、特殊なデバイスの操作に用いられます。
上記は入力を検出する方法について述べています。これとは逆に、プログラムの処理として、キー、マウスおよびその他のデバイスでの入力を実現するには、基本的にSendInput関数を使用します。また、タッチ入力のシミュレートには、専用のInjectTouchInput関数が用意されています。
64ビットアプリケーションの解析について知りたい
(執筆:2012年)時勢として、OSおよび各種ソフトウェアは64ビット版が標準となりつつあるため、今から64ビットアプリケーションの解析について学ぶことは有用と考えられます。ただし、商業的な理由から、市販ソフトウェアやPCゲーム(アダルトゲームや同人ゲームを含む)ではいまだ32ビット版のみの提供も多いため、学んだことをすぐに活用できるとは限りません。
現状のプログラム解析は、64ビット版Windows OS上(WOW64)でも可能な、従来の32ビット版アプリケーション向けの解析に高い需要があります。そのため、個人製作による、フリーの64ビット版のプログラム解析ツールは、その需要の低さにより、現状ではごく少数しか公開されていません。しかし、今後のPCゲーム等の64ビット化の流れに沿って、徐々に増えていくものと予想されます。なお、汎用プロセスメモリエディタ兼デバッガ『うさみみハリケーン』には、逆アセンブラやデバッガ等を備えた64ビット版も同梱しています。
有償の64ビット版対応プログラム解析ツールとしては『IDA Pro』有償版があり、特に情報セキュリティ分野で64ビット版のマルウェアの解析等に使用されています。関連して、64ビット版アプリケーションの開発に対応している『Visual Studio』評価版などを用いて、自分でx64プラットフォームのプログラムを組んでデバッグを行えば、プログラム開発・解析のスキルアップに役立つと考えられます。
●プログラム解析という視点での、64ビット版のCPU・Windows OS・アプリケーションの、従来の32ビット版との比較や注意点は下記。
・CPUアーキテクチャの変更に伴い、既存の汎用レジスタは64ビットに拡張されている(EAX ⇒ RAXなど)。たとえばRAXレジスタ64ビットの下位32ビットがEAXレジスタに相当する。また、新たにR8~R15の64ビットレジスタが追加されている。これらの汎用レジスタは下位32/16/8ビットを指定して使用することも可能。なお、EFLAGSレジスタも64ビットに拡張されRFLAGSとなったが、上位32ビットは予約され使われないため、下位32ビットをEFLAGSレジスタとして使用する。
・汎用レジスタや命令等のCPUアーキテクチャ拡張に伴い、実行ファイルの逆アセンブルには、64ビット版実行ファイル対応の逆アセンブラが必要となる。
・実行ファイルのPEヘッダは、「ImageBase」やスタック・ヒープサイズ項目が64ビット化されたPE32+形式となる。64ビット版であることを示すフラグは、PEヘッダ内のIMAGE_NT_HEADERS64構造体→IMAGE_FILE_HEADER構造体のメンバMachineの値(0x8664)。
・64ビット版Windows 7/8におけるプロセスメモリ関連の主な規定値は下記
ページサイズ:00001000(32ビット版と同じ)
VirtualAlloc関数によるメモリ領域確保時の先頭アドレス単位:00010000(32ビット版と同じ)
ユーザーモードアプリ最小アドレス:000'00010000(32ビット版と同じ)
ユーザーモードアプリ最大アドレス:7FF'FFFEFFFF
(システムはFFFF0800'00000000 ~ FFFFFFFF'FFFFFFFFの一部を使用)
・64ビット版Windows 8.1ではユーザーモードアプリ最大アドレスが7FFF'FFFEFFFFとなる。
・64ビット版Windowsでは、アドレスとポインタだけではなく、プログラムの処理で使用される各種ハンドルのサイズも64ビットとなる。なお、32ビット版同様に、通常プロセスメモリ上のメモリ領域やモジュールのハンドル(の値)には先頭アドレスが使用される。
・ASLRが適用されない場合の、メインモジュールのプロセスメモリ上のアドレスは001'40000000となる。プロセスメモリエディタでの検索対象となる、VirtualAlloc関数等によリ確保されたメモリ領域のアドレスが、このメインモジュールのアドレスより低くなるとは限らない。
・PEB(Process Environment Block)もアドレスの64ビット化に伴いレイアウトが変更されている。NtQueryInformationProcess関数によるPEBのアドレス取得は可能。
・関数呼び出し時の、整数の第1引数から第4引数の指定は、順にRCX、RDX、R8、R9レジスタへの格納で行う。この際スタック内にはこれらのレジスタに対応する4レジスタ分(0x20バイト)の領域が確保され、呼び出し先の関数が、引数のレジスタの値等をこの領域に格納して使用可能。コンパイラによっては、オプションを指定することで、プログラムがこの領域にRCX、RDX、R8、R9レジスタの値を格納してから関数を呼び出すようにすることもできる。第5引数以降の引数はそのままスタックを用いる。引数が整数ではなく浮動小数点数ならば、128ビットSSEレジスタのXMM0~XMM3レジスタが使用される。API関数内で書き換えられうる揮発性レジスタはRAX、RCX、RDX、R8~R11、およびXMM0~XMM5レジスタ。 戻り値の格納にはRAXあるいはXMM0レジスタ(浮動小数点数用)が使用される。
・レジスタの退避・復旧(PUSH/POP)やローカル変数の設定・破棄(SUB/ADD)といった関数内でのスタックの操作は、関数の最初と最後に配置される「プロローグ」および「エピローグ」と呼ばれるプログラムコード群で行われる。RSPレジスタはスタックポインタとしてローカル変数の操作にも使用され、「プロローグ」と「エピローグ」の間では変化しない。なお、スタックは 「プロローグ」内など以外では、16バイトでアライメントされる必要がある。RBPレジスタは、旧来のEBPレジスタのようなスタックフレームを指し示す用途に使用することもできる。
<参考>MessageBoxW関数呼び出し例 ;プロローグ/エピローグに4引数分(20h)+アライメント用(8h)の下記スタック確保/破棄処理あり ;sub/add rsp,28h if(IDYES == ::MessageBoxW(MyhWnd, L"Text", L"Caption", MB_YESNO) ) //ソースコード 000000013F9B1984 mov rcx,qword ptr [rcx+40h] 000000013F9B1988 lea r8,[string L"Caption" (13FB6ABC0h)] 000000013F9B198F lea rdx,[string L"Text" (13FB6ABD0h)] 000000013F9B1996 mov r9d,4 ;MB_YESNO 000000013F9B199C call qword ptr [__imp_MessageBoxW (13FB1B030h)] 000000013F9B19A2 cmp eax,6 ;IDYES <参考>CreateFont関数呼び出し例 ;プロローグ/エピローグに14引数分(70h)の下記スタック確保/破棄処理あり ;別途1レジスタの退避/復旧でアライメントされている ;sub/add rsp,70h ;引数の内訳はLOGFONT構造体の定義を参照 CreateFont(lf.lfHeight, lf.lfWidth, lf.lfEscapement, lf.lfOrientation, lf.lfWeight, lf.lfItalic, lf.lfUnderline, lf.lfStrikeOut, lf.lfCharSet, lf.lfOutPrecision, lf.lfClipPrecision, lf.lfQuality, lf.lfPitchAndFamily, lf.lfFaceName); //ソースコード 000000013FFB198A movzx eax,byte ptr [lf+1Bh (1401B017Bh)] 000000013FFB1991 movzx ecx,byte ptr [lf+1Ah (1401B017Ah)] 000000013FFB1998 movzx edx,byte ptr [lf+19h (1401B0179h)] 000000013FFB199F movzx r8d,byte ptr [lf+18h (1401B0178h)] 000000013FFB19A7 movzx r9d,byte ptr [lf+17h (1401B0177h)] 000000013FFB19AF movzx r10d,byte ptr [lf+16h (1401B0176h)] 000000013FFB19B7 movzx r11d,byte ptr [lf+15h (1401B0175h)] 000000013FFB19BF movzx ebx,byte ptr [lf+14h (1401B0174h)] 000000013FFB19C6 lea rdi,[lf+1Ch (1401B017Ch)] 000000013FFB19CD mov qword ptr [rsp+68h],rdi 000000013FFB19D2 mov dword ptr [rsp+60h],eax 000000013FFB19D6 mov eax,dword ptr [lf+10h (1401B0170h)] 000000013FFB19DC mov dword ptr [rsp+58h],ecx 000000013FFB19E0 mov ecx,dword ptr [lf (1401B0160h)] 000000013FFB19E6 mov dword ptr [rsp+50h],edx 000000013FFB19EA mov edx,dword ptr [lf+4 (1401B0164h)] 000000013FFB19F0 mov dword ptr [rsp+48h],r8d 000000013FFB19F5 mov r8d,dword ptr [lf+8 (1401B0168h)] 000000013FFB19FC mov dword ptr [rsp+40h],r9d 000000013FFB1A01 mov r9d,dword ptr [lf+0Ch (1401B016Ch)] 000000013FFB1A08 mov dword ptr [rsp+38h],r10d 000000013FFB1A0D mov dword ptr [rsp+30h],r11d 000000013FFB1A12 mov dword ptr [rsp+28h],ebx 000000013FFB1A16 mov dword ptr [rsp+20h],eax 000000013FFB1A1A call qword ptr [__imp_CreateFontW (14011A290h)]
・基本的に、プログラム解析ツールと解析対象アプリケーションはビット数を合わせるのが望ましい。特に既存の32ビット版プロセス解析・操作ツールからの64ビットアプリケーションのプロセス操作は、可能なケースもあるが、内部処理で32ビットのアドレスやハンドルを適用するため不具合を招くケースもある。64ビット版の解析ツールから32ビット版のプロセス・スレッドを操作する場合は、Wow64GetThreadContext関数などの専用API関数が必要となるケースがある。
・64ビット版アプリケーション間での、CreateRemoteThread関数、VirtualAllocEx関数およびWriteProcessMemory関数等を用いた、対象プロセスへの64ビット版DLLのロードは問題ない。この場合32ビット版DLLを指定するとロードに失敗する。なお、CreateRemoteThread関数を使って、プロセスメモリ上で動的に書き込んだプログラムコードを実行させる場合は、DEPへの対処のため、書き込み先のメモリ領域には「読み書き」に加えて「実行」のアクセス属性が必要となる。
・一部のコンパイラでは、64ビット版アプリケーション作成時での、API関数呼び出し時の引数として指定した構造体に、誤って32ビット版を自動的に適用するケースがある(GetThreadContext関数呼び出し時のCONTEXT構造体など)。この場合は64ビット版の構造体を明示的に指定する。64ビット版の構造体の定義は、コンパイラやWindows SDKに付属する、インクルードファイル「WinNT.h」などで参照可能。
Visual C++ での64ビット版アプリケーション開発時ではインラインアセンブラが使用できない。ただし、標準で用意されている「__cpuid」などの組み込み関数の使用や、MASMの64ビット版でソースファイルをアセンブルした上でリンクするという代替策がある。プロジェクト内のasmファイルを、カスタムビルドする際のコマンドライン設定例と「出力ファイル」の設定例は下記。
ml64 -c "-Fl$(IntDir)%(Filename).lst" "-Fo$(IntDir)%(Filename).obj" "%(FullPath)"
$(IntDir)%(Filename).obj;%(Outputs)
<参考>上記asmファイルの記述例(動作確認:Visual C++ 2010) ;プロセッサ指定は不要 .data ;グローバル変数設定例 dqVar1 dq 0 ;QWORD dwVar2 dd 10h DUP(0) ;DWORDの配列 dbBytes db 20h DUP(0) ;Byte列 ;メッセージボックス関連設定 NULL equ 0 MBCaption db "子曰く",0 MBText db "義を見てせざるは勇無きなり",0 MB_OKCANCEL equ 1 IDCANCEL equ 2 .code ;API関数の設定 extern MessageBoxA :proc extern MessageBeep :proc ;CPUID命令を実行する関数の記述 ;第1,2引数は観察用のダミー ;int TestCpuid(__int64 Arg1, int Arg2, unsigned int CpuidInput); //関数呼び出し元での定義 TestCpuid PROC frame ;第1引数はRCX、第2引数はEDXレジスタに格納されている ;R8dレジスタ32ビットには第3引数が格納されている ;スタックを16バイトにアライメント ;リターンアドレス分8バイトにダミー8バイト追加 ;さらに引数用4レジスタ分の領域20hを確保 sub rsp, 28h ;.allocstack 8 ;アンワインド用 ;R8レジスタをスタックに退避 mov qword ptr [rsp +20h], r8 .endprolog ;プロローグここまで ;変数操作例 mov dqVar1, rcx mov dword ptr [dwVar2], edx mov dword ptr [dwVar2 +18h], edx mov rax, offset dwVar2 mov r10, 8 lea rcx, [rax +r10*2 +8] ;アドレスを演算 mov eax, dword ptr [rcx] ;API関数使用例 mov rcx, NULL mov rdx, offset MBText mov r8, offset MBCaption mov r9, MB_OKCANCEL call MessageBoxA ;ジャンプ例 cmp rax, IDCANCEL ;戻り値と比較 je SelCancel xor rcx, rcx ;MB_OK = 0 call MessageBeep SelCancel: ;NOP nop ;0x90 ;nop dword ptr [eax +00h] などは最適化の影響を受ける ;R8レジスタをスタックから復旧 mov r8, qword ptr [rsp +20h] ;以下でRAXレジスタ64ビットにR8dレジスタをゼロ拡張して格納 mov eax, r8d cpuid ;ECXレジスタは得られたCPU情報の一部 ;これを戻り値となるRAXレジスタに格納 ;mov rax, rcx xchg rax, rcx ;エピローグ ;ダミー8バイトの破棄 ;引数用4レジスタ分の領域20hも破棄 add rsp, 28h ret TestCpuid ENDP end
ホットパッチ型APIフックがうまくいきません
●基本事項ホットパッチ型APIフックについて考察する前に、まず、DLLインジェクション(注入)を用いたAPIフックについて、その仕組みを理解しておくことをお勧めします。特に、対象プロセスが使用しているフック対象モジュールのIAT(Import Address Table)を書き換えて、APIフック用DLL内の関数を実行させるアプローチを自分で試してみることは、PEヘッダ・IAT・関数呼び出し(引数と戻り値)・API関数というプログラム解析の基礎事項の理解に繋がります。なお、IAT書き換え型のAPIフックについては、参考となるソースコードがネット上で多々公開されています。
たいていの場合、DLLインジェクションは頻用される、CreateRemoteThread関数とVirtualAllocEx関数およびWriteProcessMemory関数を使用する方法で対処可能です。しかし、プロセスに任意のDLLをロードさせる方法としては、他にも「レジストリのAppInit_DLLs」や「SetWindowsHookEx関数」、「対象モジュールがロードするDLLを仲介DLLに差し替え」および「対象プロセスのスレッドのEIP書き換えでDLLロード処理を実行」などのアプローチがありますので、それぞれの長所・短所を考察してから、状況によって使い分けるとよいでしょう。なお、『うさみみハリケーン』は、任意のDLLを対象プロセスにロードさせる機能を実装しています(メニューの[デバッグ]→[DLLのロードとアンロード])。
ホットパッチ型APIフックでは、一般的なAPI関数のエントリとその前方にある、以下のニーモニックを書き換えます(以下のアドレスは例)。『うさみみハリケーン』でメニューの[デバッグ]→[選択範囲を逆アセンブル]から一行アセンブラによる[コード修正]を使えば、この書き換え処理を簡単に試行可能です。
▲書き換え前 77D307E5 90 NOP 77D307E6 90 NOP 77D307E7 90 NOP 77D307E8 90 NOP 77D307E9 90 NOP 77D307EA 8BFF MOV EDI,EDI //フック対象API関数エントリ ▲書き換え後 77D307E5 E9 5608728B JMP 03451040 //APIフック用DLL内の関数にジャンプ:03451040-77D307EA=8B720856 77D307EA EB F9 JMP SHORT 77D307E5ホットパッチ型のAPIフックに関しては、以下の点を十分に踏まえて使用する分には特段の問題は無いと考えられます。
・本来はシステムを再起動するまでのセキュリティパッチなどに使用されるべきであること
・一部のシンボル系API関数の動作に影響を及ぼすこと
●IAT書き換え型のAPIフック用DLL内の関数記述例(動作確認:Visual C++ 2008)
ホットパッチ型のAPIフックとの比較用に、一般的に使用されるIAT書き換えによるAPIフックの例を挙げておきます。
プロセスメモリ上の対象モジュールのIATから、APIフック用DLLモジュール内のこの関数に繋げて、関数内部でフック対象API関数を呼び出します。対象モジュールが参照するIAT内の、フック対象API関数のアドレスを格納しているアドレスを探し出す際には、対象モジュールのPEヘッダ→インポートディレクトリ(IMAGE_OPTIONAL_HEADER32構造体のDataDirectory[1])→IMAGE_IMPORT_DESCRIPTOR構造体と辿っていきます。関連するImageDirectoryEntryToDataEx関数についても、MSDNなどで参照されることをお勧めします。なお、パッカー使用によりPEヘッダが指し示すIATが異常な場合は、対象モジュール内のメモリサーチといった独自の方法で本来の上記アドレスを探し出す必要があります。ただし、一部のパッカーは元のIATを書き換えるため、必ずしも本来の上記アドレスが見つかるとは限りません。
このフック用関数の名前は、DEFファイルでエクスポート関数として名前を指定すれば、関数名は修飾されません。
//MessageBoxA関数呼び出しをフック //関数の定義をMessageBoxA関数と同じにする int WINAPI MyMessageBoxA( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType ){ //ここで引数を操作可能(記録・変更等) //関数呼び出し時の変更後引数指定例 ::MessageBoxA(hWnd, "MessageBoxA関数をフックしました", "APIフック例", MB_YESNOCANCEL|MB_ICONINFORMATION); //ここで戻り値を操作可能(記録・変更等) return IDOK; //return ::MessageBoxA(hWnd, (LPCSTR)lpText, (LPCSTR)lpCaption, uType); //戻り値を変更しない例 };
●ホットパッチ型のAPIフック用DLL内の関数その他記述例(動作確認:Visual C++ 2008)
[stdafx.h] //MessageBoxA関数の定義を複製 typedef int (WINAPI *p_MessageBoxA)( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType ); [dllmain.cpp] //グローバル変数としてMessageBoxA関数エントリ+2のアドレスを保存する p_MessageBoxA p_NewMessageBoxA; //フック用関数のアドレス取得用 p_MessageBoxA p_MyMessageBoxA; DWORD g_JmpOpe; //JMP命令用オペランド unsigned char NewEntry[7] = {0xE9,0x00,0x00,0x00,0x00, 0xEB,0xF9}; DWORD g_NewEntryAdr; DWORD g_OldProtect; DWORD g_CalcAdr; DllMainのcase DLL_PROCESS_ATTACH: //MessageBoxA関数のアドレスを取得 p_NewMessageBoxA = (p_MessageBoxA)GetProcAddress(GetModuleHandle("User32.dll"), "MessageBoxA"); //簡易エラーチェック if(!p_NewMessageBoxA){ OutputDebugString("フック対象関数アドレスの取得に失敗しました"); break; } if( (*(WORD *)p_NewMessageBoxA == 0xF9EB) || (*(BYTE *)p_NewMessageBoxA == 0xE9) ){ OutputDebugString("すでにAPIフックが施されています"); break; } if( (*(WORD *)p_NewMessageBoxA != 0xFF8B) || (*(DWORD *)((DWORD)p_NewMessageBoxA-4) != 0x90909090) ){ OutputDebugString("関数エントリのバイナリデータが異常です"); break; } //このDLLモジュール内のフック関数のアドレスを取得 p_MyMessageBoxA = (p_MessageBoxA)GetProcAddress(hModule, "MyMessageBoxAHP"); //ホットパッチ用のバイナリデータとアドレスを作成 g_JmpOpe = (DWORD)p_MyMessageBoxA - (DWORD)p_NewMessageBoxA; *(DWORD *)&NewEntry[1] = g_JmpOpe; //*(DWORD *)(NewEntry + 1)と同義 g_NewEntryAdr = (DWORD)p_NewMessageBoxA - 5; //追加ニーモニック分の5バイト //このDLLモジュール内のフック関数から呼び出すMessageBoxA関数アドレスを設定 g_CalcAdr = (DWORD)p_NewMessageBoxA; g_CalcAdr += 2; //上書きする「MOV EDI,EDI」の次のニーモニックのアドレスにする p_NewMessageBoxA = (p_MessageBoxA)g_CalcAdr; //__asm{ add p_NewMessageBoxA, 2 } //インラインアセンブラによる設定例 //VirtualProtect関数は0x1000バイトのページ単位で処理するためEXECUTE属性を保持してDEPに対処 //ホットパッチ用のバイナリデータで書き換え実行 if( VirtualProtect( (LPVOID)g_NewEntryAdr, sizeof(NewEntry), PAGE_EXECUTE_READWRITE, &g_OldProtect) ){ memcpy( (void *)g_NewEntryAdr, NewEntry, sizeof(NewEntry) ); VirtualProtect( (LPVOID)g_NewEntryAdr, sizeof(NewEntry), g_OldProtect, &g_OldProtect); } else{ OutputDebugString("ホットパッチ用のエントリ作成に失敗しました"); } [MyHookDll.cpp] char g_OutPutTxt[1024]; extern p_MessageBoxA p_NewMessageBoxA; int WINAPI MyMessageBoxAHP( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType ){ wsprintf(g_OutPutTxt, "Text:%s, uType:0x%08X", lpText, uType); OutputDebugString(g_OutPutTxt); //本来のMessageBoxA関数エントリ+2のアドレスをコール p_NewMessageBoxA(hWnd, "MessageBoxA関数をフックしました", "APIフック例", MB_YESNOCANCEL|MB_ICONINFORMATION); return IDOK; //return p_NewMessageBoxA(hWnd, (LPCSTR)lpText, (LPCSTR)lpCaption, uType); };
●同上インラインアセンブラでの記述例
__declspec( naked ) int WINAPI MyMessageBoxAHPasm( ) { //nakedなので引数の定義は記述不要、スタックフレームは作成されない //ここでdword ptr [esp]にはリターン先アドレスが格納されている static char HookTxt[48] = "MessageBoxA関数をフックしました"; static char HookCap[48] = "APIフック例"; static char OutPutTxtForm[48] = "Text:%s, uType:0x%08X"; static char OutPutTxt[1024]; __asm{ //push dword ptr [ESP + ??h] も例外的に可能だがバグの誘因になりえる mov eax, dword ptr [esp + 10h] //uType mov edx, dword ptr [esp + 8h] //lpText push eax push edx push offset OutPutTxtForm push offset OutPutTxt call dword ptr [wsprintfA] add esp, 10h //可変引数なのでスタックの破棄を呼び出し側で行う push offset OutPutTxt call dword ptr [OutputDebugStringA] //新しい引数を設定 mov eax, dword ptr [esp + 4h] //hWnd push MB_YESNOCANCEL|MB_ICONINFORMATION push offset HookCap push offset HookTxt push eax mov eax, dword ptr [MessageBoxA] add eax, 2 //上書きする「MOV EDI,EDI」の次のニーモニックのアドレスにする call eax mov eax, IDOK //戻り値を変更 ret 10h //この関数の引数分のスタックを破棄してリターン } };
●補足
ホットパッチ型ではなく、API関数エントリ以降を書き換えるタイプのAPIフックについては、『うさみみハリケーン』でメニューの[デバッグ]→[パラサイトルーチン作成]で行うことが出来ますので、参考にされることをお勧めします。また、うさみみハリケーン付属のPEダンパー兼PEエディタ 「UMPE」には、別のAPIフックのアプローチである、「仲介DLL(Proxy DLL)ソースコード出力機能」を実装しています。
Windows 8以降での、WindowsストアアプリといったAppContainerアプリケーションに対しては、基本的にCreateRemoteThread関数などを用いたDLLインジェクションが使用できません。
プログラミングにおけるデバッグ目的のAPIフックならば、『sexyhook』といった開発者用APIフック手法を使用する選択肢もあります。
プログラムが認識するフォルダのパスを変更したい
たとえばPCゲームでは、セーブデータの格納先フォルダがマイドキュメント内などに固定されるケースがあります。ユーザーにおける利便性の向上を目的として、シンボリックリンクなどではなくプログラム解析を用いて、このようなプログラムによって固定化されるフォルダのパスを変更したい場合は、まずプログラムがどのようにしてフォルダのパスを取得するかを考察する必要があります。以下では、プログラムが各種フォルダのパスを取得する主な方法を列挙しています。なお、必ずしも下記の方法を使用するとは限りませんのでご注意願います。
また、プログラムが認識するフォルダのパスを変更すれば、特にアンインストール時など各種操作時に何らかの支障が生じる可能性があります。下記で解説しているアプローチは、不具合発生の可能性を十分に踏まえた上で行ってください。
●特殊フォルダ
「マイドキュメント」や「アプリケーションデータ」などの、Windowsが用意している特殊フォルダのパスは、基本的にSHGetFolderPath関数で取得可能です。同関数呼び出し時の第2引数にCSIDL(特殊フォルダの種類を識別するための値)を指定します。同関数呼び出しにより、第5引数に指定したバッファに特殊フォルダのパスが格納されます。PCゲームでは、同関数に置き換えられた旧式のSHGetSpecialFolderPath関数を呼び出すケースもあります。主なCSIDLの定義名と定義値は以下。
・マイドキュメント
CSIDL_PERSONAL (0x0005)
CSIDL_MYDOCUMENTS (0x000C) //古い定義値:現在では0x0005
・アプリケーションデータ(全ユーザー)
CSIDL_COMMON_APPDATA (0x0023)
・アプリケーションデータ(現在のユーザー)
CSIDL_APPDATA (0x001A)
・プログラムファイル
CSIDL_PROGRAM_FILES (0x0026)
CSIDL_PROGRAM_FILESX86 (0x002A)
・Windows
CSIDL_WINDOWS (0x0024)
・システム
CSIDL_SYSTEM (0x0025)
CSIDL_SYSTEMX86 (0x0029)
アプローチとしては、EXEファイルを書き換えて、SHGetFolderPath関数呼び出し時のCSIDLを変更したり、あるいは、同関数呼び出し処理を改変してあらかじめEXEファイルに書き込んでおいた特定パス文字列(NULL終端必須)を、ASLR対処の上でリピートプリフィックス命令(参照)を用いて第5引数のバッファにコピーすることで、プログラムが認識するフォルダのパスを変更可能になります。このアプローチは、PCゲームならば異常動作を回避するために、初回起動時といった「各種情報をセーブデータやレジストリに書き込んでいない状態」で行うことをお勧めします。なお、もしも変更後のパスが既定の「Program Files」フォルダ内ならば、Windows Vistaで実装されたUACのファイル/レジストリ仮想化機能(VirtualStore)の影響を受けることになります。
関連して、特殊フォルダのパス取得は、SHGetFolderPath関数以外では、SHGetKnownFolderPath関数や、COMのIKnownFolderManager::GetFolderメソッドを用いる方法もありますが、ともにWindows Vistaで実装されたため、Windows XP対応のPCゲーム等ではまず使用されないと考えられます。なお、Windows Vista以降では、SHGetFolderPath関数は互換性保持のために存在し、内部でSHGetKnownFolderPath関数を呼び出します。IKnownFolderManager使用の場合は、あらかじめCoCreateInstance関数(第1引数はCLSID_KnownFolderManagerすなわちバイト列"30C7F04D9DDFE34A9153AA6B82E9795A"のアドレス)を呼び出します。SHGetKnownFolderPath関数呼び出し処理そのものを書き換える場合は、同関数呼び出しとセットになるCoTaskMemFree関数呼び出し処理も併せて書き換える必要があることに注意してください。
アプリケーションデータフォルダの上位となる、ユーザープロファイル関連フォルダのパス取得には、以下のAPI関数が使用可能です。だたし、一般的なPCゲームでのセーブデータ等の格納フォルダならば、上記SHGetFolderPath関数で直接パスを取得すると考えられます。
GetProfilesDirectory
GetAllUsersProfileDirectory
GetDefaultUserProfileDirectory
GetUserProfileDirectory
加えてWndows 8以降では、AppContainerアプリケーション用のGetAppContainerFolderPath関数が追加されています。
ちなみに、これらの特殊フォルダには、エクスプローラの「フォルダ オプション」で表示の詳細設定を「すべてのファイルとフォルダを表示する」にしておかないと、エクスプローラ上で表示されないものがあります。
『うさみみハリケーン』では、上記の主な特殊フォルダをオープンする機能を実装しています(メニュー[その他]→[特殊フォルダを開く])。
●Windowsフォルダ・システムフォルダ
これらのフォルダのパスを取得する場合は、上記SHGetFolderPath関数とは別に、専用のGetWindowsDirectory関数やGetSystemDirectory関数が使用可能です。なお、64ビット版Windows上では、GetSystemWow64Directory関数という、32ビット版システムファイルの格納フォルダのパスを取得するAPI関数もあります。
●テンポラリフォルダ
プログラムが一時的に使用するファイルを格納するための、テンポラリフォルダのパス取得にはGetTempPath関数を使用します。一部のソフトウェアでは、EXEファイル内にリソースとして暗号化した別の実行ファイル(EXEやDLLなど)を格納しておき、その実行ファイルをこのフォルダに展開・復号化して実行させるケースがあります。
●プログラムのEXEファイルがあるフォルダ
実行されているプログラムが、自分自身のEXEファイルがあるフォルダのパスを取得する場合は、通常GetModuleFileName関数(第1引数はNULL)を利用します。同関数でEXEファイルのパスを取得し、StrRChr関数などを使ってそのパスからEXEファイル名部分を切り離せば当該フォルダのパスを得られます。GetModuleFileName関数呼び出し時には、第2引数で指定したバッファにEXEファイルのパスが格納されます。
なお、自分を含む特定プロセスのメインモジュールのパスを取得するということならば、Tool Help APIのModule32First関数や、Process status API(PSAPI)のGetModuleFileNameEx関数、GetProcessImageFileName関数およびQueryFullProcessImageName関数を使用することができます。他にも、ネイティブAPIのNtQueryVirtualMemory関数などを用いた、特定プロセスのモジュールのパスを列挙する複数の方法が流用可能です。
●カレントフォルダ
現在の作業用フォルダのパスはGetCurrentDirectory関数で取得します。
カレントフォルダは、プログラムがLoadLibrary関数などで特定DLLファイルをロードする際に、自分のEXEファイルがあるフォルダやWindowsフォルダ・システムフォルダに同DLLファイルが存在しない場合、DLLファイル検索対象フォルダとなります(Windows XP SP2以降の仕様)。この仕様を利用する、悪意のあるDLLファイルをプロセスにロードさせる攻撃手法があり、「DLLのプリロード攻撃」や「バイナリ・プランティング攻撃」などと呼ばれています。関連して、Windowsには、DLLファイル検索対象フォルダを操作するSetDllDirectory関数や、同種の仕組みである「DLLのリダイレクション」が実装されています。
●補足
アプリケーションデータフォルダ等の一部のフォルダのパスは、GetEnvironmentVariable関数を用いて、自分のプロセスが保持する環境変数を参照することでも取得可能です。同関数ではPEB(Process Environment Block)を経由してこの環境変数を辿っており、同関数の処理を自分のプログラムの処理でエミュレートすることもできます(参照)。ちなみに、「%APPDATA%」や「%windir%」といった環境変数を示す文字列を、Windowsの[ファイル名を指定して実行]で実行させると、当該フォルダがエクスプローラでオープンされます。
このような特定プロセスの環境変数は、『うさみみハリケーン』でメニューの[ファイル]→[プロセスの各種情報を表示]→[環境データ]で確認できます。また、『うさみみハリケーン』起動時に表示されるプロセスリスト上で、右クリックによるポップアップメニューで[プロセスの各種情報を表示]から同機能を呼び出すこともできます。
エントリーポイント以前に実行される解析対策について知りたい
●概要Windowsには、特定スレッドに独自のデータを関連付ける「スレッドローカルストレージ(TLS)」という仕組みが用意されています。この仕組みには、EXEモジュールのエントリーポイントよりも先に実行される特殊な関数「TLS Callback関数」を設定することができるという特徴があります。
TLS Callback関数はエントリーポイント以前に実行されるという特殊性で、リバースエンジニアの耳目を集めることもあり、「Crack Me」及び「Proof of Concept」などで題材として取り上げられることがあります。また、一部のマルウェアで解析対策に採用されています。
ただし、後述の解析手順のようにTLS Callback関数はPEヘッダから辿ればその存在が容易に判明することや、その解析もさして困難ではないことから、日本製のマルウェア、PCゲーム(アダルトゲームや同人ゲームを含む)及びシェアウェアで解析対策に用いられたケースは少ないと見られます。
TLS Callback関数は、何らかの初期化処理といった用途ではたいてい1つで事足りますが、PEファイルの仕様上は複数のTLS Callback関数を実装することが可能です。プログラム解析ツールによっては一番最初のTLS Callback関数のみ認識すること等を勘案すると、TLS Callback関数が複数実装されているケースでは解析対策の可能性を十分に考慮すべきと考えられます。なお、PEヘッダのIMAGE_OPTIONAL_HEADER構造体でディレクトリの「Thread local storage (TLS) Table」が設定されていても、TLS Callback関数が実装されているとは限りません。
TLS Callback関数を実装したEXEファイルにパッカーを適用する場合は、そのパッカーがTLS Callback関数をサポートしていないならば、TLS Callback関数が実行不能になるといった不具合が生じます。なお、TLS Callback関数を、パッカーが実行ファイルに付加する展開・復号ルーチンのように使用する解析対策も可能です。
『うさみみハリケーン』同梱のPEエディタ『UMPE』では、TLS Callback関数の一覧表示機能を実装しています。
●TLS Callback関数の実装例(動作確認:Visual C++ 2010)
//グローバル //引数はDllMain関数と同じ仕様 VOID NTAPI tls_callback(PVOID hinstExe, DWORD fdwReason, PVOID) { //EXEモジュールが実行可能になったことを通知されたら処理を実行 if (fdwReason == DLL_PROCESS_ATTACH)//値1 { char Txt[128]; wsprintf(Txt, "HINSTANCE: %08X fdwReason: %08X", hinstExe, fdwReason); MessageBox(NULL, Txt, "TLS Callback", MB_OK); //構造化例外ハンドラを用いたデバッグ検出も可 if(IsDebuggerPresent()){ MessageBox(NULL, "デバッガから起動されています", "TLS Callback", MB_OK); ExitProcess(0xDEADC0DE); } } } #pragma comment(linker, "/INCLUDE:__tls_used") #pragma comment(linker, "/INCLUDE:_tls_entry") #pragma data_seg(".CRT$XLB" ) extern "C" PIMAGE_TLS_CALLBACK tls_entry = tls_callback; #pragma data_seg() //Visual C++ の古いバージョンならば以下で複数のTLS Callback関数を実装可(動作確認:Visual C++ 6.0) /* DWORD _tls_index = 0; PIMAGE_TLS_CALLBACK callback = tls_callback; PIMAGE_TLS_CALLBACK callback2 = tls_callback2; extern "C" IMAGE_TLS_DIRECTORY _tls_used = {0, 0, &_tls_index, &callback, 0, 0}; */なお、TLSの仕様からみれば、既存のEXEファイルを改変して、TLS Callback関数を手動で追加実装することも可能といえます。ただし、使用可能なAPI関数などで制約が生じます。
●TLS Callback関数の解析手順(OllyDbgの操作方法一般については「OllyDbg Q&A」も参照願います)
まず、日本語化済OllyDbgでメニュー[オプション]→[解析詳細設定]の[イベント]タブで、「最初に止める条件」を「システムブレークポイント」に設定します。次に、対象EXEファイルをOllyDbgから起動し、システムブレークポイントでのブレーク後に、メニュー[表示]→[メモリ]で表示されるメモリマップ上で、EXEモジュール先頭アドレスを選択して、右クリックから[Dump]でPEヘッダの内容を表示させます。PEヘッダの内容にある「TLS Table address = 」で表示される値(RVA)に、EXEモジュール先頭アドレスを加算し、メモリマップ上でこの演算結果アドレスが含まれる領域の先頭アドレスを選択して、右クリックから[Dump]でバイナリデータを表示させます。これにより、演算結果アドレス+0Chの格納値が、TLS Callback関数先頭のアドレス(群)が格納されたアドレス(AddressOfCallBacks)になります。そこで、このAddressOfCallBacks以降のバイナリデータを確認し、メインウィンドウの逆アセンブルコードリスト上で、TLS Callback関数先頭のアドレスそれぞれに、F2キーを使ってブレークポイントを設定します。このTLS Callback関数先頭のアドレスの配列は、NULL(値0)が終端です。最後にF9キーでプロセスを実行させると、TLS Callback関数が呼ばれるごとにブレークします。
上記とは別の方法でTLS Callback関数の先頭アドレス(群)を簡単に確認したければ、『うさみみハリケーン』で対象プロセス選択後に、メニュー[プラグイン]→[UHPAdrCalc.dll]でアドレス演算プラグインを起動してから、「演算式」入力欄に下記の演算式を貼り付けます。これにより、演算結果アドレス以降に格納される、TLS Callback関数の先頭アドレスの配列を、『うさみみハリケーン』のダンプ表示ウィンドウ上で視認可能になります。なお、対象EXEファイルがASLR対応ならば、TLS Callback関数の先頭アドレスは起動時ごとに変化します。下記演算式はASLRによるEXEモジュールのアドレス変動に対応していますが、必要に応じてASLRを無効化してください(参考)。
ptr(g+ptr(g+ptr(g+3C)+C0)+0C)
解析対策用TLS Callback関数を簡単に無効化するならば、『うさみみハリケーン』に同梱している『UMPE』のPEエディタを使用して、解析対象EXEファイルをオープン後「PEファイル」→「ディレクトリ」→「Thread local storage (TLS) Table」のRVAの値をゼロに書き換えます。ただし、この書き換えは解析対象側でも容易に検出可能な点に注意してください。
●余談
TLS Callback関数を用いた解析対策は、このような仕組みがあることを知らなければ対処が困難なケースといえます。少なくともリバースエンジニアリングで興味の赴く方面だけは、日頃から知識の涵養を怠らないことをお勧めします。
また、マルウェアは必然的に解析されにくいトリッキーな解析対策を実装することが少なくありません。マルウェア製作者が採るであろう、『孫子』の兵法でいえば「其の必ず趨く所に出で、其の意(おも)わざる所に趨く」という戦略への留意が、マルウェアの解析にあたり思索の一助となることもあるでしょう。
プロセスとプロセスでやり取りする方法を知りたい
●概要Windows上での各プロセスは独立して実行されています。そのため、複数のプロセスで一つのアプリケーションを実現している場合などに使用する、プロセス同士でデータの共有・送信を行うといった通信方法が用意されています。Windows APIでのこの方法は「プロセス間通信(Interprocess Communications:IPC)」と呼ばれています。また、他のプロセスとの通信を想定していないプロセスの実行処理に介入し、プロセス間でのデータのやり取りなどを可能にする特殊な方法もあります。なお、以下で述べるような直接的なやり取りではない、データ保存ファイルやレジストリ等を介した間接的なプロセス間のデータの受け渡しも可能です。
●プロセス間通信
プロセス間通信の手法としては、「COM」、「Pipes」、「Mailslots」、「Windows Sockets」、「File Mapping」および「WM_COPYDATA」ほかの色々な方法がありますが、特に実装が容易なのはFile Mappingです。この場合、CreateFileMapping関数で第1引数にINVALID_HANDLE_VALUE(定義値は-1)を指定して呼び出せば、各プロセスから参照・編集可能な「共有メモリ」を作成することができます。他のプロセスでは、OpenFileMapping関数とMapViewOfFile関数の呼び出しにより、この共有メモリに対して、自分のプロセスメモリ内のメモリエリアのようにアクセスすることが可能になります。
WM_COPYDATAはWindowsメッセージの一つです(定義値は0x004A)。このメッセージで、32/64ビットの数値(64ビットのプロセスならば64ビットの数値)や、特定データを送信することができます。受信側プロセスでは、その送信されたデータが一時的にコピーされている、受信側プロセスのプロセスメモリ上のアドレスが通知されます。このメッセージの送信にはSendMessage関数を使用します。
WM_COPYDATAメッセージではその特殊な仕組みにより、送信側と受信側のプロセスで、データの格納アドレスの違いを意識する必要がありません。しかし、基本的には各プロセスでのプロセスメモリのレイアウトは異なることに留意してください。つまり、もしWM_COPYDATAメッセージ以外の方法で、送信側プロセスでのプロセスメモリ上のデータ格納アドレスだけが送信されると、たとえそのアドレスが送信側からみた共有メモリ内であっても、そのアドレスを受信したプロセスは、送信元プロセスに対してReadProcessMemory関数を使って送信されたアドレスにあるデータを参照する必要があります。
なお、WM_COPYDATAメッセージはWindows上でのドラッグ・アンド・ドロップ操作の内部処理でも使用されています。そのため、Windows Vista以降では、受信側プロセスの「整合性レベル(Integrity level)」が送信側プロセスよりも高いと、「ユーザーインターフェイス特権の分離(User Interface Privilege Isolation:UIPI)」というセキュリティ関連機能により同メッセージが受信不能となり、ドラッグ・アンド・ドロップ操作を含む、WM_COPYDATA受信処理が無効となるケースがあります。例えば、エクスプローラ(整合性:中)から、管理者権限で起動された解析ツール(整合性:高)のウィンドウへのドラッグ・アンド・ドロップ操作が無効になります(参照)。整合性レベルは、簡単に言えばアクセスに関連する特権レベルのようなものであり、低い整合性のプロセスでは他のプロセスやファイル・レジストリへの操作などが制限されます。特定プロセスの整合性レベルおよびその他セキュリティ上の制約状況は、『うさみみハリケーン』で起動時に表示されるプロセス一覧上で対象プロセスを選択してから、右クリック→[プロセスの各種情報を表示]→[実行状況]カテゴリで一括して確認できます。
ちなみに、上記のようなデータを送信するのではなく、単純に何らかの処理のトリガーとなるWindowsメッセージだけを送ればよい状況ならば、送信先プロセスの特定ウィンドウに対する、WM_COMMANDメッセージ(定義値は0x0111)やWM_APPメッセージ(0x8000~0xBFFFで任意設定可)などの送信で事足ります。SendMessage関数の第3、第4引数で指定する任意のパラメータ(WPARAMとLPARAM)は、32ビットアプリケーションでは32ビットの値で、64ビットアプリケーションでは64ビットの値となることに注意してください。
●コマンドラインオプション
プロセスが他のプロセスに直接情報を伝える方法としては、コマンドラインオプションも挙げられます。例としてCreateProcess関数の第2引数に、何らかのデータを含むコマンドラインオプション文字列を指定することで、新しく実行されるプロセスにそのデータを渡すことが可能です。
コマンドラインオプションの例:
Explorer.exe /select,C:\Neko\Hiyoko.bmp
PCゲームには、まずゲームのランチャーがユーザーや実行環境等のチェックを行い、その後、子プロセス(ゲーム本体)をチェック結果と見られるコマンドラインオプション付きで起動するものがあります。この場合、たいていゲーム本体の実行可能ファイルは直接起動はできないようになっていますが、上記の適切なコマンドラインオプションを指定して実行すると、直接起動が可能になりえます。なお、実行されているプロセスのコマンドラインオプションは、うさみみハリケーンで起動時に表示されるプロセス一覧上で対象プロセスを選択してから、右クリック→[プロセスの各種情報を表示]→[パス]カテゴリで確認可能です。
このような、「必ず親プロセスから起動される子プロセス」をデバッガで解析したい場合は、子プロセスを解析対象に設定できるデバッガ(OllyDbg 2.xxなど)の使用や、下記のJust-In-Time(JIT)デバッガを特定プロセスに自動でアタッチさせる方法などが有用とみられます。
※参考
方法 : デバッガを自動的に起動する
ちなみに、親プロセスが呼び出す、CreateProcess関数の第6引数にCREATE_SUSPENDED(定義値は0x00000004)を論理和で指定するようプログラムコードを修正すれば、子プロセスをエントリーポイントが実行される前に一時停止した状態で起動できるため、子プロセスの解析に役立つことがあります。プロセスの一時停止の解除(Resume)は、うさみみハリケーンを使うならば、起動時に表示されるプロセス一覧上で対象プロセスを選択してから、右クリック→[プロセスを再開]です。
なお、子プロセスは必ずウィンドウを表示する、つまり視認可能とは限りません。また、子プロセスの実行可能ファイルは、必ず親プロセスの実行可能ファイルと同じフォルダから起動されるとは限りません。特にマルウェアを解析する際には、一般的なアプリケーションでの子プロセス作成とは挙動が異なる可能性を考慮してください。
●DLLインジェクション
他のプロセスとの通信を想定していないプロセスから、何らかのデータを送信させたり、あるいは共有メモリにデータ・フラグを書き込ませるならば、対象プロセスにそのような処理を追加して行わせる必要があります。その方法としては、対象プロセスに行わせたい処理を記述したDLLファイルを、対象プロセスに読み込ませて同処理を行わせる手法が一般的です(参考)。この、対象プロセスに任意のDLLファイルをロードさせるアプローチは「DLLインジェクション(DLL Injection)」と呼ばれ、複数の方法があります。うさみみハリケーンでは、メニューの[デバッグ]→[DLLのロードとアンロード]でDLLインジェクションを行うことができます。
対象プロセス内で特定処理を実行させるためには、同DLLロード通知時の処理としてDLL内に処理内容を記述しておいたり、ロードされた同DLL内特定処理のプログラムコードをCreateRemoteThread関数を使って新規スレッドとして実行させます。なお、対象プロセスに新しくスレッドを作成して実行させるCreateRemoteThread関数と同様の処理を行う、ネイティブAPIのRtlCreateUserThread関数ほかがあり、DLLインジェクションなどに使用可能です。
プログラム解析において、DLLインジェクションは特にAPIフック(の簡易化)という用途で頻用されますが、他の用途として、APIフックを用いないプロセスのモニタリングにも活用されています。例えば、プロセスの各種実行状況に関する情報の一部は、自分のプロセスからしか取得できません(オープンしているGDIハンドルの詳細など)。また、特定プロセスのヒープの詳細情報といった、Tool Help APIなど外部からの取得専用のAPI関数を用いても、正確に取得できない情報もあります。そのため、これらのような情報を取得し、モニター側のプロセスへ送信あるいは共有する際には、対象プロセスへのDLLインジェクションが有用です。ただし、対象プロセス内から実行させたい処理のプログラムコードが短いならば、VirtualAllocEx関数で対象プロセスのプロセスメモリ内に確保した、読み書き実行属性付き(実行属性はDEP対策)のメモリエリアにプログラムコードを書き込み、CreateRemoteThread関数で実行させるという簡易的なアプローチで代用可能です。
●ネットワークでのプロセス間通信
上記の手法は同じPC上で実行されているプロセス間でのやり取りを想定したものです。もし、他のPC上のプロセスと双方向の通信を行うならば、Windows Sockets(Winsockとも呼ばれる、ネットワークへのアクセスに関する技術的仕様)を用いた、いわゆるチャットソフトのようにネットワーク内でデータを送受信する方法があります。Windows Socketsで使用するAPI関数は、システムファイルである「Ws2_32.dll」が主に提供しています。ただし、特定プロセスで同DLLがモジュールとしてロードされていても、ネットワークでの送受信を行っているとは限りません。
ちなみに、Winsockによる双方向通信はプログラム解析ツールでも使用されます。その例として、うさみみハリケーン(32ビット版)が実装している、他PC上のプロセスのリモートデバッグ機能および、同プロセスのプロセスメモリのリモートエディット機能で、この通信方法を用いています。かつては、フルスクリーンでのみ動作するゲームを解析するならば、リモートデバッグやリモートエディット対応の解析ツール、あるいはSoftICEのようなカーネルモードデバッガ使用が有効なアプローチとされていました。しかし現在では、『Window Mode Patch for Game』や『DXWnd』などの、ゲーム等のウィンドウモード化(および各種APIフック)を実現するソフトウェアが存在するため、ユーザーモードでのプログラム解析において、リモートデバッグやリモートエディット機能が必要となるケースは少なくなっています。
ネットワークでの通信状況は、『Wireshark』などの、「パケットモニター」その他の名称で呼ばれるツールを用いて解析することができます。 特に情報セキュリティに興味のある方は、この種の解析ツールとその活用例に触れてみることをお勧めします。ただし、送受信されるパケット(ネットワークで送受信されるデータの単位)について学ぶ過程で、送信されるパケットを本来ありえない内容に改変してみるといった研究目的の試行結果が、各種業務妨害等の不法行為にならないよう、十分にご注意願います。
●相互運用性
32ビットのアプリケーションと64ビットのアプリケーション間で、プロセス間通信における相互運用性を持たせるといった理由により、ウィンドウハンドルやファイルハンドルなど一部のハンドル(識別用IDのようなもの)は、32ビットの数値として使われています。32ビットのアプリケーションでは、ハンドルは32ビットの数値です。これに対し、64ビットのアプリケーションでは、ハンドルは64ビットの数値となりますが、上記のウィンドウハンドルなどの場合は下位32ビットだけでも有効なハンドルとなる訳です。ただし、EnumProcessModules関数で得られるモジュールハンドルなど、ハンドルにプロセスメモリ上でのアドレスの数値が用いられるものでは、基本的に上記のような相互運用性はありません。
IAT書き換え型APIフックの注意点を知りたい
●概要インポートする関数のアドレスが格納されるIAT(Import Address Table)を書き換えるタイプのAPIフックは、その実装が簡単でかつ有用なことからAPIフックの基本的なアプローチとされ、長らく各種ユーティリティやプログラム解析ツールを含む色々なソフトウェアで採用されてきました。このAPIフックでは、対象プロセスのモジュール(プロセスメモリ上にロードされた実行可能ファイル)内にあるIATを動的に書き換えます。
基本的なAPIフックの手法とはいえ、以下に列挙するような、このAPIフックを阻害する要因もあるため使用時には注意が必要です。
基本的に、IAT書き換え型APIフックで対応できないケースでは、API関数のエントリ(開始アドレス)以降をツール内蔵の逆アセンブラなどで解析して書き換えるタイプのAPIフック(『うさみみハリケーン』の「プロセスの実行速度調整」機能など)、あるいはホットパッチ型APIフックで対応できます(参照)。また、手動または自動でIATを検索してそのアドレスを書き換えるというアプローチなども可能です。うさみみハリケーンには、モジュールに格納されたインポート関数名の情報に頼らずに、プロセスメモリ上に格納されたアドレスからAPI関数名と提供DLLを解析して一覧表示する、IAT検索機能を実装しています(メニューの[デバッグ]→[指定メモリエリアでIAT検索])。指定アドレスからAPI関数名と提供DLLを得るには、SymFromAddr関数などシンボル系API関数が役立ちます。さらに、うさみみハリケーン付属のPEダンパー兼PEエディタ 「UMPE」には、別のAPIフックのアプローチである、「仲介DLL(Proxy DLL)ソースコード出力機能」を実装しています。
なお、アプローチとしてのIAT書き換え型APIフックを考慮するならば、まず、対象のプログラムが呼び出すAPI関数の一覧を取得・観察してください。これは、うさみみハリケーンならばメニューの[デバッグ]→[モジュール別参照関数表示]で一覧表示されます。この機能では、後述する遅延ロード分も併せて一覧表示されます。
●GetProcAddress関数
APIフック対象プロセスが、LoadLibrary関数かGetModuleHandle関数呼び出し後に、GetProcAddress関数などで特定API関数のアドレスを動的に取得してそのAPI関数をコールするならば、IAT書き換え型APIフックでは対応できません。なお、たいてい、デバッガでGetProcAddress関数のエントリへのブレークポイントを設定することで、どのAPI関数のアドレスを取得したか解析可能です。また、プロセスメモリエディタなどを用いたプログラム解析により、動的に取得されたAPI関数のアドレスを格納したポインタが、プロセスメモリ上で見つかる場合もあります。
上記以外の、特定API関数のアドレスを取得する方法には、GetProcAddress関数と同機能のネイティブAPIを使用する方法があります。他にも、各プロセスのプロセスメモリ上に存在し、プロセスの各種情報を格納する構造体PEBから、ポインタ項目LoaderDataが指すPEB_LDR_DATA構造体経由でモジュールのリストを辿って、特定DLLのエクスポート関数の情報を取得するといった別の方法もあります。LoaderDataは32ビットのプロセスならばPEB先頭+0Chにあります。このPEBを参照する方法は、独立して実行可能なプログラムコードであるシェルコードにおいて、モジュールkernel32.dllのエクスポートテーブル内からWinExec関数のアドレスを取得といった用途に使用可能です。PEB_LDR_DATA構造体やLDR_DATA_TABLE_ENTRY構造体の非公式の定義は下記。うさみみハリケーンでは、メニューの[デバッグ]→[PEB/TEB表示]でPEBの詳細情報を参照可能です。このPEB詳細表示後に、LoaderData(右クリックでポインタ格納アドレスに移動)→PEB_LDR_DATA構造体→LDR_DATA_TABLE_ENTRY構造体と辿ることができます。この際、うさみみハリケーンのダンプ表示画面で「選択アドレスへの栞設定」機能(Ctrl + Jキー)や、「選択アドレスをポインタとみなして表示アドレス変更」機能(Ctrl + Pキー)が役に立ちます。
▲PEB_LDR_DATA構造体 この構造体はPEBのLoaderDataが指すアドレスにある LIST_ENTRYは「双方向リンクリスト」で、次・前2項目のアドレスを格納する2つのポインタで構成される 下記「+**h」は32ビットプロセスでの構造体先頭からの相対位置 64ビットプロセスではアラインメントによりアドレスが格納されるポインタは0x08の倍数のアドレス(QWORD境界)上となる typedef struct _PEB_LDR_DATA { ULONG Length; UCHAR Initialized[4]; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList;//ロード順モジュールリストで最初と最後のモジュールの下記同名Linksにリンク、+0Ch LIST_ENTRY InMemoryOrderModuleList;//同上メモリレイアウト順(≠アドレス順)、+14h LIST_ENTRY InInitializationOrderModuleList;//同上初期化順、+1Ch PVOID EntryInProgress; } PEB_LDR_DATA, *PPEB_LDR_DATA; ▲LDR_DATA_TABLE_ENTRY構造体 この構造体はロードされているモジュールの数だけ存在する 上記PEB_LDR_DATA構造体のInLoadOrderModuleListが、ロード順で最初のモジュールに対応するこの構造体のInLoadOrderLinksにつながる ロード順とメモリレイアウト順での順序はEXE→ntdll.dll→kernel32.dll→その他 初期化順での順序はntdll.dll→KernelBase.dll(Windows 7以降)→kernel32.dll→その他 下記「+**h」は32ビットプロセスでの構造体先頭からの相対位置 64ビットプロセスではアラインメントによりアドレスが格納されるポインタは0x08の倍数のアドレス(QWORD境界)上となる typedef struct _LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks;//ロード順モジュールリストで次・前2つのモジュールの同名Linksにリンク LIST_ENTRY InMemoryOrderLinks;//同上メモリレイアウト順 LIST_ENTRY InInitializationOrderLinks;//同上初期化順 PVOID DllBase;//モジュールの先頭アドレス、ここからPEヘッダを解析してエクスポート関数のアドレスを探す PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName;//モジュールのフルパスを指す、+24h UNICODE_STRING BaseDllName;//モジュール名を指す、+2Ch ULONG Flags; WORD LoadCount; WORD TlsIndex; union { LIST_ENTRY HashLinks; struct { PVOID SectionPointer; ULONG CheckSum; }; }; union { ULONG TimeDateStamp; PVOID LoadedImports; }; _ACTIVATION_CONTEXT * EntryPointActivationContext; PVOID PatchInformation; LIST_ENTRY ForwarderLinks; LIST_ENTRY ServiceTagLinks; LIST_ENTRY StaticLinks; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; ▲UNICODE_STRING構造体 この構造体はUnicode文字列のサイズや格納先アドレスの情報で構成される typedef struct _UNICODE_STRING { USHORT Length;//WORD USHORT MaximumLength;//WORD PWSTR Buffer;//文字列バッファのアドレスを格納したポインタ } UNICODE_STRING, *PUNICODE_STRING;
●独自のDLL
もし、対象プロセスのメインモジュール(EXE)が、独自のDLLをロードして間接的にAPIフック対象のAPI関数をコールするならば、メインモジュールへのIAT書き換え型APIフックでは対応できません。この独自のDLLとは、ソフトウェアのパッケージでEXEファイルに同梱されるDLLや、各種ランタイムを指します。この場合、対象プロセスが使用する、システムDLLを含む全モジュールでIAT書き換えを行うと実行上の不具合を招きかねません。そのため、汎用的なアプローチとしては、APIフック対象API関数のエントリ以降を書き換えるといった、別のAPIフック手法で対処することになります。
●遅延ロード
PEファイルには、指定DLLを最初からではなく、必要に応じてロードしてそのDLLのエクスポート関数を使用するという、特殊な設定を行うことが可能です。この「遅延ロード」の対象DLLは、USER32.dllなどの、kernel32.dll以外のDLLです。遅延ロードの対象DLLから呼び出すAPI関数の情報は、PEファイル内およびそのモジュールでは、PEヘッダ内のディレクトリ設定で通常のIATの情報とは別の情報(Delay import descriptor)として格納されるため、基本的なIAT書き換え型APIフックは使用できません。
PEファイルでの遅延ロードの設定方法には、アドレスの指定方法が異なる新旧の2種類があります。Visual C++ 6.0(1998年リリース)でコンパイルすると旧式となり、それ以降のVisual Studio等でコンパイルすれば新方式の設定方法となります。近年ではプログラム解析ツールにおける遅延ロード対象API関数の一覧表示にあたり、新しい設定方法だけ対応すればよいという向きもあります。ただし、Windows 10といった新しいWindows OSが旧式の遅延ロード設定にも対応しており、また、Visual C++ 6.0はWindows 10にインストール可能(若干の特殊操作が必要)で、Windows 10対応ソフトウェアを問題なくコンパイルできることなどから、新旧両方式の遅延ロード設定に対応したプログラム解析ツールを使用されることをお勧めします。うさみみハリケーンの上記「モジュール別参照関数表示」機能は新旧両方式に対応しています。
ちなみに、Windows 95といった古いWindows OSに対応しているソフトウェアでは、新しいWindows OSで実装されたAPI関数をそのままコールすることはできません。実行可能ファイルがインポートするAPI関数として、実行環境のWindows OSで未実装のAPI関数が指定されていると、その古いWindows OS上では基本的に起動不能になるためです。そこで、新しいWindows OS上で新しいAPI関数をコールするならば、GetProcAddress関数を用いるか、遅延ロードを設定することになります。遅延ロードならば例外の処理として、対象DLLが存在しない場合および、対象DLLにコールする関数が存在しない場合の対処も可能です。
関連して、Visual C++の新しいバージョンでは、コンパイルされた実行可能ファイルは、自動的にIsDebuggerPresent関数といった新しく実装されたAPI関数をインポートする仕様になります。そのため、新しいAPI関数のインポートを特殊な方法で回避して、古いWindows OSでも動作可能にするアプローチが考案され、下記ページにて、Visual C++2005/2008/2010でコンパイルしたプログラムをWindows 95/98/Me/2000で動くようにするためのヘッダファイルが配布されています。
MS11-025 で98/Me/NT/2000でソフトが動かなくなる件 その2
●関数フォワーダ
Windows OSには、モジュールのIATに格納される一部の関数アドレスが、Windows OSによって自動的にネイティブAPIの関数アドレスに変更されるといった、「関数フォワーダ」という仕組みがあります。例として、IATでのHeapAlloc関数(kernel32.dll)のアドレスはRtlAllocateHeap関数(ntdll.dll)のアドレスに変更されます。
この場合、モジュールのインポート情報として、IATに格納される関数アドレスと、それに対応するImport Name Table (これは通称、以下INT)内のAPI関数名が一致しません。これにより、パッカー対策などでIATを検索し、格納アドレスからAPI関数名を解析して目的のAPI関数のアドレスか検証するタイプのAPIフックでは、目的のAPI関数のアドレスが見つからない可能性が生じます。ただし、一般的な、指定したAPI関数名を元にINTを参照しIAT内の対象関数アドレス格納箇所を探索するIAT書き換え型APIフックならば、そのような問題はありません。
関数フォワーダは上記のようなWindows OSにおけるネイティブAPIへのリダイレクトだけではなく、一般的なアプリケーションが使用する独自のDLLでも使用可能です。この場合、Visual C++といった関数フォワーダに対応したコンパイラが必須となります。一般的なアプリケーションでは、関数フォワーダはダミーのDLLを用いた互換性の維持などに活用されるケースがあります。なお、関数フォワーダを施した独自のDLLをLoadLibrary関数で動的にロードすれば、インポート関数の隠蔽に近い状況となるため、 プログラム解析ツール等がセキュリティソフトからマルウェアと誤検出されることの回避や、一種の解析防止策としての用途も想定されます。しかし、実際には、これらの用途で誤検出回避や解析防止が実現可能なケースはかなり限定的だといえます。
プロセスにおける各モジュールの関数フォワーダの適用状況は、上記うさみみハリケーンのIAT検索機能で確認可能です。
<参考>
Visual C++でのDLLのエクスポート関数に対する関数フォワーダ設定例
ソースコード内で指定
#pragma comment(linker, "/export:UpdateDllSetting_AsDummy=Kernel32.IsDebuggerPresent")
あるいは、より確実な方法としてDEFファイルの「EXPORTS」以下で指定
UpdateDllSetting =Kernel32.IsDebuggerPresent
<関数フォワーダについての参考文献>
Advanced Windows 下
●パッカー
通常、パッカーが適用された実行可能ファイルは、本来のIATが取得できなくなります。この場合、IAT書き換え型APIフックでのアプローチは困難です。上記うさみみハリケーンのIAT検索機能で、プロセスメモリ上から本来のIATを探し出すこともできますが、パッカーによっては本来のIATの格納値を変更するため、必ずしも対処できるわけではありません。また、本来のINT内のAPI関数名文字列を消去してしまうパッカーもあります。実行可能ファイルの解析ではなくAPI関数コール時の挙動変更が目的ならば、アンパックでIAT再構築を図るよりも、関数エントリ書き換え型のAPIフックなどで対処した方が簡便といえます。なお、プログラミング上、自分がアンパックされたことを検出して挙動を変更、あるいは悪意のある操作を行うことも可能ですので注意が必要です。
ちなみに、パッカーが適用された実行可能ファイルでの、新しいIATでは、たいていGetProcAddress関数とLoadLibrary関数、そして少数のAPI関数のアドレスが格納されることになります。
余談ですが、書き換えた関数エントリ直後がコール命令あるいはジャンプ命令だと、簡単にAPIフックを検出されてしまいます。そのためかつて、プログラム解析・改造ツールが行うAPIフックとしては、関数エントリ直後にはMOV命令やPUSH命令その他ありがちな命令を擬装用に配置し、その後のニーモニックでコール命令またはジャンプ命令でフック用のプロシージャに飛ばして、擬装用命令に対応するレジスタ等復旧と、フック時の処理、そして本来のエントリ直後のニーモニックの復元・実行を行ってから、本来のAPI関数の処理(コール・ジャンプ元の次のニーモニック)に戻すのが理想的とされていました。これには簡易アセンブラと逆アセンブラの実装が必要でした。しかし、関数エントリ直後をジャンプ命令で書き換えるAPIフックは、一部のユーティリティでも行っており、また、Windows 8以降ではOSの処理として関数エントリ直後が書き換えられることもあります。そのため現在では、各種ソフトウェアの解析対策として、関数エントリ直後のジャンプ命令等を検出したら即プロセスを終了させるといった対抗策が施されるケースは、少なくなっていると考えられます。
●IAT内の重複
古いバージョンの「Delphi」といった一部のコンパイラでコンパイルされた実行可能ファイルでは、IATにおいて、インポートするAPI関数の指定が重複することがあります。つまり、1つのAPI関数の「関数名とインポートアドレスの指定」が2箇所以上で行われるわけです。この場合、IAT書き換え型APIフックを用いる既存のユーティリティ等で、フックするAPI関数についてIAT内で最初に見つかった箇所だけ書き換えるタイプならば、APIフックが不十分で正常に機能しない可能性が生じます。
IAT書き換え型APIフックのソースコードはネットで多数公開されていますが、IAT内で対象API関数が1度見つかれば探索を終了してしまう仕様のものもありますので、特に汎用ツールのコーディングで参考とする際には注意が必要です。
●AppContainer
Windows 8以降では、「AppContainer」と呼ばれるセキュリティ機能により、Windowsストアアプリといった一部のアプリケーションは、隔離された特殊な環境で実行されます。この場合、基本的に、対象プロセスのプロセスメモリ上にAPIフック用のDLLを読み込ませることができません(プロセスメモリの読み書きは可能)。Windowsストアアプリ等にAPIフックが必要となるケースは少ないと考えられますが、このようなセキュリティ上の制約があることに留意してください。
特定プロセスにおけるAppContainer等セキュリティ機能の適用状況は、うさみみハリケーンのメニューで[ファイル]→[プロセスの各種情報を表示]からカテゴリ「実行状況」選択で参照できます。うさみみハリケーン起動時に表示されるプロセスリスト上で、右クリックからポップアップメニューの[プロセスの各種情報を表示]でも同機能を呼び出します。
なお、PEヘッダ内にAppContainer適用対象を示すフラグが格納されるものの、ASLRの無効化(参照)とは異なり、同フラグをクリアすれば実行に不具合が生じます。
●Microsoft Edge
Microsoftのブラウザ「Edge」は、そのプロセスメモリ上で、MicrosoftあるいはWHQL(Microsoftが運営する認証組織)のデジタル署名が無いDLLはロードできない仕様になりました。これはブラウザの安全性向上のための措置です。また、Edge本体は上記AppContainerの適用対象でもあります。これらにより、何らかの用途でブラウザに対してDLLインジェクションを伴うAPIフックを行う場合は、Edge以外のブラウザを対象にする必要があります。
●apphelp.dll
このシステムDLLのバージョン情報内「ファイルの説明」は、「アプリケーションの互換性クライアント ライブラリ」です。Windows 8以降で、EXEファイルPEヘッダの項目MajorOperatingSystemVersionが値4(Windows 95/98/Me)あるいはその他の要因により、IATに格納されるMessageBoxA関数などの一部のAPI関数のアドレスが、プロセスメモリ上にロードされたapphelp.dll内のプログラムコードのアドレスに書き換えられることがあります。この書き換えられたアドレスはapphelp.dllのエクスポート関数のアドレスでもないため、解析がやや煩雑になります。実際の例としては、うさみみハリケーン同梱の「UsaTest2Win.exe」で上記IAT内格納アドレスの書き換えが生じます(Windows 8/10の32/64bit版で確認、書き換えが生じない状況もある)。なお、このケースでは、例えばプログラムからGetProcAddress関数をコールしてMessageBoxA関数のアドレスを取得しても、その戻り値にはapphelp.dll内のアドレスが返されます。
複数の該当例で検証した限りでは、IAT内の、apphelp.dll内アドレスへ書き換えられたアドレスを、元のMessageBoxA関数などのアドレスに書き換えても、プログラム実行上の問題は生じないようです。そのため、現状では、基本的に通常のIAT書き換え型APIフックも使用可能とみられます。ただし、このapphelp.dll内アドレスへの書き換えは互換性維持が目的と推測されるため、将来的にはIAT書き換え型APIフック適用で不具合が生じる要因となり得ます。また、Windows OSでの将来の互換性維持を勘案すれば、いずれは上記MajorOperatingSystemVersionが5(Windows 2000/XP)や6(Windows Vista/7/8)でも、apphelp.dllあるいはまったく別の仕組みにより、IAT内格納アドレスが書き換えられることも予想されます。
追記:Windows 10 では2018年10月のアップデートで、上記MajorOperatingSystemVersionが5でもapphelp.dllを用いるIAT書き換えが行われるようになりました。ただし、状況によりこのIAT書き換えが行われないこともあります。
このapphelp.dllが用いられる自動的なIAT書き換えは、Windowsの「互換モード(で実行)」といったアプリケーション互換性修正プログラムの適用形態の一つといえます。WindowsにおけるIAT書き換えを用いた互換性問題解決の仕組みは「shim (Shim Infrastructure)」と呼ばれています。shimの考察に当たっては、適当な実行可能ファイルに互換モード(Windows XP等)を適用して起動し、そのIATがどのように変化するか観察されることをお勧めします。また、shimの適用に関する情報は、各プロセスのPEB先頭+1E8h(32ビットプロセスの場合)にあるポインタ項目「pShimData」から参照可能です。PEB内pShimDataの前後には互換性に関連する他の項目もあります。ちなみに、Microsoftはshimを一般企業向けの互換性に関する特殊な状況のソリューションとも位置づけており、その説明によれば、たとえばIT関連企業などが、オリジナルの開発元は倒産した特定アプリケーションへ独自のshim(カスタムshimデータベース)を適用するといった状況が想定されています。
●仮想的なDLLおよびAPI関数リダイレクトのスキーマ
例えば、Windows 7以降において、ADVAPI32.dllといったシステムDLLのインポート関数をフックするならば、対象API関数によっては本来の「kernel32.dll」などではなく「API-MS-Win-Core-ProcessThreads-L1-1-0.dll」といった仮想的なDLL(ApiSet Stub DLL)がインポート元になっているケースがあります。この場合、「kernel32.dllが提供するCreateProcessAsUserW関数」というように、指定したDLL名とAPI関数名を元にINTを参照しIAT内での対象関数アドレス格納箇所を探索するという、従来のアプローチに支障が生じます。そのため、IATの格納アドレスからAPI関数名を解析するといったアプローチが有用となります。また、この仮想的なDLLの仕組みを補助するapisetschema.dllの、「.apiset」セクションすなわちスキーマをあらかじめ解析しておくという方法も考えられます。なお、ここでのスキーマ(schema)とは、「仮想的なDLLと本来のDLLの関係情報を格納したデータベース」という意味です。
上記「.apiset」セクションのコピーにあたる、各プロセス上での仮想的なDLLを用いたAPI関数のリダイレクトに関する情報(スキーマ)は、Windows 7以降ではPEBの先頭から+38h/+68h(32bit/64bit)にあるポインタ項目「ApiSetMap」から参照可能です。ただし、この情報の仕様はWindows 7→8.1→10で若干の変更が行われてきており(最新Windows SDK等付属のapiset.h参照)、将来的な仕様変更も想定されます。そのため、スキーマの解析により仮想的なDLL名からリダイレクト先のDLL名を取得するよりも、より簡便な方法として、そのプロセスでのIATの格納アドレスを元に該当モジュール名を解析することをお勧めします。上記うさみみハリケーンのIAT検索機能で、仮想のDLLではなく「kernel32.dll」といった本当のインポート元のモジュール名を表示可能です。
<参考>
上記API関数リダイレクト等の解説
なぜWindows 7のカーネルはVistaより軽量化できたのか?
●その他のAPIフック阻害要因
たいていの場合、APIフック時にはAPIフック用DLLを対象プロセスにロードさせる、つまりDLLインジェクションを行います。そのため、マルウェアなどの対象プロセスでは、自分のプロセスメモリ上にロードされた全モジュールを列挙することで、既知のAPIフック用DLLの存在を検出し、検出時にプロセスを終了させたり、マルウェアとしての動作を行わないことも可能です。モジュールの列挙には、EnumProcessModules関数やModule32Next関数およびネイティブAPIのNtQueryVirtualMemory関数が使用可能であり、また、上述のPEBからモジュールリストを辿る方法もあります。モジュールを列挙せず、特定モジュール名でGetModuleHandle関数をコールしてもその検出が可能です。このような、モジュール名によるAPIフック用DLLの検出に対処するため、APIフックを行う汎用ツール側では、対象プロセスにロードさせるDLLの名前を任意に変更可能にすることが望ましいといえます。
<参考>
APIフックに用いられるプロセス注入用DLLを検出したい
プログラム解析ツールの自作に興味のある方は、特定プロセスの使用モジュールを列挙して各モジュールのアドレスを取得、そして各モジュール毎にインポート・エクスポート関数の名前とアドレスを表示するツールを試作し、さらにモジュールの依存関係の仕組みを考察されることをお勧めします。この試作と考察の経験は、いずれデバッガやプロセスメモリエディタおよび、APIフックを用いる各種プログラム解析ツールを自作する上で、大いに役立ちます。
●補足:汎用ツールにおけるIAT書き換えの代替アプローチ
IAT書き換え型APIフックは、プロセスメモリ上のIATを動的に書き換えるという仕様上、確実性や安定性が求められる汎用のアプローチとしては不向きなシーンもあります。そのため、近年のAPIフックを行う各種汎用ツールにおいては、代替アプローチのひとつである、「仲介DLL」を用いるケースが散見されます。例として、主にPCゲームのウィンドウ化や各種挙動改善を行うツール『Window Mode Patch for Game』では、対象実行可能ファイルに「個人使用における利便性向上が目的といえる」パッチをあてて、仲介DLLを経由してAPI関数を呼び出させることで各種機能を実現しています。
64ビット環境で.NETアプリケーションが解析できません
●概要と注意点.NET Framework対応アプリケーション(以下.NETアプリケーション)のコンパイル時には、対象とする実行環境(プラットフォームターゲット)を指定可能です。その指定内容としては「Any CPU」、「x86」および「x64」などがあります。このうち「Any CPU」が指定されたならば、コンパイルされた実行可能ファイル(PE32)は、32ビットWindows OS上では32ビットのプロセスとして実行され、64ビットWindows OS上では64ビットのプロセスとして実行されます。
そのため、上記「Any CPU」が指定された.NETアプリケーションの場合、64ビットOS上で実行されるプロセスは、32ビットプロセスのみを対象とする既存のプロセス解析ツールでは解析や操作ができなくなります。
この状況に対処するためには、対象となる.NETアプリケーションの実行可能ファイル内にある、32ビットのプロセスとして実行するためのフラグを書き換えて有効化します。これにより、OSが32ビットか64ビットかに関わらず常に32ビットのプロセスとして実行されるようになります。
なお、上記のフラグ書き換えは、対象となる.NETアプリケーションの実行可能ファイルが「Any CPU」指定の場合に有効なアプローチです。64ビットのCPUアーキテクチャである「x64」や「Itanium」指定のものはPEヘッダが64ビットの形式(PE32+)となっており、このフラグ書き換えでは、32ビットのプロセスとして実行できるようにはなりません(STATUS_INVALID_IMAGE_FORMAT:定義値0xC000007Bのエラーで起動不可)。ただし、Windows OSが32ビット版もリリースされる間は、特殊な事情がない限り、一般的な.NETアプリケーションで64ビット版のみ提供するというケースは少ないと考えられます。
ちなみに、.NETアプリケーションのEXEファイルやDLLファイルなどは、「アセンブリ」と呼ばれます。この呼称をアセンブリ言語と混同しないでください。また、.NET Frameworkの共通言語ランタイム(CLR)を使用する.NETアプリケーションのプログラムコードは「マネージドコード」、.NETアプリケーションではない場合のプログラムコードは「アンマネージドコード」と呼ばれることがあります。
●アプローチ詳細
.NETアプリケーションの実行可能ファイル内にある「Any CPU」等の設定情報(IMAGE_COR20_HEADER構造体)は、PEヘッダのディレクトリ項目「COM Runtime descriptor」が指し示すアドレスを、アドレス(RVA)→オフセット変換した結果のオフセット以降にあります。『うさみみハリケーン』に同梱しているPEダンパー兼PEエディタ『UMPE』を使用すれば、PEエディタの[ディレクトリ]画面で「COM Runtime descriptor」アドレス確認と、[補助ツール]画面でアドレス(RVA)→オフセット変換を簡単に行うことができます。さらに得られたオフセット以降をUMPE内蔵のバイナリエディタで表示すれば、左記オフセット+10hにある、書き換え対象となる32ビット実行関連フラグの状況を視認可能です。このDWORDのフラグ値を、0x00000002との論理和演算結果に書き換えることで、「Any CPU」が指定された.NETアプリケーションを、64ビットOS上でも32ビットのプロセスとして実行可能になります。たとえば元の値が0x00000001ならば0x00000003に書き換えることになります。
この実行関連フラグの指定要素と定義値には以下のものがあります(Windows SDK等付属のCorHdr.h参照)。
COMIMAGE_FLAGS_ILONLY = 0x00000001 COMIMAGE_FLAGS_32BITREQUIRED = 0x00000002 //この要素を論理和で付加する COMIMAGE_FLAGS_IL_LIBRARY = 0x00000004 COMIMAGE_FLAGS_STRONGNAMESIGNED = 0x00000008 COMIMAGE_FLAGS_NATIVE_ENTRYPOINT = 0x00000010 COMIMAGE_FLAGS_TRACKDEBUGDATA = 0x00010000 COMIMAGE_FLAGS_ISIBCOPTIMIZED = 0x00020000ただし、対象実行可能ファイルが厳密名(StrongName)で署名されているならば、基本的に上記のフラグ書き換えは行えません。この場合、改変済みの対象実行可能ファイルは、起動時の.NET Frameworkのセキュリティチェック段階で改ざんされたものと認識されるためです。「厳密名」とは、かなりおおまかに言えば、実行可能ファイルの識別情報の集合体であり、その識別情報にはデジタル署名も含まれます。
●代替アプローチ
上記の32ビット実行関連フラグは、Visual StudioやWindows SDKに付属するツールの、「CorFlags.exe」でも変更可能です。CorFlags.exeにコマンドラインオプションとして対象実行可能ファイルのパスと「/32BIT+」を指定することで、32ビット実行関連フラグが有効になります。なお、このツールにおいても、実行環境に関わらず32ビットプロセスとして起動可能に変更できるのは、「Any CPU」が指定されているケースに限定されます。「x64」や「Itanium」が指定されているならば、32ビット化はできません。
CorFlags.exeのインストール先の例は以下。
C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\CorFlags.exe
他には、対象実行可能ファイルをILSpy等を用いて逆コンパイルしてから、対象とする実行環境を「x86」に指定して再コンパイルするというアプローチも想定されます。
ちなみに、リバースエンジニアの方々には、表題のような実行環境に依存する問題に対処すべく、昨今主流の64ビットOSだけではなく、32ビットOSの解析用実行環境も用意している方が多いと考えられます。実行環境が原因といった、解析以前の問題に余計な工数を割くのは、避けるのが望ましいといえます。
●補足:アセンブリ関連
.NETアプリケーションならば、実行可能ファイルのPEヘッダ内ディレクトリ項目「COM Runtime descriptor」が使用されることから、この項目のアドレスとサイズが有効である(ゼロではない)ことを、.NETアプリケーションの実行可能ファイルか否かの判定に使用可能です。この判別手法ならば、対象実行可能ファイルのインポート関数提供元、あるいはプロセスメモリ上のモジュールに、.NET Frameworkの実行環境を構成するmscoree.dllが存在するかを検証する判別手法よりも、簡単かつ確実に判別することが可能です。なお、mscoree.dllのエクスポート関数は同DLLを静的または動的にリンクした上で、アンマネージドコードから使用することもできます(注:.NET Framework 4以降では使用不可または非推奨のエクスポート関数あり)。
プログラミング初心者の方は、自分が学んでいる開発言語で、実行可能ファイルを指定して.NETアプリケーションの実行可能ファイルか判別するツールを試作してみると、プログラム解析の勉強になります。この際、PEファイル内にあるセクションの仕組みを考察し、アドレス(RVA)→オフセット変換機能を、ネット上で容易に入手可能なソースコードに依存せず自分で考えて実装すれば、PEファイルの構造への理解が深まります。
対象.NETアプリケーションの実行可能ファイル内にある、参照するアセンブリなどの各種情報が格納された「メタデータ」と呼ばれるデータ領域を解析するプログラムコードは、必ずしもマネージドコードである必要はありません。COMを使って、CoCreateInstance関数呼び出しの第1引数に「CLSID_CorMetaDataDispenser」を指定するというアプローチにより、たとえばC++言語のプログラムコードからでも、対象.NETアプリケーションの各種情報を取得可能です。この際、IMetaDataAssemblyImportインターフェイスを用いれば、依存している別のアセンブリの一覧が取得できます。また、IMetaDataImportインターフェイスを用いれば、C#言語ソースコード上で「[DllImport("Neko.dll")]」という形で指定した、使用するアンマネージドコードのDLLの一覧を取得可能です。
<参考>
メタデータ API / アセンブリの情報
ただし、これらは静的なアプローチであり、たとえば.NETアプリケーションのプロセスがロードしているアセンブリの情報を動的に取得するためには、対象プロセスが使用するモジュール一覧のPSAPI/PSAPI Version2による取得や、対象プロセスのランタイム関連イベントの取得(イベントトレース:ETW)で対処することになります。
なお、.NETアプリケーションが参照する主要なアセンブリ(DLL)は、Windows付属のエクスプローラ上では「%windir%assembly」(通常はC:\Windows\assembly\)直下にあるように表示されますが、『うさみみハリケーン』などで表示可能な、プロセスが使用するモジュールの一覧上では、「C:\Windows\assembly\NativeImages_v2.0.50727_64\...」といった正しいパスが表示されます。
上記CorFlags.exeと同じフォルダには、.NETアプリケーション用の中間言語逆アセンブラ「ildasm.exe」もあります。また、.NET Frameworkの2.0や4.0には中間言語アセンブラの「ilasm.exe」が付属しており、これらのツールで試行錯誤してみることは、.NETアプリケーションのより深い考察に繋がるとみられます。
ilasm.exeのインストール先の例は以下。
C:\Windows\Microsoft.NET\Framework\v4.0.30319\ilasm.exe
●補足:プロセスメモリ関連
.NETアプリケーションのプロセスメモリ上では、プログラムが使用する変数がstatic指定でグローバル変数のようなものであっても、その変数のアドレスはメインモジュール(EXE)のモジュールエリア内にはならないこと、さらにその変数が格納されたメモリ領域は、アクセス属性が読み書き属性だけではなく読み書き実行属性になることもあります。これらの特徴を踏まえ、『うさみみハリケーン』での検索機能で、検索を効率化させるための、検索対象アドレス範囲の指定や、検索対象メモリ領域のアクセス属性の指定を行う際には、指定内容が絞り込み過ぎにならないよう注意してください。
ポインタを解析したい
●概要以下の解説は、主にPCゲームやアプリケーションでの、特定データ格納に関連するポインタの解析について述べています。静的プログラム解析の一手法である、プログラム上の問題点を探るといった目的のポインタ解析(シェープ解析等)とは異なります。
PCゲームのパラメータ格納アドレスなどを調べるにあたり、そのアドレスがプログラムの処理としてポインタを用いて管理されていることを解析するのは、この種の解析の重要な要素といえます。ポインタの解析により、対象アドレスが固定ではなく、メモリ動的確保に伴い変動する実行環境に依存のものであっても、対象アドレスを容易に特定することが可能になります。
参考:「環境依存型変動アドレス」について教えてください
メモリ動的確保に伴う環境依存アドレスの例については、『うさみみハリケーン』に同梱している、プロセスメモリ検索練習用の UsaTest2.exe や UsaTest2_x64.exe を参照してください。これらの例では、動的に確保されたメモリブロックの先頭アドレスの数値が格納された固定アドレスがあり、これを特定アドレスを指し示す「ポインタ」として、ポインタに格納されたアドレスに、相対距離を意味する定数すなわち「オフセット」を足すことで、パラメータのアドレスを管理しています。パラメータのアドレスは動的確保されたメモリブロック先頭+1000hという形になります。これは、プロセスメモリ上で動的に確保された、多種多様なデータを格納するためのメモリブロックの中で、目的のデータは先頭から0x1000バイトの相対位置にあるという意味です。
実際の解析においては、ポインタ1つだけで対象アドレスを管理しているケースは少なく、たいてい複数のポインタが連なる「多重ポインタ」で管理されています。つまり、ポインタの格納値(アドレス)に相対距離を意味する定数(オフセット)を足した数値が次のポインタのアドレスとなり、さらにそのポインタの格納値に定数を足して次のポインタという風に、ポインタが連結している状態となっています。これにより、ポインタ解析のゴールは、「基点アドレス(最初のポインタ); オフセット; オフセット; オフセット…」というポインタとオフセットのセットを導き出すことになります。このポインタとオフセットのセットが最終的に示すアドレスが、すでに判明しているパラメータ等のアドレスとなれば良い訳であり、ポインタを機械的に検索する場合は、そのすでに判明しているパラメータ等のアドレスを手がかりにして、そこからポインタ+オフセットの可能性がある組み合わせを逆に辿っていくことになります。
なお、現在のWindows OS上では、ASLRによるEXEモジュールのアドレス変動がありうるため、必要に応じて解析工数低減のために、ASLR無効化を行っておくことをお勧めします。
●ポインタ使用例の逆アセンブルコードリスト
多重ポインタがプログラムの処理としてどのように扱われているか示すために、簡易モデルを組んでコンパイルしてみました。以下の逆アセンブルコードリストでは、EXEモジュールエリア内にある「EXEモジュール先頭+xxxxxxh」で指定可能な固定アドレス(ASLRの影響あり)を基点アドレスとなる最初のポインタとして、そのポインタの格納値に0xC0を加算して次のポインタのアドレスとし、その2番目のポインタの格納値に0x140を加算して次のポインタ...と繰り返し、最終的にパラメータのアドレスに辿り着いてから加算処理を行います。2番目以降のポインタはすべて動的に確保したメモリブロック内を示すものであり、ポインタ自体も動的に確保されたメモリブロック内にあるため、固定アドレスで表示することはできません。以下の例で言えば、ポインタの解析は、「111A1E8; 0xC0; 0x140; 0x40; 0x160; 0x10」というポインタとオフセットのセットを導き出すことを意味します。
MOV EAX,[111A1E8] ;EXEモジュール先頭+16A1E8hのアドレスの格納値をEAXレジスタへ転送 PUSH ESI MOV ESI,ECX MOV ECX,[EAX+C0] ;「EAXレジスタ(アドレス111A1E8の格納値)+0xC0」で得られるアドレスの格納値をECXレジスタへ転送 MOV EDX,[ECX+140] ;「ECXレジスタの値+0x140」で得られるアドレスの格納値をEDXレジスタへ転送 MOV EAX,[EDX+40] MOV EAX,[EAX+160] ADD DWORD PTR [EAX+10],64 ;パラメータのアドレス(これを範囲検索等で解析しブレークポイントを設定する)の格納値に100加算
●ポインタ解析のアプローチ
主なポインタ解析のアプローチとしては、デバッガでブレークポイントを使う方法と、既存あるいは自作PCゲーム解析ツールに実装されたポインタ検索機能を使う方法があります。このポインタ検索の基本処理は、範囲検索などですでに判明しているパラメータなどのアドレスを基に、プロセスメモリ上でポインタとオフセットのセット(可能組み合わせ)を抽出する再帰処理等を使って、基点となるモジュールエリア内アドレスまで辿っていくことになります。基点となるのはEXEモジュールエリア内に限らず、対象PCゲームやアプリケーション独自のDLL等のモジュールエリア内となることもあります。
日本においては、ポインタの解析はデバッガで、判明済みパラメータ等のアドレスにブレークポイントを設定してブレークさせ、逆アセンブルコードリストやステップ実行などから得られる情報で解析していくというアプローチが一般的に使用されてきました。この手法ならば、使用ポインタ数が多い多重ポインタでも比較的容易に解析可能です。さらに、ポインタと、対象PCゲームの独自モジュールや関数テーブル、連想配列や各種リスト構造、あるいは特定グローバル変数などとの関連性を解き明かしていけば、独自モジュールエリア内アドレス等をベースとした最小限のポインタとオフセットのセットで、最終的な対象アドレスへの到達が可能になります。なお、パラメータ等の変更処理が汎用のルーチンで行われている場合は、同ルーチン内でのブレーク後の解析とプログラム改変がやや煩雑になります。上記により、プログラム解析の練習およびスキル向上という面では、デバッガを用いるアプローチが適しているといえます。
日本でデバッガを用いたアプローチが重視されてきた理由として、多重ポインタに関する事情が特殊という背景が挙げられます。日本のPCゲーム(アダルトゲームや同人ゲームを含む)においては、ゲーム制作ツール(ゲームエンジン)である独自のスクリプトエンジンが頻用されるといった理由により、パラメータ等の管理で5重~8重ポインタが使用されているケースも珍しくありません。筆者が知る限りでは13重ポインタのケースもありましたが、当然これ以上の多重ポインタが使用されているケースもあると考えられます。このような使用ポインタ数が多い複雑な多重ポインタに対するポインタ検索は、絶対不可能とまでは言えないものの、プログラムの処理から見て正しいであろうポインタとオフセットのセットを得るためには、工数が極端に嵩む等の問題があります。
海外のPCゲーム解析においては、ポインタ検索を重視する傾向が見受けられます。ちなみに、海外のPCゲーム解析ツールの古いバージョンとOllyDbgのバージョン1.x は、デバッガの仕様を巧妙に利用する一部のデバッガ対策が用いられた場合、対象アドレスに読み/書きアクセスのブレークポイントを設定した状態でもブレークできなくなるという問題があり、これが海外でのPCゲーム解析でポインタ検索を重要視してきた一因と考えられます。なお、うさみみハリケーンといった日本のプロセスメモリエディタ兼デバッガと、OllyDbgのバージョン2.x はこの種のデバッガ対策へ対処できるようコーディングされており、ブレークポイントの無効化を自動で回避します。
もし、何らかの原因で、デバッガを用いる方法もポインタ検索を用いる方法も使用できない場合は、対象プロセスのプロセスメモリ上で検索可能な特定バイト列や特定文字列などを基点として、その相対位置でポインタあるいはパラメータ等対象データそのもののアドレスを特定できないか、プロセスメモリエディタの検索機能を活用して調べてみてください。
ちなみに、将来的には、デバッガでブレークしたアドレス周辺の逆アセンブルコードリスト(要中間コード化)や対象プロセスのメモリマップを基に、シンボリック実行のようなアプローチを用いる、ポインタ解析の半自動化が進むと予想されます。
●参考:ポインタ検索機能の実装にかかる注意点
筆者はポインタ検索機能を自作解析ツールに実装した経験がありますので、参考までに実装上の注意点について述べておきます。
ポインタとオフセットのセットの検索は、対象プロセスのプロセスメモリ上で格納値+上限値内オフセットが対象アドレスとなるアドレスを、DWORD/QWORD境界ごとに格納値を読み込んで探し出します。これは再帰処理で行う方法や、ポインタの深さ(2重・3重等)に応じてポインタとオフセットの候補をリストアップし、その全候補に対してさらにポインタとオフセットの候補をリストアップして次の深さへと繰り返す方法があります。ポインタとオフセットの候補は各種コンテナあるいは構造体の配列に格納します。
解析対象にもよりますが、複雑な多重ポインタならば、最終的な候補となるポインタとオフセットのセットは膨大な数になります。検索後に時間をおいてから、ポインタとオフセットの候補の正当性を判断する絞り込み検索を行っても、候補数が数千を下らないこともあります。
このため、プロセスメモリ上でDWORD/QWORD境界ごとに格納値を調べてポインタである可能性を判断する際には、不適切なセットをふるい落とし検索の効率化を図るために、検索中に色々な条件でポインタの正当性を判断します。さらに多重ポインタはポインタの数が増えるごとにポインタ検索結果、すなわちポインタとオフセットのセットの候補が等比級数的に増えていくことから、ポインタ検索処理はマルチスレッド化といった高速化を図り、処理に極端に時間がかかる事態をなるべく回避します。
ポインタの正当性を判断する条件としてよく挙げられるのは、多重ポインタで使用されるポインタはヒープ領域内になる(はず)という条件です。しかし実際には、ポインタの配置はすべてヒープ領域内になるとは限りません。HeapAlloc関数使用時であっても、メモリ動的確保サイズ等により、Windows OSがヒープを使うかコミットしたページ領域を使うかを決めるためです。さらに、新しいWindows OS上では、ToolHelp APIによって取得できる対象プロセスのヒープ関連情報は不正確なこともありますので、ヒープ領域内か否かという条件とその判別は過信しないでください。また、各ポインタのアドレスがあるメモリエリア(ページ領域)のアクセス属性は、読み書き属性だけではなく、読み書き実行属性となる可能性も考慮した方が良いといえます。加えて、ポインタのアドレスはDWORD/QWORD境界上にあるという前提でほぼ問題ないのですが、例外はありえます。動的に確保されたメモリブロック内のポインタのアドレスは、EXEモジュールの先頭アドレスより前になることもありますので、ポインタ検索時のプロセスメモリ検索対象アドレス範囲の先頭を、EXEモジュールの先頭アドレス以降にしないでください。なお、対象プロセス特有のモジュール(EXEやDLLおよび動的生成PEファイル)のモジュールエリアアドレス範囲は、PSAPIなどを使ってポインタ検索前に調べておきます。
オフセットの正当性に関しては、オフセットはデータのアライメントに伴いDWORDの倍数となる可能性が高いといえます。また、オフセットはゼロになるときもあります。データ構造上の理由により、たいていオフセットは正の値になります。通常、ポインタ検索で設定するオフセットの上限値は0x200バイト程度で十分といわれていますが、実際のオフセットは0xA8000バイトといった大きな値になることもあります。なお、32ビット・64ビットのプロセスともに、オフセットがDWORDの範囲を越えることはまず考えられません。
実際には、ポインタ検索で「簡単に」適切なポインタとオフセットのセットが判明するのは、多重ポインタでも使用ポインタ数が少ないものに限られるといえます。使用ポインタ数が多ければ、ポインタを辿っていく過程で、メモリアクセス属性が読み書き可能か判別したり、オフセットの上限値を設定や、最初のポインタがモジュールエリア内か判別といった色々なふるいわけを施しても、目的のポインタとオフセットのセットが簡単に判明することは期待できません。
また、シーンごとといった頻繁なメモリ動的再確保や、構造体のメンバ数可変に伴うメンバの移動などがあれば、ポインタ検索とその絞り込み検索(対象プロセス再起動後の絞り込み検索を含む)は正常には機能しなくなります。なお、パラメータなどが格納されるメモリブロックの動的な再確保は、必ずしも解析・改造対策を意図したものではありません。たとえば、解析・改造対策を施す必要が全くない、Windows XP Embedded を用いた一部のアーケードゲームで、パラメータなどが格納されるメモリブロックをシーンごとに再確保するケースがありました。
日本人向けのプログラム解析ツールでポインタ検索機能を実装すると、日本では複雑な多重ポインタが頻出するという実情から、使用ポインタ数が多い複雑な多重ポインタでもポインタ検索が可能という誤解を招く懸念があります。そのため実装時には、ヘルプかReadMeで、日本ではデバッガを用いたポインタ解析がアプローチとしてよく使用されることや、ポインタ検索は万能ではないということを説明するなど、誤解を招かないための工夫が必要です。さらに、ポインタ検索で導かれたポインタとオフセットのセットは、たとえそれがうまく動作しても、プログラムの処理からみて正しい組み合わせかはよく分からないという、根本的な問題についても説明しておきましょう。
ポインタ検索の実装はプログラミングやプログラム解析を学ぶ上で勉強にはなるのですが、実装したポインタ検索機能に固執せずに、デバッガを用いたアプローチとの柔軟な使い分けが望ましいといえます。
ちなみに、単純なポインタと1つのオフセットならば、うさみみハリケーンの範囲検索機能(メニューの[検索]→[メモリ範囲を指定して検索(64Bit Mode)])でも解析可能です。この場合、検索条件には「数値範囲」を選択して、判明しているパラメータのアドレスから同アドレス-0x200~-0x1000程度を数値範囲として指定し(例:パラメータのアドレスが0x8B000なら「0x8A000,0x8B000」)、さらに検索対象メモリエリアのアクセス属性には「Commit-ReadWrite」を指定します。この検索によるポインタ解析操作を繰り返して、多重ポインタを手動で辿っていくことも可能です。また、うさみみハリケーンには、多重ポインタの解析を補助する「Address Calculator」プラグインが付属しています(メニュー[プラグイン]→[UHPAdrCalc.dll])。
●ポインタ解析結果の表現と役割
たとえば、うさみみハリケーン用の改造コードならば、多重ポインタを用いたプロセスメモリ書き換え内容の指定は以下のようになります。これは上述のポインタ使用例に対応するものです。このプロセスメモリ書き換え用スクリプトの例では、アドレス111A1E8のダブルワードの格納値に0xC0を加算した値を次のポインタのアドレスとし、そのポインタアドレスのダブルワードの格納値に0x0140を加算してさらに次のポインタ...というように繰り返し、最終的に導かれるアドレスにバイナリデータE703を書き込みます。なお、改造コードで対処できないオフセット可変型など特殊な多重ポインタの場合は、専用プラグインを組んで対処します。
*111A1E8>C0/0140/40/0160/10-E703
このようなプログラム解析・改造ツール独自のスクリプトにより、特にプログラム解析の知識や経験が全く無い方でも、実行環境に依存しない、多重ポインタに対応するプロセスメモリ書き換えといったプログラムの改造を容易に行うことが可能になります。これはプログラム解析の世界に興味を持ってもらうよい体験となり、その先にあるプログラミングあるいは情報セキュリティ関連のプログラム解析へ進むことを促したケースも散見されます。
ただし、取り扱いの容易さを優先し簡単な文字列で表現されたスクリプトは、プログラム解析を学ぶための学習資料としては不十分な面があります。また、スクリプトの形式が各プログラム解析・改造ツール独自のものとなっており、共通のフォーマットが存在しないという問題もあります。このため、ポインタ解析結果の公開にあたっては、既存のスクリプトのフォーマットを使うのとは別に、共通フォーマットとしてのC++など開発言語を用いたプロセスメモリ書き換え処理のソースコード(解説コメント多数)で表すことができれば、プログラム解析の初心者の方々に、プログラム解析とプログラミングの両面で大いに役立つと考えられます。特にポインタを用いたプログラムの処理を理解することは、初歩のプログラム解析やプログラミングにおいて重要なステップの一つですので、その理解に役立つ解説ならば需要も多いとみられます。上記C++ならば開発言語の中でも特にポインタが扱いやすいといえますが、汎用性や分かりやすさを重視した別の解説形態を自分なりに考えてみるのも良いでしょう。
余談ですが、リバースエンジニアである筆者の経験則として、最初にプログラム解析を通してアセンブリ言語ベースで処理の流れを理解する練習を重ねたならば、後々高級言語でのプログラミングを学ぶにあたり、ポインタで悩むことはほとんど無いといえます。
アプリケーションがユーザーインターフェース上に表示する文字列を取得したい
●概要とアプローチ任意のアプリケーションでウィンドウやダイアログ上に表示される文字列の取得や、ボタンの疑似クリックなどの操作は、一部のユーティリティやプログラム解析ツールの有用な機能として重宝されてきました。汎用プロセスメモリエディタ兼デバッガ『うさみみハリケーン』でも、解析対象プロセスの親ウィンドウと、コントロール(ボタンなど)といった子ウィンドウを列挙してからの、各ウィンドウへの文字列取得や詳細情報取得および各種操作等に対応しています。この操作は、メニューの[ファイル/プロセス]→[プロセスの各種情報を表示]からカテゴリ[ウィンドウ]でウィンドウ一覧を表示後、対象ウィンドウを選択し右クリックで表示されるポップアップメニューから行います。同様に、起動時に表示するプロセス一覧上で対象プロセスを選択して、右クリックから[プロセスの各種情報を表示]で行うこともできます。
一般的に、このような親子ウィンドウの列挙には、EnumWindows関数とEnumChildWindows関数が使用されます。しかし現在では、このウィンドウを列挙した上での、API関数呼び出しあるいは「WM_GETTEXT」などWindowsメッセージ送信による情報取得や操作は、適切に機能しないケースも散見されるようになりました。例えば、Windowsに付属する一部のアプリケーションおよび、.NETアプリケーションやMicrosoft以外のコンパイラでコンパイルされたアプリケーションなどでは、子ウィンドウのクラス名や挙動が特殊で、従来の「SysListView32」といった既存の定義済クラス名を基にコントロールの種類を認識して操作することができません。特定種類コントロール専用のWindowsメッセージを送信して、戻り値を基にコントロールの種類を認識可能なケースもありますが、処理が煩雑となります。そもそも、ウィンドウの列挙によるアプローチでは、メニュー項目文字列など、表示されているが取得できない文字列も少なくありません。
このような表示文字列取得に係る問題に対処するため、Windowsに実装されている「Microsoft Active Accessibility(MSAA)」という仕組みを用いた、ウィンドウではなくユーザーインターフェースを構成するオブジェクトを対象とするアプローチが採用されるようになってきました。これにより、ユーザーインターフェース上の多くの表示文字列を取得可能です。このアプローチを実装したアプリケーションの例としては「Textify」などが挙げられます。
なお、現在のWindowsには、各種アプリケーションのユーザーインターフェースを外部から操作可能なAPI群である「UI Automation」が実装されており、こちらもユーザーインターフェース上の文字列の取得に使用可能です。将来的な互換性や拡張性からみれば、こちらが上記「Microsoft Active Accessibility」よりもアプローチとして妥当といえますが、標準的な表示文字列の手動による取得が目的ならば、「Microsoft Active Accessibility」でも特段の問題はありません。また、「Microsoft Active Accessibility」と「UI Automation」ともに、JAVAやQtを用いたアプリケーションなどの、標準的ではない特殊な方法で表示された文字列の取得はできません。『うさみみハリケーン』同梱のボタン型ランチャー「NekoLaunch」では、「UI Automation」を用いた、カーソル直下の文字列を取得する機能を実装しています。
Windows SDKには、「Microsoft Active Accessibility」と「UI Automation」両方に対応した、ユーザーインターフェースの構成要素を調査するツール「Inspect.exe」が付属しています(公式解説)。このツールは、いわゆるスクリーンリーダーの開発や、特定アプリケーション専用の情報取得・自動操作ツールの開発に役立つと見られます。ちなみに、「Inspect.exe」からさらにUI操作自動化用の機能を強化した、ユーザーインターフェイスの構成要素に対する操作内容出力ツール「WinAppDriver UI Recorder」もあります。
アプリケーションが使用する固定文字列の取得であれば、実行ファイルに対して文字列抽出処理を行うか、リソースビューアあるいはリソースエディタで実行ファイルをオープンしてみるというアプローチもあります。任意のバイナリファイルに対する文字コード別の文字列抽出処理は、『うさみみハリケーン』同梱の汎用ファイルアナライザ「青い空を見上げればいつもそこに白い猫」(AoZoraSiroNeko.exe)が対応しています。また、『うさみみハリケーン』には、EXEファイルやDLLファイルなど、実行ファイルのリソースに格納された文字列の取得に特化したリソースビューア(ResStrView_DotNET35.exe/ResStrView_DotNET40.exe)も同梱しています。
ちなみに、ユーザーインターフェース上の文字列の取得については、プログラム解析とは全く異なる、スクリーンショットの画像を作成してOCRに読み取らせるというアプローチもあります。このアプローチは特に、「Microsoft Active Accessibility」と「UI Automation」ともに対応できない、ユーザーインターフェース上に描画された文字列の取得に有用です。現在では、一部のOCRはWebツールとして無償で提供されており、容易に試してみることができます。ただし、OCRは適切な文字認識ができないケースも多々あります。
●Microsoft Active Accessibility による表示文字列取得例
//Visual C++ でのSetTimer関数を用いた取得例(動作確認:Visual C++ 2010) //起動時の記述例 //OleInitialize(NULL);//Visual C++ では、"Oleacc.h"や"Oleacc.lib"の参照指定は不要 //SetTimer(1234, 500, MyTimerProc);//500ミリ秒ごとに以下の関数をコールして文字列取得 //任意のキー押し下げのタイミングで文字列取得ならばRegisterHotKey関数で対処可能 VOID CALLBACK MyTimerProc( HWND hwnd, UINT message, UINT idTimer, DWORD dwTime) { //『うさみみハリケーン』のメニュー[デバッグ]→[デバッグ文字列出力を監視]で出力文字列を取得・ログ化可能 //Ctrlキーが押されていたらカーソル直下の文字列を取得 SHORT iKey = GetAsyncKeyState(VK_CONTROL); if( (iKey & 0x8000) != 0x8000) return; POINT ptCursor; GetCursorPos(&ptCursor); IAccessible* pAcc = NULL; HRESULT hr; VARIANT varChild; hr = AccessibleObjectFromPoint(ptCursor, &pAcc, &varChild); if(hr == S_OK) { BSTR bstrName = NULL; hr = pAcc->get_accName(varChild, &bstrName);//オブジェクトの名前を取得(取得できないケースも多い) if(bstrName && (hr == S_OK)) { OutputDebugString(bstrName); SysFreeString(bstrName); } BSTR bstrName2 = NULL; hr = pAcc->get_accValue(varChild, &bstrName2);//オブジェクトに設定された値や文字列を取得 if(bstrName2 && (hr == S_OK)) { OutputDebugString(bstrName2); SysFreeString(bstrName2); } varChild.vt = VT_I4; varChild.lVal = CHILDID_SELF; VARIANT varResult; DWORD dwRoleID; hr = pAcc->get_accRole(varChild, &varResult);//オブジェクトの種類などの説明文を取得 if((hr == S_OK) && (varResult.vt == VT_I4)) { dwRoleID = varResult.lVal; UINT nRoleLength; nRoleLength = GetRoleText(dwRoleID, NULL, 0); LPTSTR lpszRoleString = (LPTSTR)malloc((nRoleLength +1) * sizeof(TCHAR)); GetRoleText(dwRoleID, lpszRoleString, nRoleLength +1); OutputDebugString(lpszRoleString); free(lpszRoleString); } //リストビュー(最初のカラム)・リストボックス・コンボボックス・タブ内容の項目一括取得は、 //AccessibleChildren関数で子のVARIANT配列を取得して配列要素ごとにget_accNameメソッドで文字列取得 //ソースコード例はMSDNのAccessibleChildren関数欄を参照 //pAccが親オブジェクトになるよう、子オブジェクト上ではない位置にカーソルを配置する必要あり //オブジェクトからウィンドウを特定する例 HWND hWnd; hr = WindowFromAccessibleObject(pAcc, &hWnd); if(hr == S_OK) { wchar_t hwndtxt[32]; wsprintf(hwndtxt, L"hWnd: %08X", (DWORD)hWnd);//32bit OutputDebugString(hwndtxt); } } else if(hr == S_FALSE) { OutputDebugString(L"不正あるいはアクセス不能なオブジェクトが指定されました"); } else if(hr == E_ACCESSDENIED) { OutputDebugString(L"IAccessible オブジェクトへのアクセスを拒否されました"); } else { OutputDebugString(L"その他のエラー発生"); } }
●UI Automation による表示文字列取得例
//Visual C# でのSystem.Timers.Timerを用いた取得例(動作確認:Visual C# 2010) /* 参照設定に追加 UIAutomationClient UIAutomationTypes */ //System.Timers.Timer eventTimer;//事前に記述しておく //public ClientForm()内などでタイマ開始 eventTimer = new System.Timers.Timer(); eventTimer.Elapsed += new ElapsedEventHandler(OnTimerTick); eventTimer.Interval = 250; eventTimer.Enabled = true; //GetAsyncKeyState関数を直接使用するための記述 [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern UInt16 GetAsyncKeyState(int vKey); private void OnTimerTick(object sender, EventArgs e) { //『うさみみハリケーン』のメニュー[デバッグ]→[デバッグ文字列出力を監視]で出力文字列を取得・ログ化可能 //if (System.Windows.Input.Keyboard.IsKeyDown(Key.LeftCtrl)==false)return;//確実ではない //Ctrlキーが押されていたらカーソル直下の文字列を取得 UInt16 iKey = GetAsyncKeyState(0x11);//VK_CONTROL if ((iKey & 0x8000) != 0x8000) return; System.Drawing.Point mouse = System.Windows.Forms.Cursor.Position; AutomationElement element = AutomationElement.FromPoint(new System.Windows.Point(mouse.X, mouse.Y)); if (element != null) { //取得したUI構成要素の名前や値の文字列をデバッグ文字列として出力 object pattern; if (element.TryGetCurrentPattern(ValuePattern.Pattern, out pattern)) { ValuePattern valuePattern = (ValuePattern)pattern; System.Diagnostics.Trace.Write("Name=" + element.Current.Name + ", Value=" + valuePattern.Current.Value); } else { System.Diagnostics.Trace.Write("Name=" + element.Current.Name); } //UI操作自動化用IDの取得例 System.Diagnostics.Trace.Write("ID=" + element.Current.AutomationId.ToString()); } }