読者です 読者をやめる 読者になる 読者になる

Twitterに書ききれないこと

イベントや技術的なことを記したい・・・

Practical Malware Analysis Chapter 4

マルウェア解析についての有名な書籍にPractical Malware Analysisがある。

その書籍を勉強のために翻訳・要約した記事。

全訳ではないので、詳細は原書を読んでください。

Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software

Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software

4章 X86アセンブル入門

基本的な静的解析および動的解析は、解析の優先順位付けには良いが、完全にマルウェアを解析することができない。 基本的な静的解析で、マルウェアの挙動の推論を導き出すことができるが、全容を知るために詳細な解析が必要とされる。 たとえば、特定の関数がインポートされていることを見つけても、その関数が全て使われているか、どのように使われるかはわからない。

また、基本的な動的な技術も欠点がある。 基本的な動的解析では、特別なパケットを受信したとき、解析対象のマルウェアがどのように応答を返すかを見ることができる。 しかし、そのパケットのフォーマットのみしか解析できない。

アセンブルは専門的なスキルである。 この章では逆アセンブルの基礎について説明する。

抽象化のレベル

従来のコンピュータアーキテクチャでは、コンピュータシステムは、いくつかの抽象化のレベルで表すことができる。 これは実装の詳細を隠す。 たとえば、ハードウェアの異なる環境でWindows OSを実行することができる。これは、基盤のハードウェアが、OSから抽象化されているからである。

マルウェアの解析に関わる3つのコーディングレベルがある。 マルウェアの作成者は、高級言語でプログラムを作成して、コンパイラを使い、CPUによって実行されるマシンコードを生成する。 逆にマルウェア解析者は、低級言語でリバースエンジニアリングする。 逆アセンブラを使い、アセンブリコードを生成することで、プログラムの挙動を把握するための解析が可能となる。

コンピュータシステムの抽象化レベルは、以下の6つある。

  • Hardware
    ハードウェアレベルは唯一の物理的なレベルで、電気回路で構成されている。デジタル論理回路の論理演算子(XOR、AND、ORなど)の複雑な組み合わせで実装されている。物理的という性質上、ソフトウェアによって容易に操作することはできない。

  • Microcode
    マイクロコードレベルは、ファームウェアとして知られている。マイクロコードは、専用に設計された回路でのみ動作します。マイクロコードには、ハードウェアとの接続のために、機械語を変換するマイクロ命令が含まれている。 マルウェア解析を行う場合、コンピュータのハードウェア毎に固有であるマイクロコードを意識する必要はない。

  • Machine code
    マシンコードは、オペコードと呼ばれるプロセッサに対する16進数の命令で構成されている。 基盤となるハードウェアがコードを実行できるように、機械語は、いくつかの典型的なマイクロコード命令で実装されている。 高級言語で書かれたコンピュータプログラムが、コンパイルされるときに機械語が生成される。

  • Low-level languages
    低レベルの言語は、コンピュータアーキテクチャの命令セットを人間が読めるようにしたものである。 最も一般的な低レベルの言語はアセンブリ言語である。 マシンコードは、人間が理解するのは難しすぎるため、マルウェア解析者は、低レベルの言語で解析を行う。 低レベルの言語のテキストを生成するために逆アセンブラを使用する。これは、MOV命令とJMP命令のような単純なニーモニックで構成されている。 アセンブリ言語には、多くの異なる形式が存在する。 アセンブリは、高レベルの言語のソースコードがないときに利用できる、機会語から確実に復元可能な最も高いレベルの言語である。

  • High-level languages
    ほとんどのコンピュータプログラマーは高水準言語のレベルで仕事をする。高レベルの言語は、マシンレベルからの強い抽象化を提供する。これによって、簡単にプログラミング論理とフロー制御メカニズムを使用することが可能となる。 高レベルの言語はC、C++などを含む。これらの言語は、通常コンパイラによってマシンコードに変換される。

  • Interpreted languages インタプリタ言語は、トップレベルにある。多くのプログラマは、C#の、Perlの、.NET、Javaなどのインタプリタ言語を使用する。このレベルのコードはマシンコードにコンパイルされない。代わりに、バイトコードに変換される。バイトコードは、プログラミング言語に固有の中間表現である。 バイトコードは、バイトコードを変換するインタプリタ内で実行され、実行時にその場で実行可能な機械語コードに変換される。 従来のコンパイルされたコードと比較した場合、インタプリタはOSに依存しない独自のエラーハンドリングや、メモリ管理の処理できるため、抽象化を無意識的なレベルで提供します。

