概要
最も広く知られているCPUアーキテクチャ
- フォイ・ノイマン・アーキテクチャ
CPUの構成
- 算術論理ユニット
- レジスタからフェッチされた命令を実行する
- 実行結果は、レジスタかメモリに格納される
- レジスタ
- メインメモリより小さい、CPU内のメモリ
- 命令実行時間を節約するのに必要
- コントロールユニット
- メインメモリから、命令を受け取る
レジスタ
- CPUは他のどの記憶媒体よりも高速にレジスタのデータにアクセスできますが、そのサイズは限られている
レジスタの分類
命令ポインタ
- CPUが次に実行する命令アドレスを保持するレジスタ
- プログラムカウンタ
- 呼び方の変遷
- IP
- 元々はIntel 8086プロセッサ(x86という用語の由来)の16ビットレジスタ
- EIP
- 32ビットプロセッサでは、命令ポインタはEIP(拡張命令ポインタ)と呼ばれる32ビットレジスタになった
- RIP
- 64ビットシステムでは、このレジスタはRIP(レジスタの略)と呼ばれる64ビットレジスタになった
- IP
- 呼び方の変遷
汎用レジスタ
- x86システムの汎用レジスタはすべて32ビットレジスタ
- その名の通り、CPUによる命令の一般的な実行時に使用された
- 64ビットシステムでは、これらのレジスタは64ビットレジスタとして拡張された
EAX・RAX
色々あるけど、要は、1 本のレジスタを “拡大/縮小” してアクセスしているだけ?
64-bit モード
┌────────────────────────────────────┐
│ RAX (64 bit) │
├──────────────┬──────────────┤
│ EAX (32) │ 使われない │ ←32 bit 書込みはここを 0 クリア
├──────┬──────┤
│ AX16 │ │ ←16 bit (=下半分)
├──┬──┤
│AL│AH│ ←8 bit×2
└──┴──┘
- アキュムレータレジスタ
- 算術演算の結果は、多くの場合このレジスタに格納される
EBX・RBX
- ベースレジスタ
- オフセットを参照するためのベースアドレスを格納するためによく使われる
- ベースアドレス
- “基準”を保持するためによく使われるレジスタ
- オフセット
- 「基準アドレスから何バイト先(もしくは前)か」を表す符号付き数値
- わかりやすい例
- 駅(ベース)+ 駅から徒歩○分(オフセット) = 目的地
- ベースアドレス
ECX・RCX
- カウンタ レジスタ
- ループなどのカウント操作によく使われる
EDX・ RDX
- データレジスタ
- 乗算/除算演算でよく使われる
ESP・RSP
- スタックポインタ
- スタック
- 関数ごとに作られる一時置き場
- 引数、戻り先アドレス、ローカル変数を出し入れするメモリ領域
- PUSHとPOPのやつね
- x86 系ではアドレスが “高い→低い” 方向へ伸びる(下向き)
- 皿を積むたびにアドレスが小さくなる
- スタック
- スタックの先頭を指し、スタックセグメントレジスタと組み合わせて使われる
- より小さなレジスタとしてアドレス指定することはできない
ESPとRSPは、スタックの一番上(最新)に積まれたデータの次のアドレスを指している
高アドレス ─────────────────
← スタックは下へ伸びる
以前に積んだデータ
︙
最新のデータ
[ RSP ] ← “今ここ”(次の PUSH が書かれる場所)
低アドレス ─────────────────
EBP・RBP
- ベースポインタ
- 関数が始まった直後の RSP のスナップショット
- スタックセグメントレジスタと組み合わせて使用される
- どのプログラムでも、ベースポインタは一定
- 現在のプログラムスタックがローカル変数と引数を追跡する参照アドレス
役割
- スタックの基準点
- 関数開始直後のスタックの状態を記録
- 引数アクセス
[rbp + 8]
などで簡単にアクセスできるように- ここの+8とかは、オフセット
- ローカル変数アクセス
[rbp - 4]
などで安定してアクセスできる
ESI・RSI
- ソースインデックスレジスタ
- ESI / RSI は「データの送り元(ソースアドレス)」として使われる専用寄りレジスタ
- 読み出しに使うレジスタ
- 文字列操作に使用される
- 文字列操作やコピーで「元データが入ってる場所」
- データセグメント(DS)レジスタと組み合わせてオフセット
EDI・RDI
- 宛先インデックスレジスタ
- データの書き込み先(宛先アドレス)」として使われるレジスタ
- 特に文字列操作命令(メモリコピーなど)や関数引数で自動的に使われる
- コピーとかで、読み出し元がRSIで、書き込み先(destination)が、RDI
- エクストラセグメント(ES)レジスタと組み合わせてオフセットとして使用される
R8~R15
- 64bitモードで使えるようになった「第2世代の汎用レジスタ群」
- 基本は自由に使えるけど、関数呼び出しなどで“暗黙の意味”を持つ場面もある
- R10 が
RCX
の代わりに使われる理由もあり、R8〜R10 あたりはシステムレベルでもよく使われる
昔(32bit)の汎用レジスタは8個(EAX〜EDI)しかなかった
-
EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP
だけど64bit化で開放的になって、以下の8個が追加された -
R8, R9, R10, R11, R12, R13, R14, R15
-
一時的な計算結果を入れるのに便利
-
だけど64bit化で開放的
- 32bitの時は命令のエンコード(バイナリ化)にも制約があって、 3ビットでレジスタを表現してたから、使えるのは2³=8個まで
- 64bitでレジスタ表現
- 2の4乗まで=16個まで使えるようになって、追加で増設されたって感じか
ステータスフラグレジスタ
- 実行状態に関するなんらかの情報が必要なときに使う
- if文とかね・四則演算とかね
-
てか、フラグって、フラグレジスタなんだね
ゼロフラグ ・ZF
- 最後に実行された命令の結果がゼロだったら、立つ(1)になる
キャリーフラグ ・CF
- 要は、オーバーフローしたらCFが立つってこと
サインフラグ・SF
- 計算結果の最上位ビット(符号ビット)が 1 なら → SF = 1
- つまり、符号付き整数で“負の数”になったら立つ
トラップフラグ・TF
- プロセッサがデバッグモードにあるかどうかを示す
- TFがセットされている場合、CPUはデバッグのために一度に1つの命令を実行する
- マルウェアはこれを利用して、デバッガで実行されているかどうかを識別できる
セグメントレジスタ
- セグメントレジスタは、フラットなメモリ空間を異なるセグメントに変換し、アドレス指定を容易にする16ビットレジスタ
コードセグメントレジスタ・CS
- コード セグメント (CS) レジスタは、メモリ内のコード セクションを指す
データセグメントレジスタ・DS
- メモリ内のプログラムのデータ セクションを指す
スタックセグメント・SS
- メモリ内のプログラムのスタックを指す
追加セグメント・ES・FS・GS
- 異なるデータセクションを指す
- これらとDSレジスタは、プログラムのメモリを4つの異なるデータセクションに分割する
メモリ
- Windowsオペレーティングシステムのメモリにプログラムがロードされると、プログラムはメモリの抽象化されたビューを参照する
- プログラムはメモリ全体にアクセスするのではなく、自身のメモリのみにアクセスできる
主メモリ
メモリアドレス空間(仮想アドレス)
0xFFFFFFFFFFFFFFFF ← 高アドレス
│
│ ┌───────────────┐
│ │ スタック │ ← 関数のローカル変数、戻り番地、引数など
│ │ (下向き成長) │
│ └───────────────┘
│
│ ┌───────────────┐
│ │ マップ領域 │ ← mmap()、共有ライブラリ、メモリマップファイルなど
│ └───────────────┘
│
│ ┌───────────────┐
│ │ ヒープ │ ← malloc/new の領域(上向きに成長)
│ └───────────────┘
│
│ ┌───────────────┐
│ │ BSS領域 │ ← 初期化されていないグローバル変数
│ ├───────────────┤
│ │ データ領域 │ ← 初期化済みのグローバル/静的変数
│ ├───────────────┤
│ │ コード領域 │ ← 実行ファイルの機械語(実行専用)
│ └───────────────┘
│
0x0000000000000000 ← 低アドレス
コード領域
- プログラムのコードを含む
- 具体的には、このセクションはPEファイル内のテキストセクションを指し、CPUによって実行される命令が含まれる
- メモリのこのセクションには実行権限があり、CPUはプログラムメモリのこのセクション内のデータを実行できる
データ領域
- 可変ではなく定数のままである初期化されたデータが含まれる
- PEファイルのデータセクションを指す
- プログラム実行中に変更されないグローバル変数やその他のデータが含まれる
ヒープ領域
- 動的メモリとも呼ばれる
- プログラム実行中に作成および破棄される変数とデータが格納される
- 変数が作成されると、実行時にその変数にメモリが割り当てられる
- その変数が削除されると、そのメモリは解放される
バッファーオーバーフローが起こる例
char *buffer = malloc(32);
strcpy(buffer, user_input); // ← 入力サイズ不明なまま書き込み
user_input
が 32 バイトを超えるとバッファーオーバーフロー起きる
スタック領域
- マルウェア分析の観点から見ると、スタックはメモリの重要な部分の一つ
- メモリのこのセクションには、ローカル変数、プログラムに渡される引数、そしてプログラムを呼び出した親プロセスの戻りアドレスが含まれる
- 戻りアドレスはCPU命令の制御フローに関連しているため、マルウェアはスタックを標的として制御フローを乗っ取ることがよくある
- バッファーオバーフローは、スタック領域やヒープ領域で起こる
スタックレイアウト
-
スタックはプログラムのメモリの一部であり、プログラムに渡される引数、ローカル変数、そしてプログラムの制御フローが格納される
-
そのため、スタックはマルウェア分析やリバースエンジニアリングにおいて非常に重要
-
マルウェアはしばしばスタックを悪用してプログラムの制御フローを乗っ取る
-
スタックは後入れ先出し(LIFO)メモリ
- スタックに最後にプッシュされた要素が最初にポップアウトされる
-
CPUがスタックを管理するために2つのレジスタを使用する
- スタックポインタ(ESP・RSP)
- ベースポインタ(EBP・RBP)
おさらい
- スタックポインタ
- スタックポインタはスタックの先頭を指す
- スタックに新しい要素がプッシュされると、スタックポインタの位置は、プッシュされた新しい要素を考慮して、次のプッシュ位置を指す
- ベースポインタ
- どのプログラムでも、ベースポインタは一定
- 現在のプログラムスタックがローカル変数と引数を追跡する参照アドレス
スタックバッファオーバーフロー
- ベースポインタの下には、呼び出し元プログラム(現在のプログラムを呼び出すプログラム)の古いベースポインタがある
- 古いベースポインタの下にはリターンアドレスがあり、現在のプログラムの実行が終了すると命令ポインタが戻る
- 制御フローをハイジャックする一般的な手法は、スタック上のローカル変数をオーバーフローさせ、マルウェア作成者が任意のアドレスでリターンアドレスを上書きすること
バッファーオーバーフローが起こる例
void vulnerable() {
char buffer[16]; // ← スタックに確保された小さいバッファ
gets(buffer); // ← 入力の長さを制限せず読み込む ← ここが原因!
}
ここで16以上書き込むとバッファーオーバーフローが起きて、バッファを超えて入力が入り、関数の戻り先(RET)を書き換えてしまうので、任意コード実行できる
[stack]
--------------------------
| 戻り番地(RETアドレス) | ← ここが壊れる!
|------------------------|
| saved RBP |
|------------------------|
| buffer[16] | ← ここに書きすぎると上を上書き!
--------------------------
引数
- 関数に渡される引数は、関数の実行開始前にスタックにプッシュされます。これらの引数は、スタック上の戻りアドレスのすぐ下に配置される
関数のプロローグ
- 関数が呼び出されると、関数の実行に備えてスタックが準備される
- つまり、関数の実行開始前に引数がスタックにプッシュされる
- リターンアドレスと旧ベースポインタがスタックにプッシュされる
- ベースポインタのアドレスはスタックの先頭(その時点での呼び出し元関数のスタックポインタ)に変更される
- 関数の実行中、スタックポインタは関数の要件に従って移動する
- 引数、リターンアドレス、およびベースポインタをスタックにプッシュし、スタックとベースポインタを再配置する
関数のエピローグ
- 関数終了時に、旧ベースポインタはスタックからポップされ、ベースポインタにセットされる
- リターンアドレスは命令ポインタにポップされ、スタックポインタはスタックの先頭を指すように再配置される