リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。
軌跡
環境
OS: Windows10 64bit Home (日本語)
CPU: Intel® Core™ i3-6006U CPU @ 2.00GHz × 1
メモリ: 2048MB
Python: 3.6.5
私の環境は、普段Ubuntu16.04を使っていますが、ここではWindows10 64bitを仮想マシン上で立ち上げております。
ちなみに教科書では、Windowsの32bitで紹介されています。
Intel Deveroper's Manual [1] Chapter18 - Chapter19
現在CPU周りの知識をつけるために『Intel Software Developer's Manual(IDMと呼びます)』をさっくりまとめています。
前回はIDMのChapter12までをまとめました。ここからドーンと飛ばしまして、今回はChapter18 - Chapter19をまとめます。
なお、画像は全てIDMより掲載しています。
CHAPTER 18 INPUT/OUTPUT
IA-32プロセッサは外部メモリへ/からデータを転送でき、入力/出力ポート(I/Oポート)へ/からもデータを転送できます。I/Oポートは、プロセッサ上の制御、データ、アドレスpinをデコードする循環によってシステムハードウェアに作成されます。これらのI/Oポートは周辺機器との通信を可能にします。I/Oポートは入力ポートまたは出力ポート、両方向ポートです。いくつかのI/Oポートはシリアルインターフェイスデバイスの送信レジスタと受信レジスタへの/からのようなデータを送信するために使用されます。他のI/Oポートはディスクコントローラーのレジスタの制御のような、周辺機器の制御に使用されます。
18.1 I/O PORT ADDRESSING
プロセッサは2つの方法でI/Oポートへアクセスするためのアプリケーションを許可します。
- 分離したI/Oアドレススペースによるアクセス
- メモリマップI/Oによるアクセス
I/Oアドレススペースを通したI/OポートアクセスはI/O命令セットと特別なI/O保護メカニズムを通して処理されます。メモリマップI/Oを通したI/Oポートアクセスはセグメンテーションまたはページングを通して提供される保護されたプロセッサの汎用移動命令と汎用文字列命令で処理されます。I/Oポートは、I/Oアドレススペースまたは物理メモリアドレススペース(メモリマップI/O)またはその両方に現れるようにマッピングできます。
I/Oアドレススペースを使用するメリットの1つは、I/Oポートへの書き込みが、命令ストリーム内の次の命令が実行される前に完了することが保証されることです。したがって、システムハードウェアを制御するためのI/O書き込みによって、他の命令が実行される前に新しい状態へセットされます。Section 18.6参照。
18.2 I/O PORT HARDWARE
ハードウェアの観点から、I/Oアドレス指定はプロセッサアドレスラインを通して処理されます。P6ファミリとPentium 4、Intel Xeonプロセッサにおいて、要求コマンドラインはアドレスがメモリアドレスまたはI/Oアドレスで操作されているかどうかを示します。Pentiumプロセッサと以前のIA-32プロセッサにおいては、M/IO#pinがメモリアドレス(1)またはI/Oアドレス(0)を示します。分離されたI/Oアドレススペースを選択した際は、メモリではなくI/Oポートを選択するためにメモリI/Oバストランザクションをデコードするのはハードウェアの責任になります。データはデータラインを通して、プロセッサとI/Oデバイス間で送信されます。
18.3 I/O ADDRESS SPACE
プロセッサのI/Oアドレススペースは分離され、物理メモリアドレススペースとは異なります。I/Oアドレススペースは0-FFFFHまで番号付けされた$2^{16}(64K)$の個々のアドレス可能な8bit I/Oポートで構成されます。0F8H-0FFHのI/Oポートアドレスは予約されています。これらのアドレスにI/Oポートを割り当ててはいけません。FFFFHのI/Oアドレススペース制限を超えてアドレス指定しようとする試みの結果は実装固有です。詳しくは特定のプロセッサのマニュアルを参照してください。
2つの連続した8bitポートは16bitポートとして扱われ、4つの連続したポートに関しては32bitポートとして扱われます。これによって、プロセッサはI/Oアドレススペース内のデバイスへ/から8, 16, 32bitの転送ができます。メモリ内のWord同様に、16bitポートは全ての1つのバスサイクルで転送できるように、偶数アドレス(0, 2, 4, ...)に整列する必要があります。同様に32bitポートでも4の倍数(0, 4, 8, ...)でアドレスを整列すべきです。プロセッサは未整列のデータ転送をサポートしますが、1つ以上の余分なバスサイクルを使用するためパフォーマンスのペナルティが付きます。
未整列ポートでアクセスするために使用されるバスサイクルの正確な順序は未定義で、将来のIA-32プロセッサで同じままであることは保証されません。ハードウェアまたはソフトウェアでI / Oポートを特定の順序で書き込む必要がある場合は、その順序を明示的に指定する必要があります。たとえば、2HのアドレスにWordのI/Oポートを、次に4Hに別のWordポートをロードするには、2Hで単一のDword書き込みではなく、2つのWord書き込みを使用する必要があります。
プロセッサは、I/Oアドレススペースへのバスサイクルのパリティエラーをマスクしないことに注意してください。したがって、I/Oアドレススペースを介してI/Oポートにアクセスすることは、パリティエラーの発生源となり得ます。
18.3.1 Memory-Mapped I/O
メモリコンポーネントのように応答するI/Oデバイスは、プロセッサの物理メモリアドレススペースを通してアクセスできます(図18-1参照)。メモリマップI/Oを使用するとき、メモリを参照するプロセッサの命令のいくつかは物理メモリアドレスに配置されたI/Oポートにアクセスするために使用することが出来ます。例えば、MOV命令はレジスタとメモリマップI/Oポート間でデータを転送することが出来ます。AND命令、OR命令、TEST命令はメモリマップ周辺機器の制御レジスタあるいはステータスレジスタのbitを操作するために使用されるかもしれません。
特定の命令はメモリマップI/Oアドレスへのメモリアクセス(読み込みまたは、書き込み)が完了したあと、例外またはVMが終了するかもしれません。この例外またはVMの終了は命令がマルチメモリアクセス(MOVS, PUSH mem, POP mem, PUSHADなど)の実行しているか、あるいは命令内(タスクスイッチVMの終了を引き起こす#DEまたはCALLをとるDIV memなど)で例外またはVMの終了の順序付けによるものかもしれません。ソフトウェアが後で命令(IRETまたはVMRESUMEの後)を再実行する場合には、MMIO(memory-mapped I/O)アクセスが再び発生する可能性があります。メモリマップI/Oアクセスに副作用がある場合、その副作用はメモリマップI/Oアクセスが発生するたびに実行されるかもしれません。それが問題である場合、ソフトウェアはMMIOのアクセス後にその例外とVMの終了が起きないようにしなければいけません。
メモリマップI/Oを使用する場合、I/O操作のためのマップされたアドレススペースのキャッシュは防止しなければいけません。Pentium 4,Intel Xeon, P6ファミリプロセッサでI/Oアクセスキャッシュはuncahable(UC)としてメモリマップI/Oのために使用されるアドレススペースをマップするためのメモリタイプレンジレジスタ(MTRR)を使って防止できます。3AのChapter 11を参照してください。
PentiumとIntel 486プロセッサはMTRRをサポートしません。代わりに、KEN# pinを提供し、inactive(high)が維持されているときはシステムバス上で送信された全てのアドレスのキャッシュを防止します。このpinを使用するために、外部アドレスデコードロジックは特定のアドレススペースでブロックキャッシュのために必要です。
on-chipキャッシュを持つ全てのIA-32プロセッサもまた、ページテーブルあるいはページディレクトリエントリにPCD(page-level cache disable)フラグを提供します。このフラグはページごとにキャッシュを無効にできます。3A-Chapter 4を参照してください。
18.4 I/O INSTRUCTIONS
プロセッサのI/O命令はI/Oアドレススペースを通してI/Oポートへのアクセスを提供します。これらの命令はメモリマップI/Oポートのアクセスで使用することはできません。これらの命令は2つのグループに分けられます。
- I/Oポートと汎用レジスタ間で単一要素(Byte, Word, Dword)の転送
- I/Oポートとメモリ間で文字列要素(Byte文字列、Word文字列、Dword文字列)の転送
レジスタI/O命令のINとOUTはI/OポートとEAXレジスタ(32bit I/O)またはAXレジスタ(16bit I/O), AL(8bit I/O)間でデータを移動します。I/Oポートのアドレスは即値またはDXレジスタの値で与えることが出来ます。
文字列I/O命令のINSとOUTSはI/Oポートとメモリ位置間でデータを移動します。I/OポートのアドレスへのアクセスはDXレジスタで与えられます。ソースメモリアドレスまたは宛先メモリアドレスはそれぞれDS:ESIレジスタまたはES:EDIレジスタで与えられます。
REPプレフィクスを使用するとき、INS命令とOUTS命令は文字列(またはブロック)を入力または出力操作を実行します。REPプレフィクスはI/Oポートとメモリ間でデータのブロックを転送するためにINS命令とOUTS命令を変更します。このとき、ESIレジスタまたはEDIレジスタはI/Oポートとメモリ間で各Byte, Word, Dwordは転送されたあと、(EFLAGSレジスタのDFフラグの設定に従って)インクリメントまたはデクリメントされます。
2Aと2BのChapter3とChapter4を参照してください。
18.5 PROTECTED-MODE I/O
プロセッサが保護モードで稼働しているとき、次のように保護メカニズムはI/Oポートのアクセスを規制します。
- I/Oアドレススペースを通してI/Oポートにアクセスする際、2つの保護デバイスのアクセスを制御します。
- EFLAGSレジスタのI/O特権レベル領域(IOPL)
- タスク状態セグメント(TSS)のI/O許可bitマップ
- メモリマップI/Oポートにアクセスするとき、標準セグメントとページング保護と(これらをサポートしたプロセッサの)MTRRもまたI/Oポートへのアクセスに影響します。3AのChapter5とChapter11を参照してください。
次のSectionではI/O命令でI/OアドレススペースのI/Oポートアクセス時に利用可能な保護メカニズムについて説明します。
18.5.1 I/O Privilege Level
I/O保護が使用されたシステムにおいて、EFLAGSレジスタのIOPL領域は選択された命令の使用を制限することでI/Oアドレススペースへのアクセスを制御します。この保護メカニズムはI/Oを実行するのに必要な特権レベルを設定するためのOSまたは実行環境を許可します。一般的な保護リングモデルでは、I/Oアドレススペースへのアクセスは特権レベル0あるいは特権レベル1へ制限されます。このとき、カーネルとデバイスドライバはI/O実行を許可しますが、特権レベルが低いデバイスドライバとアプリケーションプログラムのI/Oアドレススペースへのアクセスは拒否されます。アプリケーションプログラムはI/Oを実行するためのOSを呼び出しを作らなければなりません。
IN, INS, OUT, OUTS, CLI(割り込み許可フラグをクリア), STI(割り込み許可フラグを設定)の各命令は、現在実行されているプログラムまたはタスクの現在の特権レベル(CPL)がIOPL以下の場合のみ実行することが出来ます。これらの命令はIOPL領域にセンシティブなため、I/Oセンシティブ命令と呼ばれます。I/Oセンシティブ命令を使用するために低い特権のプログラムまたはタスクによる試みは、一般保護例外(#GP)が通知される結果となります。各タスクはEFLAGSレジスタの独自のコピーを持つため、各タスクは異なるIOPLを持つことができます。
TSSのI/Oパーミッションbitマップは、I/Oセンシティブ命令のIOPLの影響を変更し、低い特権のプログラムまたはタスクによる一部のI/Oポートへのアクセスを許可することができます(Section 18.5.2を参照)。
プログラムまたはタスクはPOPF命令とIRET命令でのみIOPLを変更することが出来ます。しかし、このような変更は特権です。特権レベル0で実行されている場合を除き、現在のIOPLを変更するプロシージャはありません。IOPLを変更するための、より低い特権のプロシージャーによる試みでも、例外は発生しません。IOPLは単に変更されないままです。
POPF命令は、(CLIとSTIと同じように)IFフラグの状態を変更するために使用されます。しかし、この場合のPOPF命令もI/Oにセンシティブです。プロシージャはCPLが現在のIOPL以下の場合のみIFフラグの設定を変更するためにPOPF命令を使用するかもしれません。IFフラグを変更するために、低い特権のプロシージャによる試みでは例外が発生しません。IFフラグは変更されません。
18.5.2 I/O Permission Bit Map
I/Oパーミッションbitマップは低い権限のプログラムまたはタスクによるI/Oポートへの制限されたアクセスを許可するため、あるいはvirtual-8086モードにおけるタスク操作のためのデバイスです。I/Oパーミッションbitマップは現在稼働中のタスクまたはプログラムのTSS(図18-2参照)に配置されます。I/Oパーミッションbitマップの最初のByteのアドレスはTSSのI/Oマップベースアドレス領域に与えられます。TSSのI/Oパーミッションbitマップのサイズと位置は様々です。
それぞれのタスクにTSSを持つため、それぞれのタスクにはI/Oパーミッションbitマップを持ちます。個々のタスクを与えられることによって、個々のI/Oポートへアクセスすることが出来ます。
保護モード、あるいはCPLが現在のIOPL以下の場合、プロセッサは全てのI/O操作を進行することを許可します。CPLがIOPL以上、またはプロセッサがvirtual-8086モードで操作されている場合、プロセッサはI/Oパーミッションbitマップをチェックして、特定のI/Oポートへのアクセスが許可されているかどうかを判断します。マップの各bitはI/OポートByteアドレスに対応しています。例えばI/OアドレススペースのI/Oポートアドレス29Hの制御bitはbitマップの6番目のByteのbit 1で見つけられます。Dwordアクセスのために、例えばプロセッサは4つの隣接する8bitポートアドレスに対応する4つのbitをテストします。テストされたbitがセットされている場合、一般保護例外(#GP)がサインされます。クリアされている場合、I/O操作の進行を許可します。
I/OポートアドレスはWord境界あるいはDword境界に整列させる必要がないため、プロセッサはI/OポートへアクセスするたびにI/Oパーミッションbitマップから2Byte読み込みます。上位アドレスのポートにアクセスするときに例外が生成されないようにするには、テーブルの直後に余分なByteをTSSに含める必要があります。このバイトはすべてのbitがセットされていなければならず、セグメントの制限内でなければなりません。
I/Oパーミッションbitマップが全てのI/Oアドレスを表す必要はありません。マップに含まれていないI/Oアドレスは、マップ内のbitを設定したかのように扱われます。例えば、TSSセグメントの制限がbitマップベースアドレスの10Byteを超えた場合、マップは11Byteを持ち、最初の80 I/Oポートがマップされます。I/Oアドレススペースの上位アドレスは例外を生成します。
I/ObitマップベースアドレスがTSSセグメント制限以上なら、I/Oパーミッションマップはなく、全てのI/O命令はCPLが現在のIOPL以上のときに例外を生成します。
18.6 ORDERING I/O
I/Oデバイスを制御する場合、メモリとI/O操作をプログラムされた順序で正確に実行することが重要な場合があります。例えば、プログラムはI/Oポートへコマンドを書き込むかもしれません。そのとき、他のI/OポートからI/Oデバイスのステータスを読み込みます。前ではなく、コマンドを受信した後に返されるステータスがデバイスのステータスであることが重要です。
メモリマップI/Oを使っている場合、プログラムされた命令がプロセッサによって保持されない状況を避けることに注意する必要があります。パフォーマンスを最適化するために、プロセッサは多くの状況でバッファされた書き込みの前に並べ替えるキャッシュメモリの読み込みを許可します。内部的には、プロセッサはバッファされた書き込み周りの並べ替えを行うことが出来ます。したがって、メモリマップI/Oを使用する場合、前の命令のメモリ書き込みの前にI/O読み込みを実行される可能性があります。Pentium4, Intel Xeon, P6ファミリプロセッサのメモリマップI/Oの強制プログラム順序の推奨される方法は、MTRRを使用してメモリマップI/Oアドレススペースをキャッシュ不可能にすることです。PentiumとIntel486プロセッサの場合、KEN#pinまたはPCDフラグをこの目的に使用できます(Section 18.3.1参照)。
読み込みまたは書き込みのターゲットがメモリのキャッシュ不可能領域の場合、メモリの並び替えはプロセッサのpinの外部で行われません(つまり、読み書きは順序通り行われます)。アドレススペースのマップI/O領域をキャッシュ不可能として指定することは、I/Oデバイスの読み書きがプログラム順に実行されることを保証します。3AのChapter 11を参照してください。
プログラム順序を実施する別の方法は、CPUID命令などのシリアライズ命令の1つを演算の間に挿入することです。3AのChapter 6を参照してください。
プロセッサ(バスコントローラ、メモリコントローラ、および/またはI/Oコントローラ)をサポートするために使用されるチップセットは、メモリアクセスのアウトオブオーダー実行を引き起こす可能性のあるキャッシュ不可能メモリへの書き込みをポストする可能性があることに注意する必要があります。チップセットによるメモリアクセスのアウトオブオーダー処理が潜在的にメモリマップI / O処理の不良を引き起こす可能性がある状況では、I/O操作の同期化および順序付けを強制するコードを書き込む必要があります。この目的のために、しばしばシリアライズ命令が使用されます。
メモリマップI/Oの代わりにI/Oアドレススペースを使用する場合、状況は2つの点で異なります。
- プロセッサはI/O書き込みをバッファしません。したがって、I/O操作の厳密な順序付けは、プロセッサによって強制されます。(メモリマップI/Oと同様に、チップセットが特定のI/O範囲で書き込みをポストすることも可能です)。
- プロセッサは、外部バス動作とI/O命令の実行を同期させます(表18-1を参照)。
CHAPTER 19 PROCESSOR IDENTIFICATION AND FEATURE DETERMINATION
IA-32プロセッサで稼働することを意図したソフトウェアを書くとき、システムの現在のプロセッサの種類と、アプリケーションで利用可能なプロセッサの機能を識別することが必要です。
19.1 USING THE CPUID INSTRUCTION
Pentium Mプロセッサファミリ、Pentium 4プロセッサファミリ、Intel Xeonプロセッサファミリ、P6ファミリ、Pentiumプロセッサ、Intel486以降のプロセッサでプロセッサ識別のためにCPUIDを使用します。この命令は、命令を実行するプロセッサのファミリ、モデル、(一部のプロセッサで)ブランド名を返します。プロセッサに存在する機能を示し、プロセッサのキャッシュとTLBに関する情報を提供します。
EFLAGSレジスタのIDフラグ(bit 21)はCPUIDのサポートを示します。ソフトウェアプロシージャこのフラグをセット、クリアすることができる場合、プロシージャを実行するプロセッサはCPUID命令をサポートします。CPUID命令は、それをサポートしないプロセッサで実行された場合、無効オペコード例外(#UD)を発生させます。
プロセッサ識別情報を得るために、ソースオペランドの値は返される情報の種類を選択するためにEAXレジスタに配置されます。CPUID命令が実行されたとき、選択された情報はEAX, EBX, ECX, EDXレジスタに配置されます。CPUID命令の完全な説明は、2AのChapter3を参照してください。
19.1.1 Notes on Where to Start
次のガイドラインは最も重要なもので、CPUIDを使用して利用可能な機能を判断する際は、常にこれに従うべきです。
- EAXが0の状態でCPUID命令を実行すると、EBX、EDX、ECXレジスタの "GenuineIntel"メッセージが常にテストされます。プロセッサがIntel純正品でない場合、機能識別フラグの意味はインテルの文書に記載されています。
- 機能識別フラグを個別にテストし、未定義bitについては仮定しません。
19.1.2 Identification of Earlier IA-32 Processors
CPUID命令は、Intel486より前のプロセッサを使用していたIA-32プロセッサでは使用できません。これらのプロセッサでは、プロセッサを識別するためにいくつかの他のアーキテクチャ上の機能を活用することができます。
EFLAGSレジスタのbit 12および13(IOPL)、14(NT)、15(予約)の設定は、インテルの32bitプロセッサとインテル8086およびインテル286プロセッサの設定が異なります。アプリケーションプログラムは、これらのbitの設定(PUSHF/PUSHFDおよびPOPF/POPFD命令を使用)を調べることにより、プロセッサが8086、Intel 286、またはIntel 32bitプロセッサのいずれかであるかどうかを判断できます。
- 8086プロセッサ
EFLAGSレジスタのbit12-15は常にセット - Intel 286プロセッサ
real-addressモードでbit12-15は常にクリア - 32bitプロセッサ
real-addressモードでは、bit15は常にクリアされ、bit12-14は最後の値をそれぞれにロードされます。protectedモードではbit15は常にクリアされ、bit14は最後の値をロードし、IOPL bitは現在の特権レベル(CPL)に依存します。IOPL領域はCPLが0の場合のみ変更できます。
他のEFLAGSレジスタbitは32bitプロセッサ間で異なって使用されます。
- Bit 18(AC)
Pentium 4、Intel Xeon、P6ファミリ、Pentium、Intel 486プロセッサでのみ実装されています。このbitをセットまたはクリアできないと、Intel386プロセッサとそれ以降のIA-32プロセッサが区別されます。 - Bit 21(ID)
プロセッサがCPUID命令を実行できるかどうかを決定します。このbitをセットおよびクリアする機能は、それがPentium 4、Intel Xeon、P6ファミリ、Pentium、またはそれ以降のバージョンのIntel486プロセッサであることを示します。
x87 FPUまたはNumeric Processor Extension(NPX)がシステムに存在するかどうかを判断するには、アプリケーションはFNINIT命令を使用してx87 FPUのステータスと制御レジスタに書き込み、FNSTENV命令を使用して正しい値が読み戻されることを確認します。
x87FPUまたはNPXが存在することを確認した後、その型を確認することができます。ほとんどの場合、プロセッサタイプによってFPUまたはNPXのタイプが決定されます。ただし、Intel386プロセッサはIntel 287またはIntel 387数値演算コプロセッサと互換性があります。
コプロセッサが$∞$を表すために使用する方法(FINIT、FNINIT、またはRESET命令の実行後)は、どのコプロセッサが存在するかを示します。Intel 287数値演算コプロセッサは、$+∞$と$-∞$に対して同じbit表現を使用します。インテル387数値演算コプロセッサは、$+∞$と$-∞$に対して異なる表現を使用します。
ここで(補足以外)1部は終了です。
まだまだマニュアルには続きがありますが(というよりまだ全体の半分すら程遠いところです笑)、ここで終了し、次回から逆アセンブラについて学んでいこうと思います。適宜IDMを参照し、進めていこうと思います。