f:id:pinksawtooth:20150902233818p:plain

リバースエンジニアリング

マルウェアは大抵、機械語レベルのバイナリ形式でディスク上に保存される。前述の通り、マシンコードは、コンピュータが迅速かつ効率的に実行できるコードの形態である。 マルウェアを逆アセンブルするとき、マルウェアのバイナリを逆アセンブラで、アセンブリ言語に変換する。 アセンブリ言語は、実際には言語のクラスである。 x86x64の、SPARCPowerPCMIPS、ARMなどマイクロプロセッサごとにある。x86が、PC用の最も人気のあるアーキテクチャである。 ほとんどの32ビットパーソナルコンピュータは、インテルIA-32として知られているx86アーキテクチャである。また、Microsoft Windowsは、x86アーキテクチャ上で動作するように設計されている。 さらに、ほとんどのAMD64、Intel64アーキテクチャは、Windowsのサポートでx86の32ビットのバイナリを実行できる。 そのため、ほとんどのマルウェアは、x86用にコンパイルされる。 マルウェアの解析中に、最も頻出するx86アーキテクチャに焦点をあてる。

x86アーキテクチャ

現代のコンピュータアーキテクチャの内部は、フォンノイマンアーキテクチャに従う。 これには、3つのハードウェア・コンポーネントがある。

  • 中央処理装置(CPU)は、コードを実行する
  • システムのメインメモリ(RAM)は、すべてのデータとコードを格納する
  • 入力/出力システム(I / O)、ハードディスクドライブ、キーボード、モニターなどのデバイスとのインタフェース

f:id:pinksawtooth:20150904014903p:plain

CPUは、いくつかの構成要素からなる。 制御ユニットは、実行する命令のアドレスを格納するレジスタ(命令ポインタ)を使用して、RAMから実行するための命令を取得する。 レジスタは、CPUの基本的なデータ記憶装置であり、CPUのRAMへのアクセスを抑え、処理の高速化するためにある。 算術論理演算ユニット(ALU)は、RAMからフェッチした命令を実行し、レジスタやメモリに結果を配置する。 プログラムが実行されるように、命令を実行、フェッチをした後繰り返す。

メインメモリ

単一プログラムのメインメモリ(RAM)は、4つの主要なセクションに分けることができる。

  • Data
    データセクションと呼ばれるメモリの部分。プログラムが最初にロードされたときに、値が置かれる場所。プログラムの実行中に変更されない値を、静的な値と呼ぶ。これらは、プログラムのどの部分からでも利用可能なため、グローバル値と呼ばれる。
  • Code
    コードは、プログラムのタスクを実行するために、CPUによってフェッチされた命令を含んでいるコードは、プログラムの動作やタスクを制御している。
  • Heap
    ヒープは、プログラム実行中の動的メモリ(割り当て)に使用され、新しい値を作成する。また、プログラムが必要ない値を排除(free)する。この内容は、プログラムの実行中に頻繁に変更できるため、ヒープは動的メモリと呼ばれている。
  • stack
    スタックは、関数のローカル変数やパラメータのために使用され、プログラムの制御フローを支援する。

f:id:pinksawtooth:20150904084036p:plain

命令

アセンブリプログラムは命令で構成されている。x86アセンブリでは、命令ニーモニックは、0以上のオペランドで構成されている。

ニーモニックは、実行するための命令を識別する単語である。以下にデータを転送するMOV命令の例を示す

Mnemonic Destination operand Source operand
mov      ecx                 0x42

オペコードとエンディアン

各命令は、プログラムが実行したい操作のCPUのオペコード(オペレーションコード)に対応している。 逆アセンブラは、人間が読み取り可能な命令にオペコードを翻訳する。

以下に例を示す。 値0xB9は、MOVのECXに対応し、0x42000000は、値の0x42に対応する。

Instruction mov ecx, 0x42
Opcodes     B9 42 00 00 00

x86アーキテクチャは、リトルエンディアン形式を使用しているため、0x42000000は、値0x42にとして扱われる。 エンディアンとは、最初に処理されるデータ・大きい値が最上位(ビッグエンディアン)か最下位(リトルエンディアン)かを示す。

