Buffer Overflow #
Buffer Overflow (バッファオーバーフロー, BOF) は、「確保されたバッファのサイズを超えてデータを書き込み、隣接するメモリ領域を破壊する」ことから生じるメモリ安全性脆弱性の古典中の古典である。1988 年の Morris Worm が fingerd の gets() を踏み台にしてインターネットを停止させて以来、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) を関数呼び出し時のスタックフレーム構造から解剖する。ローカル変数のバッファ + 戻りアドレスの位置関係こそ、攻撃の発火点。
具体的な脆弱コードと 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) に発展できる。
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) がこれ。書き込みでなく読み取りで隣接メモリの中身が漏れる
これら全部が「ポインタの正しさをコンパイラがチェックしないから起きる」という同じ根に繋がる。
4. 緩和策と bypass の軍拡競争 #
BOF は完全には消せないため、OS とコンパイラとハードウェアが多層的に緩和策を積み上げてきた。それぞれの緩和策には bypass があることを知っておくと、現代の 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 | 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 | 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 heartbeat の out-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 年時点の最も誠実な向き合い方である。