プロセスアドレス空間のレイアウトとASLR
セグメンテーション違反やスタックオーバーフローが起きる番地の理由が腑に落ちます。テキストからスタックまでの仮想配置と、ASLRがどこをどう動かすかを内部から解剖します。
- 1.プロセスの仮想空間は低位からテキスト・データ・BSS・ヒープ、中央にmmap領域、高位にスタックという順で並び、ヒープは上へ、スタックは下へ伸びて中央のmmap領域で出会う。
- 2.ASLRはテキスト(PIE)・ヒープ・mmapの基準・スタックの開始位置をロードのたびにランダム化し、攻撃者が固定番地を前提にできないようにする。エントロピーはビット数で決まる。
- 3.スタック末端にはガードページ(アクセスでフォルト)を、関数フレームにはスタックカナリア(破壊検知の番兵)を置き、伸長の暴走とバッファ溢れを別レイヤーで防ぐ。
仮想アドレス空間という器
各プロセスには、他プロセスから独立した連続した仮想アドレス空間が与えられます(前提は 仮想記憶(ページング) を参照)。64bit の x86-64 では、ハードウェアが扱える仮想アドレスは現状 48bit(一部 57bit)に制限され、Linux では下位半分の 0x0000_0000_0000_0000 から 0x0000_7FFF_FFFF_FFFF までがユーザー空間、上位がカーネル空間に予約されます。この器の中に、用途の違う領域(セグメント/マッピング)を配置していくのがアドレス空間レイアウトです。
重要なのは、これらの領域は連続して詰まっているのではなく、カーネルが管理する区間(Linux でいう VMA, virtual memory area)の集合として疎に存在する点です。区間と区間の間には、どの VMA にも属さない穴(hole)があり、そこへアクセスするとセグメンテーション違反になります。
低位から高位への配置
典型的な配置を、低位アドレス(下)から高位アドレス(上)へ順に並べると次のようになります。
高位アドレス 0x7fff_ffff_ffff
┌──────────────────────────┐
│ スタック (stack) │ ← 下方向へ伸びる(grows down)
│ │ │
│ ▼ │
├──────────────────────────┤
│ (穴・ガードページ) │
│ ▲ │
│ │ │
│ mmap領域 (共有ライブラリ, │ ← 典型的に下方向へ伸びる
│ ファイルマッピング, 大malloc)│
├──────────────────────────┤
│ ▲ │
│ │ │
│ ヒープ (heap) │ ← 上方向へ伸びる(grows up, brk)
├──────────────────────────┤
│ BSS (未初期化グローバル) │ ← 実体はゼロ初期化
├──────────────────────────┤
│ データ (初期化済みグローバル)│
├──────────────────────────┤
│ テキスト (.text, 機械語) │ ← 読み取り+実行, 書き込み不可
└──────────────────────────┘
低位アドレス 0x0000_0000_0000(先頭ページは未マップ=NULL捕捉)
各領域の役割と保護属性を押さえます。
- テキスト(コード): 機械語命令そのもの。
r-x(読み取り・実行のみ、書き込み不可)。書き込み不可なので命令の自己書き換えを防ぎ、複数プロセスで物理ページを共有できます。 - データ(.data):
int g = 5;のように初期化済みのグローバル/静的変数。rw-。実行ファイル内に初期値が記録され、ロード時にコピーされます。 - BSS(.bss):
int g;のように初期値ゼロの未初期化グローバル。実行ファイルにはサイズだけ記録され、実体は持ちません。最初に触れたときゼロ埋めページとして用意されます。 - ヒープ:
malloc等が動的確保に使う領域。古典的にはbrk/sbrkでデータ末尾の上限を押し上げて拡張します(上方向)。 - mmap 領域: 共有ライブラリ、ファイルマッピング、大きな
malloc(glibc は閾値超でmmapを使う)が置かれる中央の広大な領域。 - スタック: 関数呼び出しのフレーム(戻りアドレス・ローカル変数・退避レジスタ)を積む領域。下方向へ伸びます。
ヒープは低位から上へ、スタックは高位から下へ伸び、その間に mmap 領域が広がります。歴史的には両者が中央でぶつかると枯渇しましたが、64bit の広大な空間では現実的な衝突はまず起きません。代わりに、それぞれの伸長は隣接 VMA やリソース上限(RLIMIT_STACK 等)で頭打ちになります。
なぜ BSS とデータを分けるのか
データと BSS を分けるのは、実行ファイルのサイズを節約するためです。int table[1000000]; をゼロ初期化済みとして扱うなら、初期値の 0 を 100 万個ファイルに書く必要はなく、「この大きさの領域をゼロで用意せよ」という指示(サイズ情報)だけ持てば十分です。これが BSS の役割で、実体ページはアクセス時に初めてゼロ埋めで割り当てられます(デマンドページングとページフォルト処理 の匿名ページに相当)。一方、明示的に非ゼロの初期値を持つ変数はデータセグメントに置き、初期値をファイルに記録します。
| 観点 | データ (.data) | BSS (.bss) |
|---|---|---|
| 対象 | 初期値が非ゼロのグローバル/静的 | 初期値ゼロ or 未初期化のグローバル/静的 |
| ファイル内 | 初期値を実体として保持 | サイズ情報のみ(実体なし) |
| ロード時 | ファイルから値をコピー | ゼロ埋めページを遅延割当 |
| 保護 | rw- | rw- |
ASLR:固定番地の前提を崩す
これらの領域を毎回同じ番地に置くと、攻撃者は「戻りアドレスを 0x4011a0 に書き換えれば任意コードへ飛べる」といった固定番地を前提にした攻撃を組み立てられます。ASLR(Address Space Layout Randomization) は、ロードのたびに主要領域の基準アドレスをランダムにずらすことで、この前提を成立させない緩和策です。
ランダム化される対象は次のとおりです。
- スタックの開始位置: 起動ごとにスタックトップをランダムにずらす。
- mmap 領域の基準: 共有ライブラリ(
libcなど)やファイルマッピングの配置基準をランダム化。ライブラリ内のガジェット番地が読めなくなる。 - ヒープの基準(brk): ヒープ開始をランダム化。
- テキスト(実行ファイル本体): バイナリを PIE(Position Independent Executable) としてビルドした場合のみ、テキスト/データの基準もランダム化される。PIE でない(位置依存)実行ファイルはテキストが固定番地に置かれ、ここだけ動かない。
ASLR が有効でも、実行ファイルが PIE でなければそのコード/データ部分は固定アドレスにロードされます。完全な実行ファイル本体のランダム化には、コンパイル時の -fPIE -pie が必須です。共有ライブラリ(.so)はもともと位置独立なので、PIE でなくても mmap 配置はランダム化されます。
ランダム化の強さは**エントロピー(動かせるビット数)**で決まります。ページ単位(4KB=12bit はオフセットなので動かせない)でずらすため、エントロピーは「動かせる上位ビット数」です。32bit では空間が狭くエントロピーが小さく(mmap で 8bit 程度)、総当たりで破られ得ました。64bit では空間が広く、mmap のエントロピーは 28bit 以上になり、ブルートフォースが現実的でなくなります。これが 64bit ASLR が実効的に強い理由です。
ASLR は「番地を読まれた瞬間」に無力化されます。情報漏洩(info leak)で一つでも実アドレスが漏れると、同じライブラリ内の他の番地は固定オフセットで逆算できてしまいます。だから ASLR は単独の防御ではなく、DEP/NX(データ領域の実行禁止)やスタックカナリアと多層で組み合わせて使います。
スタックの保護:ガードページとカナリア
スタックには性質の異なる二つの保護があり、混同されがちなので分けて理解します。
ガードページは、スタック末端(最も下位側)に置かれるアクセス不可のページです。スタックは下方向へ伸びるため、再帰の暴走などで伸びすぎると、いずれこのガードページに到達します。ガードページはどの権限も持たないため、触った瞬間にページフォルトが上がり、カーネルは正常な伸長要求と区別してスタックオーバーフローを検知します(多くの場合 SIGSEGV でプロセスを止める)。これは「伸長の暴走を物理的に堰き止める」仕組みです。なお Linux には、スタックとその下の mmap 領域が飛び越えて衝突するのを防ぐためのスタックガードギャップ(既定 1MB の余白)もあり、これも同種の防御です。
スタックカナリア(スタックカナリー/スタックプロテクタ)は、各関数フレームの戻りアドレスの手前に置く番兵の値です。関数の入口でランダムなカナリア値を積み、関数を抜ける直前にそれが書き換わっていないかを照合します。strcpy 等でローカル配列を溢れさせて戻りアドレスを上書きしようとすると、その手前にあるカナリアも必ず壊れるため、照合で破壊を検知して abort します。これは「バッファ溢れによる戻りアドレス改ざんを検知する」仕組みで、伸びすぎの検知とは目的が異なります。
| 観点 | ガードページ | スタックカナリア |
|---|---|---|
| 守る対象 | スタックの伸びすぎ(暴走/衝突) | バッファ溢れによる戻りアドレス改ざん |
| 実装層 | カーネル(ページ保護+フォルト) | コンパイラ(コード生成) |
| 検知方法 | 未マップ/権限なしページへのアクセス例外 | 関数終了時のカナリア値の照合 |
| 有効化 | OSが自動配置 | ビルド時 -fstack-protector 系 |
擬似コードで、カナリア付き関数の入口と出口を示します。
function foo():
canary = __stack_chk_guard # スレッド固有のランダム値を退避
push canary # 戻りアドレスの手前に積む
char buf[64] # ローカル配列
... 本体(buf への書き込みなど)...
if pop_canary() != __stack_chk_guard: # 出口で照合
__stack_chk_fail() # 不一致 → abort(攻撃検知)
return # 一致 → 通常リターン
つまずきポイント
- 「ヒープは常に上、スタックは常に下」は伸長の向きの話であり、絶対的な配置はアーキテクチャと OS 次第です。ただしスタックが高位・ヒープが低位という大枠は x86-64 Linux では成り立ちます。
- ASLR とガードページ/カナリアは別物。ASLR は「番地を当てさせない」緩和、カナリアは「改ざんを検知する」緩和、ガードページは「伸びすぎを止める」仕組みで、防ぐ対象が違います。
- 先頭ページ(NULL 周辺)はわざと未マップにしてあり、NULL ポインタ参照を確実にフォルトさせます。0 番地が有効だとバグが暴走するためです。
- 大きな
mallocはヒープではなく mmap 領域に置かれることがあります。glibc は閾値(既定 128KB 前後)を超える要求をmmapで個別に確保し、解放時に即munmapするため、ヒープの断片化を避けられます。
まとめ
- 仮想空間は低位からテキスト/データ/BSS/ヒープ、中央に mmap 領域、高位にスタックという順で並び、ヒープは上へ、スタックは下へ伸びる。各領域は VMA として疎に存在し、間の穴に触るとセグメンテーション違反になる。
- BSS はゼロ初期化領域をサイズ情報だけで表し、実ファイルとメモリを節約する。実体はアクセス時にゼロ埋めで遅延割当される。
- ASLR はテキスト(PIE 時)・ヒープ・mmap 基準・スタック開始をロードごとにランダム化し、エントロピー(ビット数)の大きい 64bit で実効的に強い。ただし info leak で無力化されるため多層防御が前提。
- スタックは、伸びすぎを止めるガードページと、戻りアドレス改ざんを検知するカナリアという目的の異なる二層で守られる。
確保の方針そのものは メモリ管理(スタックとヒープ)、アドレス変換のハードウェア側は 仮想記憶のアドレス変換とMMU/TLBの内部 も合わせて読むと、配置から翻訳までが一本につながります。
OS Article
プロセスアドレス空間のレイアウトとASLRを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
アドレス空間
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
ASLRはテキスト(PIE)・ヒープ・mmapの基準・スタックの開始位置をロードのたびにランダム化し、攻撃者が固定番地を前提にできないようにする。エントロピーはビット数で決まる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「アドレス空間 / ASLR」に近いか確認する。
- 強みである「プロセスの仮想空間は低位からテキスト・データ・BSS・ヒープ、中央にmmap領域、高位にスタックという順で並び、ヒープは上へ、スタックは下へ伸びて中央のmmap領域で出会う。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。