マルウェアはネットワーク通信中にエンディアンの変更を行う必要がある。ネットワークデータは、ビッグエンディアンを使用するが、x86のプログラムは、リトルエンディアンを使用するからである。 そのため、IPアドレス127.0.0.1はリトルエンディアン形式(ローカルメモリ内)で0x7F000001ビッグエンディアン形式(ネットワーク経由)でと0x0100007Fとして表現される。誤ってIPアドレスなどの重要な数値の順序を逆にしないように、エンディアンを認識しなければならない。

オペランド

オペランドは、命令が使用するデータを識別するために使用される。オペランドは3種類使用することができる。

レジスタ

レジスタは、CPUで使用可能な少量データストレージである。他の利用可能なストレージよりも高速にアクセスすることができる。x86プロセッサは、一時記憶や作業領域として使用可能なレジスタの集合を持っている。

一般的なx86レジスタは、次の4つのカテゴリに分類される。

  • 汎用レジスタは、実行中にCPUによって使用される。
  • セグメントレジスタは、メモリのセクションを追跡するために使用される。
  • ステータスフラグは、判断を下すために使用される。
  • インストラクションポインタは、次に実行する命令を追跡するために使用される。

x86アーキテクチャでは、以下のように分類される。

汎用レジスタ セグメントレジスタ ステータスレジスタ インストラクションレジスタ
EAX(AX,AH,AL) CS EFLAGS EIP
EBX(BX,BH,BL) SS
ECX(CX,CH,CL) DS
EDX(DX,DH,DL) ES
EBP(BP) FS
ESP(SP) GS
ESI(SI)

すべての汎用レジスタのサイズは32ビットで、アセンブリコードで32ビットまたは16ビットのように参照することができる。 例えば、EDXは32ビットレジスタを参照するために使用され、DXはEDXレジスタの下位16ビットを参照するために使用する。 4つのレジスタ(EAX、EBX、ECX、EDX)は最下位8ビットまたは次の8ビットを使用して、8ビット値として参照することができる。 例えば、ALはEAXレジスタの最下位8ビットを参照するために使用され、AHは次の8ビットを参照するために使用する。

以下に例を示す。
32ビット(4バイト)レジスタEAXに値0xA9DC81F5が入っているとする。この場合、3つの方法でEAX内のデータを参照することができる。 AX(2バイト)では0x81F5、AL(1バイト)では0xF5、AH(1バイト)では0x81となる。

f:id:pinksawtooth:20150904222342p:plain

汎用レジスタ

汎用レジスタは、データ、メモリアドレスの保存に使用される。また、互換性を実現するのに使用される。 しかし、汎用レジスタと呼ばれているにもかかわらず、常にそのように使われない。

一部のx86命令は、特定のレジスタを使用する。 例えば、乗算、除算命令は常にEAXとEDXを使用する。 また、コンパイルされたコード全体で、一貫した方法でレジスタを使用することが慣例である。 コンパイラで使用されている規則の知識を知ることで、レジスタの使用方法を考える時間がなくなり、マルウェア解析者は、より高速にコードを調べることができる。

例えば、EAXは一般的に、関数呼び出しの戻り値が入っている。関数呼び出し直後に、EAXレジスタを使用している場合、戻り値を操作している。

フラグ

EFLAGSレジスタは、ステータスレジスタである。x86アーキテクチャでは、サイズが32ビットであり、各ビットはフラグとなっている。 実行時に、各フラグは0か1のいずれかがセットされる。この結果が、CPUの動作の制御、操作の結果を示す。

次のフラグがマルウェア解析では重要である。

  • ZF
    ゼロフラグは、演算の結果がゼロに等しい場合に設定される。それ以外の場合はクリアされる。
  • CF
    キャリーフラグは演算の結果が、ディスティネーションオペランドに対して、大きすぎるか小さすぎる場合に設定される。
  • SF
    演算の結果がネガティブの場合は符号フラグがセットされ、結果がポジティブである場合はクリアされる。 最上位ビットが、算術演算後に設定されている場合、このフラグが設定される。
  • TF
    トラップフラグはデバッグに使用される。このフラグが設定されている場合、x86プロセッサは、一度に1つの命令のみ実行する。

EIP、命令ポインタ

x86アーキテクチャでは、EIPは命令ポインタまたはプログラムカウンタとして使われる。 このレジスタに入っている、次の命令のアドレスがプログラムで実行される。 EIPの唯一の目的は、次に何をすべきかプロセッサに伝えることである。

単純な命令

最も簡単かつ一般的な命令は、ある場所から別の場所へデータを移動するために使用されるMOV命令である。 メモリへの読み書き込みの命令である。MOV命令は、レジスタやRAMにデータを移動することができる。

