Buffer Overflow とは — スタックの仕組み・攻撃・緩和策 のサムネイル

Buffer Overflow とは — スタックの仕組み・攻撃・緩和策

⏱ 約 16 分 view 89 like 0 LOG_DATE:2026-05-10
目次 / TOC

Buffer Overflow #

Buffer Overflow (バッファオーバーフロー, BOF) は、「確保されたバッファのサイズを超えてデータを書き込み、隣接するメモリ領域を破壊する」ことから生じるメモリ安全性脆弱性の古典中の古典である。1988 年の Morris Wormfingerdgets() を踏み台にしてインターネットを停止させて以来、Code Red, Slammer, Heartbleed, glibc CVE-2024-2961 に至るまで、過去 35 年の主要なインターネット規模インシデントの大半にこの脆弱性クラスが関わってきた。

Rust や Go の時代だから、もう過去の問題では?」と思われがちだが、現代の Microsoft Security Response Center / Google Project Zero の公開統計「自社の CVE の約 70% は依然としてメモリ安全性バグ」 と報告している。Linux カーネル / Windows 内部 / Chrome / Firefox / OpenSSL / FFmpeg / postfix など、現代インフラを支える基盤コードの大半は未だに C/C++ で書かれており、新しいメモリ安全性 CVE が毎月のように出るのが現実である。

本稿は 「BOF が起きる仕組みを 1 枚に解剖し、緩和策と bypass の歴史を 1 枚にまとめる」 ことを目標に、スタック BOF の力学ヒープ BOF の概要シェルコード注入緩和策と bypass の軍拡競争歴史的事件合法な学習場所 の順で扱う。

1. なぜ BOF は今でも load-bearing なのか #

BOF が現在も生きている脆弱性クラスである理由は、C/C++ 言語の根本設計 に行き着く:

  • 配列アクセスに境界チェックがないbuf[1000000] を書き込んでもコンパイラもランタイムも止めない
  • strcpy, gets, sprintf, memcpy などの標準関数に「相手側のサイズを知る引数」がない — 入力長を信じて書き込む API 設計
  • ポインタが任意のメモリを指せる — 範囲外参照も allocator を経由しないアクセスも可能
  • メモリの所有権がコンパイル時に追跡されない — use-after-free, double-free が静的に防げない

