CyberFix Note
セキュアコーディング

バッファオーバーフローの仕組みと対策。境界を越えた書き込みがコード実行に至る理由

対象の目安: C/C++など低レベル言語を扱う開発者 / 入門

ソウ攻撃・脆弱性リサーチ担当
・ 約10分で読めます
バッファオーバーフローの仕組みと対策。境界を越えた書き込みがコード実行に至る理由

確保したメモリ領域の大きさを超えてデータを書き込む。ただそれだけの操作が、プログラムのクラッシュから任意コードの実行まで、幅広い被害を生みます。これがバッファオーバーフローです。原理も対策も古くから知られていますが、C言語やC++のように開発者がメモリを直接管理する言語では、いまも作り込まれ続けている弱点です。

この記事では、境界を越えた書き込みがなぜメモリを壊すのかという原理から出発し、スタックとヒープで壊れ方がどう違うのか、上書きがどのようにして実行の流れを奪うのかを順に整理します。あわせて、開発時の境界チェックからビルドや実行環境の緩和、そしてメモリ安全な言語という根本的な方向性まで、開発者が取れる対策を段階的に扱います。なお、記載は執筆時点で確認できた範囲に限り、攻撃コードや悪用手順は示さず仕組みの説明にとどめます。

バッファオーバーフローが起きる原理

バッファとは、プログラムがデータを一時的に置くために確保した固定的なメモリ領域です。たとえば64バイト分のバッファを用意したなら、そこに書き込んでよいのは64バイトまでです。ところが、書き込む側が入力の長さを確かめないまま100バイトのデータをコピーすると、はみ出した36バイトはバッファの外、つまり隣接するメモリへ書き込まれます。この境界を越えた書き込みがバッファオーバーフローの本体です。

隣接領域には別の変数や、プログラムの動作を制御する情報が置かれていることがあります。そこが意図せず上書きされると、値が壊れて計算結果が狂ったり、後続の処理が壊れた情報を使ってしまったりします。上書きの内容と場所によっては、単なる誤作動を超えて制御そのものを奪われます。

MITRE CWE-120は、入力サイズを検証せずにバッファへコピーすることで境界を越えた書き込みが起きる弱点をClassic Buffer Overflowとして定義しています。

原因を突き詰めると、書き込むデータの長さと、書き込み先の容量が一致していない点に行き着きます。長さを確かめる仕組みが処理の中に無ければ、はみ出しは検知されずに進んでしまいます。

スタックとヒープで異なる壊れ方

バッファがどのメモリ領域に置かれているかで、あふれたときに壊れる対象が変わります。MITREはこれを下位分類として整理しており、スタック上のバッファがあふれる場合をCWE-121、ヒープ上のバッファがあふれる場合をCWE-122として区別します。

スタックは、関数の呼び出しと戻りを管理する領域です。ここには関数を呼んだ後どこへ戻るかを示す戻りアドレスや、保存されたレジスタなど、実行の流れを制御する情報が並びます。スタック上のバッファがあふれると、これらの制御情報が上書きされうるため、被害が制御奪取に直結しやすい特徴があります。

ヒープは、実行中に動的に確保されるメモリの領域です。ヒープ上のバッファがあふれると、隣にある別のオブジェクトや、確保と解放を管理するためのヒープ管理情報(メタデータ)が壊れます。壊れたメタデータが後の確保や解放の処理で使われることで、想定外の動作やさらなるメモリ破壊が連鎖することがあります。

MITRE CWE-787(Out-of-bounds Write)は境界外への書き込み全般を指す上位概念で、MITREのCWE Top 25で最上位級に位置づけられる危険な弱点です。

上書きがコード実行に至る経路

制御を奪われる古典的な経路は、スタックの戻りアドレスを狙うものです。関数は処理を終えると、スタックに保存された戻りアドレスを読み、その位置へ実行を移します。スタック上のバッファをあふれさせてこの戻りアドレスを攻撃者の用意した値に書き換えると、関数から戻る瞬間に、実行の流れが攻撃者の意図した位置へ移ってしまいます。ここから任意コードの実行につながりうるのが、この弱点の深刻さの中心です。

ヒープの場合は戻りアドレスを直接は持ちませんが、壊されたメタデータやオブジェクトが後続の処理で使われることで、間接的に制御が奪われたりメモリがさらに破壊されたりします。いずれの場合も、上書きされた値や壊れた構造が「その後どう使われるか」が被害の分かれ目になります。

ここで、よく似た名前の弱点と区別しておきます。境界を越えた読み出しはOut-of-bounds Read(CWE-125)で、こちらは書き込みではなくメモリを余分に読み取ってしまう欠陥です。読み出しは情報漏えい(バッファオーバーリード)につながる別種であり、メモリを壊して制御を奪う書き込み系のオーバーフローとは被害の質が異なります。両者を混同しないことが、対策を選ぶ際の前提になります。なお、メモリ安全性の欠陥は長く悪用されてきた弱点であり、確証のない過去事例を固有名で列挙することはここでは避けます。

作り込みやすい実装パターン

現実に多いのは、入力の長さを検証しないまま固定長バッファへコピーする処理です。外部から受け取った文字列を、容量を確かめずにそのまま格納すれば、入力が想定より長かった時点であふれます。

境界チェックを持たない古い文字列関数の不適切な利用も典型です。C言語のstrcpyやstrcat、sprintf、getsといった関数は、コピー先の容量を意識しないまま動くため、入力次第で容易にあふれます。ほかにも、書き込む長さと格納先の容量の対応がずれているケース、整数の扱いを誤って確保サイズを取り違えるケース(整数オーバーフロー起因)なども、境界を越えた書き込みの引き金になります。