MOV命令に似た命令に、LEA命令がある。LEAは、“load effective address”の略である。 LEA命令は、宛先にメモリアドレスを入れるために使用する。

MOV命令はメモリの指す値を取り出し、LEA命令はメモリアドレスを取り出す。

算術

x86アセンブリは、論理演算子の基本的な足し算と引き算など、演算のための多くの命令がある。 SUB命令は、2つの重要なフラグ(ZF、CF)を変更する。 結果が0である場合にZFが設定され、デスティネーションを差し引いた値が0未満である場合は、CFが設定される。

MUL値命令は常EAXの値を乗算する。したがって、乗算前に適切にEAXを設定する必要がある。乗算の結果が単一のレジスタに収まらないとき、結果は、2つのレジスタ(EDX EAX)に64ビット値として格納される。

div命令も除算であること以外同様である。 除算の結果は、EAXに格納され、余りはEDXに格納される。

OR、AND、ORなどの論理演算子は、x86アーキテクチャで使用される。 XOR命令は頻繁に出てくる。 例えばeaxを0に初期化したいとき、xor eax, eaxで行う。 MOV命令で同様の処理を行うと5バイトかかるが、XORだと2バイトですむので、最適化のために使われる。

SHR命令、SHL命令はレジスタをシフトするために使用する。 ROR命令、ROL命令は、シフト命令と似ているが、シフトした値が落ちるのではなく、回転する。 シフトは、しばしば最適化として、乗算の代わりに使用される。

マルウェア解析時、xor, or, and, shl, ror, shr, rolなどの命令が繰り返されているだけの関数は、暗号化や圧縮を行っている可能性が高い。 必要がない場合は各命令を解析しなくてよい。まずは、この関数を暗号化ルーチンとしてマークして、呼び出し元を確認すればよい。

NOP

NOP命令は、何もしない。NOPが発行されていた場合、単に次の命令に進む。32bit環境では、NOP命令は、xhcg EAX、EAXである。 この命令のオペコードは0x90で,バッファオーバーフロー攻撃で使用される。NOP命令を使うことで、パディングを提供し、悪意のあるシェルコードが動作しないリスクを軽減する。

スタック

関数、ローカル変数、フロー制御のためのメモリは、スタックに格納される。 スタックはpushとpopをする特徴を持ったデータ構造である。 スタックは後入れ先出し(LIFO)構造である。

x86アーキテクチャでは、ESPレジスタEBPレジスタを使用してスタックを制御する。 ESPはスタックポインタであり、典スタックの最上部を指すメモリアドレスが含まれている。 スタックからpush、popが行われるとこのレジスタの値が変化する。 EBPは、関数内で一定した値を持つ、ベースのポインタである。 EBPは、ローカル変数とパラメータの位置を追跡するための、プレースホルダとして使用することができる。

スタックで使用される命令は、push、pop、call、leave、enter、retがある。 スタックは、メモリ内のトップダウン形式で、最上位アドレスが割り当てられ、最初に使用される。 値がスタックにプッシュされる場合、より小さいアドレスが使用される。 スタックは短期的な記憶にのみ使用され、頻繁にローカル変数、パラメータ、リターンアドレスが格納される。 主な用途は、関数呼び出しの間でやり取りされるデータを管理することである。 この管理の実装は、コンパイラによって変化するが、ローカル変数とパラメータがEBPから相対参照可能な実装が一般的である。

関数呼び出し

関数は、特定のタスクを実行する比較的独立しているプログラム内のコードの一部である。 メインコードは、メインコードに戻る前に、関数を呼び出し、一時的に制御を移す。 スタックの利用方法は、バイナリ全体で一貫している。

以下では、最も一般的なCDECLについて述べる。

多くの関数は、関数の開始時にコードのプロローグが数行が含まれている。 プロローグは、関数内で使用するためのスタックとレジスタを準備をする。 関数の最後にエピローグがスタックとレジスタを復元し、関数が呼び出される前の状態にする。

以下のリストは、関数呼び出しのための最も一般的な実装の流れをまとめたものである。

1.引数はpush命令を使用してスタックに配置される。

2.関数を呼び出す。 現在の命令アドレス(つまり、EIPレジスタの値)をスタックにプッシュする。 このアドレスは、関数終了後にメインコードに戻るためのリターンアドレスとして使用される。 関数が開始されると、EIPは関数の開始点に設定される。

  1. 関数プロローグを使用することにより、ローカル変数とEBP(ベースポインタ)をプッシュするための空間がスタック上に確保される。これは、呼び出し元関数のEBPを保存するために行われる。

