なぜバッファオーバーフローは起きるのか(episode-08)

黒ネコと学ぶ・論理的エクスプロイト開発への道

バッファオーバーフローという言葉を聞くと、多くの初心者は「なんとなく危険そう」「古い脆弱性」「難しそう」という印象を持ちがちです。
しかし実際には、バッファオーバーフローは、CPU・メモリ・スタックの基本構造を理解していないと必ず起きる「自然な事故」とも言えます。
episode-08では、
・バッファとは何か
・なぜ「はみ出す」ことが可能なのか
・なぜそれがクラッシュや乗っ取りにつながるのか
を、プログラムの視点で論理的に説明していきます。
 (※イメージです。)

バッファとは何か

バッファの正体

 バッファ(buffer)とは、
「あらかじめ確保されたメモリ領域」のことです。
C言語では、以下のような形で登場します。
 
char buf[16];
これは、"buf"という名前の16バイト分の連続したメモリを意味します。
 
重要なのは、CPUやメモリは、「ここまでが"buf"だよ」と境界を自動的に守ってはくれないという点です。
 

メモリは「ただの箱の並び」

 メモリの世界では、0番地、1番地、2番地・・・というように、連続したアドレスが並んでいるだけです。
“buf[16]" の直後には、別の変数、保存されたRBPや、リターンアドレスなどが、普通に隣接して存在しています。
[fuki-r]16バイトって決まってるのに、超えて書けちゃうの?[/fuki-r][fuki-l]はい。C言語は“書くな”とは言いません[/fuki-l]  

なぜ境界チェックが行われないのか

C言語は「高速・低レベル」を優先する

 C言語の設計思想は非常にシンプルです。
・高速に動く
・ハードウェアに近い
・余計なことはしない
 
そのため、
・配列の範囲チェック
・自動的な安全確認
は、一切行われません。
 
buf[100] = 'A’;
と書けば、100番目に書きに行く、そこが何であろうと関係ないという動作になります。
 

「プログラマが責任を持つ」という思想

 C言語では、「正しいサイズを使うかどうかはプログラマの責任」という前提があります。
つまり、
・入力が長すぎた
・サイズを確認しなかった
・想定外のデータが来た
という場合でも、プログラムは止めてくれません。
[fuki-r]C言語は、親切じゃない言語だね…[/fuki-r][fuki-l]その代わり、すべてを制御できるのです[/fuki-l]  

実際に何が「壊れる」のか

 スタック上の配置を思い出す

 episode-07で学んだスタックフレームを思い出してください。
一般的な関数のスタック構造(x86-64)は以下の順です。
[高アドレス]
引数
リターンアドレス
保存されたRBP
ローカル変数 ← buf がここにある
[低アドレス]
 
ここで、"buf" に想定以上のデータを書き込むと…
 

上書きの連鎖が起きると

1. buf を埋める
2. 隣のローカル変数が壊れる
3. 保存されたRBPが壊れる
4. リターンアドレスが壊れる
この流れは、非常に自然な結果です。
CPUはただ、
・「指定されたアドレスに書いた」
・「ret 命令でアドレスを取り出した」
だけなのです。
[fuki-r]わざとじゃなくても壊れちゃうんだ…[/fuki-r][fuki-l]はい。だから「脆弱性」なのです[/fuki-l]  

なぜクラッシュや乗っ取りにつながるのか

 クラッシュの正体

 リターンアドレスが壊れると、
・存在しないアドレスにジャンプ
・実行不可領域を実行
・Segmentation fault
(プログラムがアクセスを許可されていないメモリ領域にアクセスしようとしたときに発生するエラー)
といった結果になります。
 
これは、偶然起きた暴走です。
 

乗っ取りは「制御された偶然」

 一方、攻撃者はこう考えます。
・どこに buf があるか
・何バイトで return address に届くか
・どんな値を書けばいいか
つまり、「偶然壊れる場所」を「意図した場所」に変える。
これが「エクスプロイト」なんです。
[fuki-r]攻撃って、魔法みたいじゃないんだね[/fuki-r][fuki-l]ええ。すべては構造と計算です[/fuki-l]  

GDBで「起きている瞬間」を見る意味

 バッファオーバーフローは、頭で理解するだけでは足りません。
GDBを使うことで、
・bufの開始アドレス
・RBPとの距離
・どの入力で何が壊れるか
を、目で確認できます。
 
これにより、
・なぜ 4 バイト差なのか。
・なぜ 8 バイトではないのか。
といった疑問が、一つずつ解消されていきます。
[fuki-r]見ると、怖さより納得が増えるね[/fuki-r][fuki-l]理解は恐怖を減らします[/fuki-l]  

なぜバッファオーバーフローは起きるのかのまとめ

バッファオーバーフローは、メモリの境界を自動的にチェックしない設計から生まれる現象です。
C言語では、配列のサイズを超えて書き込むこと自体が禁止されておらず、プログラムは警告を出すことなくメモリを上書きしてしまいます。その結果、スタック上ではローカル変数から保存された RBP、さらにリターンアドレスへと、構造に従って順番に破壊が進んでいきます。

攻撃とは、この壊れ方を理解した上で、書き込む位置と内容を意図的に制御する行為で、GDBで実際に観察すると、バッファオーバーフローは偶然の事故ではなく、構造から必然的に起きる「理解可能な現象」であることが分かります。