メモリ安全な言語」 (Rust / Go / Swift / Java / Python / Ruby / C# / JavaScript) は、コンパイラやランタイムがこれらをチェックすることで構造的に防いでいる。が、コードベースの大半は依然 C/C++ であり、書き換えコストが膨大なため、緩和策で生き延びさせる現実的な選択がされてきた歴史がある。Rust の Linux カーネル混入 (2022〜)Microsoft の TypeScript→Rust 移行Android の新規コードを Rust に、といった長期的な書き換えプロジェクトが動いている — それでも完全置換には今後 20 年以上かかる、というのが業界の共通認識。

2. スタックベース BOF — 関数呼び出しの裏側で起きていること #

BOF の最古典であるスタックベース (stack-based buffer overflow) を関数呼び出し時のスタックフレーム構造から解剖する。ローカル変数のバッファ + 戻りアドレスの位置関係こそ、攻撃の発火点。

スタックベース Buffer Overflow — 戻りアドレスを書き換えて制御を奪う x86_64 Linux / 関数 vulnerable() の呼び出し中スタック / 高位アドレス → 低位アドレス ▼ 正常時のスタック (vulnerable() 呼び出し中) caller の引数 / 環境 高位アドレス (例: 0x7fff...ff00) ★ saved RIP (戻りアドレス) caller の次の命令を指す = 0x401200 saved RBP (前の base pointer) スタックフレームのチェーン ローカル変数 (int, ptr など) 関数内の small variables char buf[64] スタック上に確保された 64 byte バッファ [buf] [buf+1] ... [buf+63] 低位アドレス (例: 0x7fff...fe00) ← RSP ▼ 攻撃: strcpy(buf, attacker_input) で書きすぎ caller の引数 / 環境 この上は基本的に書き換わらない ★ RIP = 0xDEADBEEF (攻撃者指定!) return 時にここへ jump する = 制御奪取 saved RBP = AAAAAAAA (上書き済) 通り道として上書きされている ローカル変数 = AAAA... 通り道として上書きされている buf[64] = "AAAA...AAAA" 攻撃者の入力 (例: 80 byte 詰め) 最後の 8 byte が saved RIP に届く位置 入力長を NULL 終端まで「全部」コピーされる ▼ 結果: 関数 ret 命令で RIP = 0xDEADBEEF にジャンプ → 任意コード実行 ① 攻撃者の入力 80 byte: [64 byte AAAA...] + [8 byte AAAA RBP] + [8 byte 攻撃者アドレス] ② strcpy(buf, input) — strcpy はサイズチェックなし、NULL byte まで全部コピー ③ vulnerable() の末尾の `ret` 命令: スタックから saved RIP を pop して PC に load ④ PC = 攻撃者指定アドレス → そのアドレスにある「攻撃者のシェルコード」or「ROP gadget」を実行 ⑤ 結果として shell が立ち、setuid root binary なら root 権限取得 ▼ 危険な C 標準関数 (= サイズを取らない / 取っても誤用しがち) • gets() — サイズ引数なし。C11 で削除 (それでも残存コードに今でも見つかる) • strcpy(), strcat(), sprintf() — サイズなし。strncpy/strncat/snprintf にも罠あり (NULL 終端漏れ) • scanf("%s", buf) — サイズ指定なしの %s は実質 gets と同じ。%63s と書く必要あり • memcpy(dst, src, attacker_controlled_len) — len が攻撃者入力なら同じ問題 これら関数は 30 年以上「使うな」と警告されているが、現存コードベースから完全に消えていない

具体的な脆弱コードと exploit の流れを、最小例で:

// vulnerable.c — 古典的なスタック BOF 脆弱性
#include <string.h>
#include <stdio.h>

void vulnerable(char *input) {
    char buf[64];           // スタック上に 64 byte
    strcpy(buf, input);     // ★ サイズチェックなし → 64 byte 超えると saved RIP 破壊
    printf("%s\n", buf);
}

int main(int argc, char **argv) {
    if (argc > 1) vulnerable(argv[1]);
    return 0;
}
# 緩和策を全部 OFF にしてビルド (現代の OS では追加でランタイム緩和が動く)
gcc -fno-stack-protector -no-pie -z execstack -O0 vulnerable.c -o vulnerable
# saved RIP までの距離を測る (典型的に buf + saved RBP = 64 + 8 = 72 byte で saved RIP)
./vulnerable $(python3 -c 'print("A"*72 + "BBBBBBBB")')   # → セグフォ at RIP=0x4242424242424242

**「攻撃者は入力に何を詰め込むか」**のパターンが分類されている:

  • シェルコード注入 — buf にシェルコード (/bin/sh を起動する数十バイトのアセンブリ) + 末尾に「buf 自身のアドレス」を saved RIP として詰める。NX (DEP) で防がれる
  • ret2libc — saved RIP を libc の system() に向け、引数として /bin/sh の文字列アドレスを渡す。ASLR で防がれる
  • ROP (Return-Oriented Programming) — 既存コード片 (gadget) を ret で繋ぎ、任意処理を構築。現代 BOF exploit の主流
  • JOP / SROP / COOP — ROP の派生

3. ヒープベース BOF — chunk metadata を狙う #

スタックではなく ヒープ (malloc() / new で確保された領域) で起きる BOF が ヒープ BOF。スタックほど直接的に PC を奪えないが、heap allocator の管理メタデータを破壊することで任意のメモリ書き換え (write-what-where) に発展できる。

glibcmalloc (ptmalloc2) を例に取ると、各 chunk は [size | prev_size | data...] というヘッダを持ち、free 済み chunk は double-linked list で繋がる。ヒープ BOF で隣接 chunk の fd / bk ポインタを書き換えると、後続の unlink() 処理で 任意アドレスへの書き込みが成立する (古典的な「unlink 攻撃」)。

現代の glibc は unlink() に整合性チェックを大量に追加しているが、House of Force / House of Spirit / fastbin dup / tcache poisoning / large bin attack といった世代ごとに違う攻撃テクニックが今も研究・公開されている。glibc 2.35 以降の tcache 系は特に頻繁に新攻撃が出る領域。

ヒープ BOF と並んで重要なメモリ安全性脆弱性として:

  • Use-After-Free (UAF)free() した後のポインタを使用 → メモリは別の用途に再割当されているため、書き込みで他データを壊す
  • Double Free — 同じポインタを 2 回 free() → free list が破壊される
  • Type Confusion — オブジェクトを違う型として扱う (C++ の vtable 系で頻発)
  • Out-of-Bounds ReadHeartbleed (CVE-2014-0160) がこれ。書き込みでなく読み取りで隣接メモリの中身が漏れる

これら全部が「ポインタの正しさをコンパイラがチェックしないから起きる」という同じ根に繋がる。

4. 緩和策と bypass の軍拡競争 #

BOF は完全には消せないため、OS とコンパイラとハードウェアが多層的に緩和策を積み上げてきたそれぞれの緩和策には bypass があることを知っておくと、現代の exploit がなぜ複雑なのかが理解できる。

緩和策と bypass の軍拡競争 — 1996 年から現在まで 各緩和策の「狙い」「導入年」「主な bypass 技法」 ▼ Stack Canary (スタックカナリア / SSP) — gcc -fstack-protector / 1998 (StackGuard) → 標準化 2000s 狙い: saved RIP の手前に「秘密の値 (canary)」を置き、関数 return 前に書き換わっていないか確認 → 改竄なら abort Bypass: canary の値を info leak で読み出して同じ値を詰める / canary を上書きせず別経路から ret アドレスに到達 ▼ DEP / NX bit (Data Execution Prevention) — Intel/AMD CPU + OS / 2003-2005 狙い: スタック・ヒープなど「データ領域」のページを実行不可に → buf に入れたシェルコードが実行できなくなる Bypass: ROP (既存コード片を繋ぐ) / ret2libc (libc の system() を呼ぶ) — 既存実行ページの命令だけで攻撃を組む ▼ ASLR (Address Space Layout Randomization) — Linux PaX 2001, mainline 2005 / Windows Vista 2007 狙い: スタック / ヒープ / libc / 実行ファイルの読込アドレスを起動毎にランダム化 → 攻撃者が「飛び先」を予測不能に Bypass: info leak (脆弱性で 1 つでもアドレスを漏らせば差分でベースが分かる) / partial overwrite / brute force (32-bit) / BROP ▼ PIE (Position Independent Executable) — gcc -fpie / 2010s 標準 狙い: 実行ファイル本体もランダム配置 (ASLR の対象に含める) → 静的アドレスを使う ROP も成立しなくなる Bypass: info leak で実行ファイルベースを取得 → ROP を再構築 / GOT (Global Offset Table) overwrite ▼ CFI (Control Flow Integrity) / Intel CET / ARM PAC / 2018-2020s 狙い: 「関数ポインタの飛び先」と「ret の戻り先」を実行時に検証 (shadow stack で saved RIP の改竄検知) Bypass: CFI を満たすパスを使った data-only 攻撃 / counterfeit object-oriented programming (COOP) ▼ Memory-Safe Languages — Rust 2015, Go 2009, Swift 2014, … (構造的解決) 狙い: 言語自体に「ポインタ範囲外参照は静的・動的に防ぐ」を組み込む → BOF クラスが原理的に発生しなくなる Bypass: `unsafe` ブロック内のバグ / FFI で C ライブラリを呼んだ先 / ロジックバグは依然存在 (memory-safe ≠ bug-free) 現代のバイナリ (Linux distro 配布物) は SSP + DEP + ASLR + PIE + RELRO の 5 段が標準 checksec.sh / pwntools の checksec で確認可能 — 1 つでも欠けていれば exploit 難易度が大きく下がる
# バイナリの緩和策を一括確認
checksec --file=./vulnerable
# 出力例:
# RELRO         STACK CANARY   NX        PIE     RPATH    RUNPATH    Symbols    FORTIFY
# Full RELRO    Canary found   NX enab.  PIE en. No RPATH No RUNPATH No symbols Yes

# pwntools の Python から
python3 -c 'from pwn import *; print(checksec("./vulnerable"))'

「すべての緩和策を満たしている = 攻撃不可能」ではない1 つの info leak (= 任意のアドレス読出し) と 1 つの BOF を組み合わせる ことで、現代の exploit はほぼ任意の保護を貫通できる。Pwn2Own で年間複数回、Chrome / Safari / iOS / Windows カーネルの完全 sandbox 脱出が成立しているのが最良の証拠。「緩和策は壁を高くする」ものであって、「完全に防ぐ」ものではない。

5. 歴史的事件 — BOF が変えた世界 #

インターネット規模で大きな影響を与えた事件の多くが BOF または隣接するメモリ安全性問題:

事件 仕組み 影響
1988 Morris Worm fingerdgets() に対するスタック BOF + sendmail debug + rsh の brute force インターネットの ~10% を停止。世界初のインターネットワーム。CERT 設立の契機
1996 Aleph One "Smashing the Stack" (Phrack 49) スタック BOF の実例とシェルコード作成を初めて教科書的に解説 現代エクスプロイト技術の起点。今も読まれる古典文献
2001 Code Red (CVE-2001-0500) IIS の Index Server に対する BOF 35 万台の Windows サーバが感染、ホワイトハウスへ DDoS 攻撃を仕掛ける設計
2003 Slammer (SQL Slammer) (CVE-2002-0649) MS SQL Server 2000 に対する 376 byte の UDP BOF パケット 10 分で 7.5 万台に拡散、世界中のインターネット帯域を圧迫、ATM が停止
2014 Heartbleed (CVE-2014-0160) OpenSSL TLS heartbeatout-of-bounds READ (BOF の親戚) 17% の HTTPS サーバから秘密鍵 / セッション / パスワードがリーク可能 に。世界規模で証明書再発行
2017 WannaCry / EternalBlue (CVE-2017-0144) SMBv1 の構造体パース時のヒープ overflow 20 万台以上の Windows がランサムウェアに感染。NHS / 鉄道 / 自動車工場が停止
2024 glibc CVE-2024-2961 (iconv の ISO-2022-CN-EXT) glibc 内部バッファの out-of-bounds write PHP filter チェーンと組合せて RCE に発展する PoC が公開、複数の Web アプリで悪用可

「メモリ安全性脆弱性は 30 年で消えなかった」という事実が、Microsoft / Google が新規コードを Rust に切り替える強い動機になっている。Linux カーネルの Rust 受け入れ (2022)Windows カーネル一部の Rust 化Android の新規ネイティブコードを Rust 標準といった動きはすべてこの帰結。

6. 学び方 — 合法な練習プラットフォーム #

他人のシステムで試す = 不正アクセス禁止法意図的に脆弱に作られた練習環境で学ぶのが正しい入口:

プラットフォーム 内容 難度
pwn.college アリゾナ州立大学の無料コース。スタック BOF → ROP → カーネル exploit までカリキュラム完備 入門〜上級
pwnable.kr 韓国の老舗 pwn 専門サイト。level 別の脆弱バイナリに対するシェル奪取 入門〜上級
pwnable.tw 台湾の上級 pwn サイト。heap exploit / kernel exploit が充実 中級〜超上級
picoCTF カーネギーメロン主催の入門 CTF。毎年の問題が PicoGym で常時公開、pwn 問題も多数 入門
HackTheBox General challenges → Pwn カテゴリ 入門〜上級
OverTheWire (Narnia, Behemoth, Vortex) 古典的な BOF / format string の wargame 入門〜中級
Microcorruption Matasano の ARM ベース組込み機器 BOF wargame 入門〜中級
Exploit-Education (Phoenix, Nebula) exploit-exercises の後継。段階的に保護を上げていくシリーズ 入門〜中級

学習に必要なツール (Kali Linux にすべて入っている):

# 動的解析・デバッガ
gdb + pwndbg / GEF / peda    # 拡張済 GDB (現代 pwn の必需品)
strace -f ./vulnerable        # システムコールトレース
ltrace ./vulnerable           # ライブラリ関数呼出しトレース

# 静的解析・バイナリ解析
checksec --file=./bin         # 緩和策確認
ROPgadget --binary ./bin      # ROP gadget 列挙
ropper --file ./bin           # 同上、別実装
objdump -d ./bin | less       # 逆アセンブリ
radare2 ./bin / r2 ./bin      # 軽量リバースエンジニアリングプラットフォーム
ghidra                        # NSA 製 GUI 逆コンパイラ

# Exploit 開発
python3 + pwntools            # exploit script の事実上の標準ライブラリ
                              # = io 操作 + ROP chain 自動構築 + shellcode + 通信ラッパ
one_gadget ./libc.so.6        # libc 内の「1 アドレスで execve('/bin/sh') する」場所列挙

最小の pwntools exploit テンプレート:

from pwn import *

elf  = ELF("./vulnerable")
libc = ELF("./libc.so.6")
p    = process("./vulnerable")        # ローカル / remote("host", port) でリモート

# 1. info leak で libc ベースを取る
p.sendline(b"A" * 64 + p64(elf.plt["puts"]))
leak = u64(p.recvline().strip().ljust(8, b"\x00"))
libc_base = leak - libc.sym["puts"]

# 2. ROP chain 構築
rop = ROP(libc)
rop.raw(b"A" * 72)                    # padding to saved RIP
rop.system(next(libc.search(b"/bin/sh\x00")) + libc_base)
p.sendline(rop.chain())

p.interactive()                       # シェル取得

CTF / 練習プラットフォーム以外で実行しない本物のサービスに対して試した時点で違法雇用主の社内ペンテストでも書面同意 (RoE) が必須。Kali Linux 記事の「法と倫理」と同じ原則がここにも適用される。


Buffer Overflow は 「C/C++ にメモリ境界チェックがない」という 1970 年代の言語設計の帰結 が、インターネット規模で 30 年以上にわたって主要な攻撃面であり続けている事実そのものである。スタック BOF の力学 (strcpy で saved RIP を上書き → ret で制御奪取) と ヒープ BOF の発展 (chunk metadata 破壊 → write-where) は、緩和策の積み上げ (SSP / DEP / ASLR / PIE / CFI) と bypass の進化 (ROP / info leak / heap grooming) という軍拡競争の歴史として 1 枚絵で頭に入れておくと、現代の任意の memory-safety CVE writeup を読むときに地図がある状態で臨める。

最終的な解は 「メモリ安全な言語に書き直す」 だが、完全置換には 20 年以上かかることを前提に、緩和策のスタックを正しく有効化 (checksec で確認)、fuzzing で memory-safety バグを早期発見新規プロジェクトは Rust / Go から始める既存 C/C++ には bounds-checked な代替 API を選ぶ (strncpy_s / snprintf / Rust ラッパ / Google の bounds-checking patches) という現実的な多層防御が、2026 年時点の最も誠実な向き合い方である。