4.関数が実行される。

5.関数エピローグを使用することにより、スタックが復元される。ESPは、ローカル変数を解放するために調整され、呼び出し元の関数が適切にその変数に対処できるように、EBPが復元される。 LEAVE命令はエピローグとして使用することができる。EBPとESPに同じ値を設定し、スタックからEBPをポップする。

6.関数がRET命令を呼び、呼び出し元へ戻る。スタックからとEIPにリターンアドレスをポップし、呼び出し元から実行を続けるようにする。

7.スタックを、引数を削除するように調整する。

スタックレイアウト

前述のとおり、スタックはトップダウン方式で割り当てられる。 コールが実行されるたびに、新たなスタックフレームが生成される。 呼び出し元のスタックフレームが復元され、実行が呼び出し元の関数に移るまで、関数は独自のスタックフレームを保持している。

f:id:pinksawtooth:20150905130635p:plain

下の図では、ESPはメモリアドレス0x12F02Cでスタックの先頭を指している。 この関数では、EBPは0x12F03Cに設定されている。ローカル変数と引数は、EBPを使用して参照することができる。 呼び出しの前にスタックにプッシュされた引数は、スタックフレームの下部にある。次に、コール命令によって自動的にスタックに置かれているリターンアドレスがある。スタックの次の場所には古いEBPがある。これは、呼び出し元のスタックフレームのEBPである。

情報がスタックにプッシュされたとき、ESPが下がる。 図の例では、push ebpを実行したとき、ESPは4デクリメントされて0x12F028になる。 そして、EAXに含まれるデータは、0x12F028にコピーされることになる。
pop ebxを実行したとき、0x12F028のデータはEBXレジスタにコピーされ、その後、ESPが4インクリメントされる。

f:id:pinksawtooth:20150905135155p:plain

プッシュ命令またはポップ命令を使用することなく、スタックからデータを読み出す方法がある。 たとえば mov eax, ss:[esp]は直接スタックの先頭へアクセスする。 これは、ESPレジスタが影響を受けないことを除いて、EAXポップと同じである。

x86アーキテクチャは、ポップとプッシュするための追加の命令を提供している。 もっとも使われるのは、PUSHAとPUSHADである。 スタックにすべてのレジスタをプッシュする。 これらの命令と一緒にすべてのレジスタをポップするPOPAとPOPASが使われる。 PUSHAは、16ビットレジスタを、PUSHADは32bitレジスタをすべてプッシュする。

これらの命令は、通常シェルコードで見られる。 シェルコード内でレジスタを後で復元できるように、スタックに現在のレジスタの状態を保存する。 コンパイラは、ほとんどこれらの命令を使用しないため、これらの命令があれば、誰かの手でコード化されたアセンブリ、またはシェルコードであることを示す。

条件文

すべてのプログラミング言語は、比較結果に基づいて条件分岐を行う。 条件文は、比較を実行する命令である。よく使われる命令にCMPとTESTがある。

TEST命令ではフラグのみ設定される。ZFはTEST命令で設定されるフラグである。この命令は多くの場合、値がNULLかをチェックするのに使用される。 たとえば、test eax, eaxは0とEAXを比較している命令である。TEST命令は、CMP命令よりCPUサイクルが少ない。

CMP命令は、サブ命令と同じである。しかし、オペランドは影響を受けない。CMP命令は、フラグを設定するためにのみ使用される。ZF、キャリーフラグ(CF)は、CMP命令の結果によって変更される。

以下にCMP命令結果によるフラグの値を示す。

cmp dst, src ZF CF
dst = src 1 0
dst < src 0 1
dst > src 0 0

分岐

分岐は条件付きでプログラムのフローに応じて実行されるコードのシーケンスである。 分岐が発生する最も一般的な方法は、ジャンプ命令である。 しかし、単純なジャンプ命令だけでは、条件分岐ができない。 そのため、条件ジャンプによって条件分岐を実現する。 条件分岐をするために、30以上の異なるジャンプ命令が用意されている。

REP命令

REP命令は、データバッファを操作するための命令である。 通常、1バイトの配列だが、word、dwordを使用することができる。 Intelは、この命令を文字列操作の命令としているが、第1章で説明した文字列との混同を避けるためにデータバッファ操作の命令と呼ぶ。