共通するのは、信頼できない長さのデータを、容量の確認を挟まずにメモリ操作へ渡している点です。入力の検証をどこで行うかという設計判断は、この弱点だけでなく多くの脆弱性に共通する土台になります。

あわせて読みたい

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

開発時の対策

開発の段階で取れる対策は、境界を意識したメモリ操作を徹底することに集約されます。

  1. 1

    外部から受け取る入力の長さを、コピーや確保の前に検証する

  2. 2

    書き込み先の容量を超えないことを、境界チェックで明示的に確認する

  3. 3

    境界を意識した代替関数(strncpyやsnprintfなど)を使い、切り詰めやヌル終端の扱いに注意する

  4. 4

    配列アクセスでは、インデックスが範囲内に収まっているかを確認する

  5. 5

    信頼できない長さの値を、そのまま確保サイズやコピー長として使わない

代替関数を使う場合も万能ではありません。strncpyは指定長で切り詰めた結果ヌル終端されないことがあり、その先で文字列として扱うと別の問題を招きます。関数を置き換えるだけで安心せず、長さと終端の扱いを一つずつ確かめる姿勢が求められます。こうした入力の扱い方は、シェルへ渡す値の検証など他の脆弱性対策とも地続きです。

あわせて読みたい

OSコマンドインジェクションの仕組みと根本対策。シェルに渡さず引数を分離する

ビルドと実行環境の緩和

ソースコードの対策に加えて、ビルドや実行環境の側で悪用を難しくする仕組みがあります。代表的なものとして、スタックの制御情報が壊されたことを検知するスタックカナリア(Stack Smashing Protector)、メモリの配置を実行のたびに変えて攻撃者が狙う位置を予測しにくくするASLR(アドレス空間配置のランダム化)、データ領域でのコード実行を禁じるDEP/NX、コンパイル時に安全な処理へ差し替えるFORTIFY_SOURCEなどがあります。

これらは有効ですが、位置づけを正しく理解しておく必要があります。いずれも悪用の難易度を上げる緩和であって、バッファオーバーフローそのものを無くす対策ではありません。欠陥がコードに残っている限り、緩和をすり抜ける手法との攻防が続きます。緩和はあくまで多層防御の一枚であり、根本の作り込みを減らす努力と併せて意味を持ちます。

検出の面では、想定外の入力を大量に投げて異常を探すファジング、コードを実行せずに危険なパターンを見つける静的解析、実行時にメモリ違反を捕らえるAddressSanitizerのような動的検査が役立ちます。開発と検証の両輪で、あふれの芽を早い段階で見つけることが現実的です。

根本的な対処としてのメモリ安全な言語

境界チェックを開発者が漏れなく書き続けるのは難しく、人手に頼る限り作り込みはゼロになりません。そこで近年強く推されているのが、言語のレベルでメモリ破壊系のバグを防ぐという方向です。

CISAはSecure by Designの方針のもと、バッファオーバーフローを設計で除ける欠陥と位置づけ、RustやGo、Java、Swift、C#、Pythonといったメモリ安全な言語への移行を推奨しています。CISAとNSAはメモリ安全性の重要性を繰り返し示しています。

メモリ安全な言語は、配列の範囲外アクセスを言語やランタイムの仕組みで検知したり、そもそも直接のメモリ操作を制限したりすることで、メモリ破壊系のバグの大きな割合を防ぐとされます。既存のC言語やC++の資産をすべて置き換えるのは容易ではありませんが、新規に書く部分や被害の大きい部分から段階的に移していく判断は、長期的な弱点の削減につながります。こうした設計段階での脆弱性削減の考え方は、Webアプリの代表的なリスクを扱う枠組みとも通じます。

あわせて読みたい

OWASP Top 10とは。開発者が押さえるべきWebの代表的リスクと対策を一気に理解する

確認チェックリスト

開発やレビューの場で見返せるよう、要点をまとめます。

  • 外部入力の長さを、コピーや確保の前に検証しているか
  • 固定長バッファへの書き込みで、容量を超えないことを境界チェックで確認しているか
  • 境界チェックの無い古い文字列関数の利用が残っていないか
  • 代替関数を使う場合、切り詰めやヌル終端の扱いを確認しているか
  • 確保サイズの計算で整数の扱いを誤っていないか
  • スタックカナリアやASLR、DEP/NXなどの緩和が有効になっているか
  • ファジングや静的解析、動的検査を検証の工程に組み込んでいるか
  • 新規開発や重要部分で、メモリ安全な言語の採用を検討したか

バッファオーバーフローは、境界を越えた書き込みという一点から始まり、上書きの場所と使われ方次第でクラッシュから制御奪取まで被害が広がります。だからこそ、入力長の検証と境界チェックという基本を守りつつ、緩和機能で多層に守り、可能な部分はメモリ安全な言語で根本から避ける、という段階的な組み立てが有効です。原理を理解しておくことが、どの対策がなぜ効くのかを見極める助けになります。

出典・参考

この記事をシェア

関連する記事

セキュアコーディング

入力バリデーションと出力エスケープの原則。入口で検証し、出口で文脈別にエスケープする

セキュアコーディングの土台である入力バリデーションと出力エスケープを、それぞれの役割の違いから整理します。入口の検証は防御の一段目、出口の文脈別エスケープがXSSやインジェクションの根本対策である理由を、原理と実務の判断基準まで掘り下げます。

セキュアコーディング

OSコマンドインジェクションの仕組みと根本対策。シェルに渡さず引数を分離する

OSコマンドインジェクション(CWE-78)が外部入力をシェルコマンドとして解釈させて成立する機構、OWASPのインジェクション分類での位置づけ、そしてシェルを介さず引数を配列で渡す根本対策までを開発者向けに整理します。入力検証が補助にとどまる理由も扱います。