最も一般的なデータバッファ操作命令は、movsx、cmpsx、stosx、scasxである。xにはb(bite)、w(word)、d(dword)が入る。 これらの命令は任意のデータ型を扱う。

ESIは、ソースインデックスレジスタであり、EDIは、ディスティネーションインデックスレジスタである。 ECXはカウント変数として使用される。

この命令はプレフィックスとして使用する。 x86では、繰り返しプレフィックスは、マルチバイトオペレーションのために使用する。REP命令は、ESIとEDIのオフセットをインクリメントし、ECXレジスタをデクリメントする。

以下の表の条件まで繰り返される。 このため、ほとんどのデータバッファ操作命令でESI、EDI、ECXを初期化する必要がある。

命令 説明
rep Repeat until ECX = 0
repe, repz Repeat until ECX = 0 or ZF = 0
repne, repnz Repeat until ECX = 0 or ZF = 1

rep movdbを使ってC言語のmemcpyと同等の機能を実現することができる。 このようなコードは、コンパイルしたCコードで見ることはない。シェルコードで使用されるテクニックである。 またrep cmpsbを使ってC言語のmemcmpと同様の機能も実現できる。 rep scasbを使用すると、memsetど同様の機能となる。

命令 説明
repe cmpsb 2つのデータバッファを比較するために使用する。EDI、ESIは、二つのバッファ位置に設定する必要があり、ECXはバッファの長さに設定する必要がある。ECX=0になるか、バッファの値が等しくなくなるまで継続される。
rep stosb バッファのすべてのバイトを、特定の値に初期化するために使用する。EDIにバッファの場所、ALに初期値が含まれている必要がある。この命令は、多くの場合のXOR EAX、EAXを使用する。
rep movsb 通常、バイトのバッファをコピーするために使用する。ESIには、ソースバッファのアドレスに設定する必要があり、EDIには、コポー先のバッファアドレスに設定する必要があり、ECXにはコピーする長さを指定する必要がある。バイト単位のコピーは、ECX=0になるまで継続される。
repne scasb シングルバイトのデータバッファを検索するために使用する。EDIには、検索対象のバッファのアドレス、ALには、検索するバイト、ECX、にはバッファの長さを設定する。ECX= 0になるか、バイトが見つかるまで比較が継続される。

Cメインメソッドとオフセット

マルウェアは、多くの場合、Cで書かれている。Cプログラムのmainメソッドが、どのようにアセンブリに変換されるかを知ることが重要である。この知識はCコードのアセンブリを解析するとき、に役立つ。

int main(int argc, char ** argv)

標準的なCプログラムは、2つの引数を持っている。 実行時に引数のargcとargvが決定する。 argcは、プログラム名を含むコマンドラインの引数の数を示す整数である。argvは、コマンドライン引数を含む文字列の配列へのポインタである。

filetestprogram.exe -r filename.txt
argc = 3
argv[0] = filetestprogram.exe
argv[1] = -r
argv[2] = filename.txt
int main(int argc, char* argv[])
{
if (argc != 3) {return 0;}
if (strncmp(argv[1], "-r", 2) == 0){
DeleteFileA(argv[2]);
}
return 0;
}

上記のCコードをアセンブリにすると以下のようになる。 argcは(1)で3と比較され、argv[1]は(2)でstrcmpによって-rと比較される。 argv配列内の各エントリは、32bitシステムで4バイトのため、004113E1でeaxにargv[0]のアドレスを入れた後、eax+4することでargv[1]にアクセスすることができる。 コマンドライン引数が-rの場合、(3)が実行される。DeleteFileA関数にはargv[2]で参照されるfilename.txtが渡される。

004113CE cmp [ebp+argc], 3 (1)
004113D2 jz short loc_4113D8
004113D4 xor eax, eax
004113D6 jmp short loc_411414
004113D8 mov esi, esp
004113DA push 2 ; MaxCount
004113DC push offset Str2 ; "-r"
004113E1 mov eax, [ebp+argv]
004113E4 mov ecx, [eax+4]
004113E7 push ecx ; Str1
004113E8 call strncmp (2)
004113F8 test eax, eax
004113FA jnz short loc_411412
004113FC mov esi, esp (3)
004113FE mov eax, [ebp+argv]
00411401 mov ecx, [eax+8]
00411404 push ecx ; lpFileName
00411405 call DeleteFileA

詳細情報:インテルx86アーキテクチャマニュアル

さらなる詳細は、インテルのマニュアルを参照すればよい。

日本語

英語