整数オーバーフローと型混同の脆弱性原理
なぜサイズ計算1つのミスがメモリ破壊に化けるのか。桁あふれが境界チェックをすり抜け、型混同が異なる型としてメモリを誤読する仕組みを、原理から追える。
- 1.整数オーバーフローは固定幅整数が表現範囲を超えて折り返す現象で、長さ・サイズ計算に紛れ込むと「小さい値」に化けて境界チェックを通過させ、過少確保バッファへの書き込み(ヒープオーバーフロー)を招く。
- 2.符号の取り違え(signed/unsigned 混在)も致命的で、負数が unsigned 比較で巨大値に解釈され、size_t を取る memcpy などへ渡ると大量コピーを引き起こす。
- 3.型混同は、あるメモリ領域を本来と異なる型として解釈する欠陥。仮想関数テーブル(vtable)ポインタやサイズ情報を誤読させ、攻撃者制御の制御フローや領域外アクセスに直結する。
なぜ「計算ミス」がメモリ破壊になるのか
メモリ破壊の入口は、必ずしも書き込みループそのものではありません。多くの実世界の脆弱性は、書き込み量や確保サイズを決める「整数計算」が誤ることから始まります。C/C++ の固定幅整数(int は典型的に 32 ビット、size_t はポインタ幅)は、表現範囲を超えると例外を出さず**静かに折り返す(wrap around)**ためです。
この静かな折り返しが危険なのは、サイズ計算の結果が、その直後の境界チェックや malloc の引数として使われるからです。チェックを通った「正しそうな小さい値」が、実際には攻撃者が意図した過少値である――この食い違いが、確保した領域を超える書き込みへとつながります。型混同はこれと並ぶもう一つの根因で、こちらはメモリの「解釈」を誤らせることで同種の破壊に至ります。本記事はメモリ破壊攻撃の原理で扱った「溢れた後どう乗っ取るか」の**手前にある「なぜ溢れるか」**を掘り下げます。
解説は防御・教育目的です。攻撃手順ではなく、なぜ成立しどこを直せば塞げるかという原理に絞ります。検証は許可された環境でのみ行ってください。前提として、固定幅の2の補数表現と、符号付き/符号なし整数の比較規則を扱います。
桁あふれ(unsigned wrap)が境界チェックを無効化する
符号なし整数の演算は 2 の n 乗を法とするモジュラ演算です。32 ビット unsigned なら、結果は常に 2^32 で割った余りに丸められます。つまり大きな値どうしの加算・乗算が、ラップして小さな値に化けることがあります。
典型は「ヘッダで指定された個数 × 要素サイズ」をそのまま確保サイズにする実装です。
/* count は攻撃者が制御するヘッダ由来の値(uint32_t) */
uint32_t count = read_u32(input);
size_t size = count * sizeof(record_t); /* ← ここで乗算がラップし得る */
record_t *buf = malloc(size); /* 過少サイズで確保される */
for (uint32_t i = 0; i < count; i++)
buf[i] = parse_record(input); /* count 回書く → 領域外書き込み */
sizeof(record_t) が 16、count が 0x10000001(約 2.7 億)なら、本来必要なのは約 4.3 GB です。ところが 32 ビット幅で計算すると 0x10000001 * 16 = 0x100000010 が 2^32 を超え、下位 32 ビットだけが残って size は 16 になります。malloc(16) は成功し、その後ループは count 回(約 2.7 億回)書き込むため、確保領域を遥かに超えてヒープを破壊します。これがサイズ計算ミス由来のヒープオーバーフローの核心です。
ここで重要なのは、if (size > LIMIT) のようなチェックを置いても無意味な点です。チェックが見るのは既にラップした後の小さい size であり、巨大な実需要量ではないからです。検査が「実際の意図」ではなく「壊れた計算結果」を見ているため、防御として機能しません。
符号の取り違え(signed/unsigned 混同)
桁あふれと並ぶ古典が符号の取り違えです。C の整数昇格・変換規則では、符号付きと符号なしを比較すると符号付き側が符号なしに変換されます。負数は 2 の補数のビット列のまま巨大な正数として解釈されます。
/* len は long(符号付き)、攻撃者が負数を送り込めるとする */
int recv_into(char *dst, long len) {
if (len > BUFSZ) return -1; /* 上限チェックのつもり */
memcpy(dst, src, len); /* memcpy の第3引数は size_t(符号なし) */
return 0;
}
len に -1 を渡すと、len > BUFSZ の比較では -1 は小さいのでチェックを通過します。ところが memcpy の第 3 引数は size_t(符号なし)で、-1 は SIZE_MAX(64 ビットなら約 1844 京)に化けます。結果、memcpy は事実上「メモリ全域をコピーせよ」と命じられ、即座に巨大な領域外アクセスを起こします。下限チェック(len < 0 の拒否)を入れ忘れた上限だけのチェックは、こうして容易に破られます。
安全な比較の鍵は、検査する値の型と、それが最終的に使われる API の引数型を揃えることです。長さは最初から size_t で扱い、負値が入り得る経路では len < 0 を明示的に弾く。a + b や a * b を確保サイズに使う前に、a > SIZE_MAX - b(加算)や a > SIZE_MAX / b(乗算)でオーバーフローを事前検査する。コンパイラ組み込みの __builtin_mul_overflow や、calloc のように内部で乗算オーバーフローを検査する API を使うのが定石です。
型混同(type confusion)がメモリ破壊に至る原理
型混同は、整数あふれとは別系統ですが、同じく「メモリの誤った扱い」という根を持ちます。あるメモリ領域を、確保・初期化されたときの型 A とは異なる型 B として解釈・操作する欠陥です。型 A と型 B でフィールドの並びやサイズが異なれば、型 B のつもりで読み書きしたバイトが、型 A の別の意味を持つ領域(特にポインタ)を踏みます。
最も危険なのは、ポリモーフィズムを持つ C++ オブジェクトの先頭にあるvtable ポインタを誤読・上書きさせる経路です。
本来の型 A(Widget)の先頭 攻撃者が B として上書きした像
+------------------+ +------------------+
| vtable ポインタ | ← 正規 | 攻撃者が選んだ値 | ← 偽 vtable を指す
+------------------+ +------------------+
| フィールド ... | | データ ... |
+------------------+ +------------------+
obj->virtual_method() は 仮想呼び出しが攻撃者の
[vtable+offset] を間接ジャンプ 用意した関数ポインタへ飛ぶ
C++ の仮想メソッド呼び出しは「オブジェクト先頭の vtable ポインタを読み → そのテーブルの所定オフセットにある関数ポインタへ間接ジャンプ」という間接呼び出しです。型混同で先頭バイト列を攻撃者が制御できれば、vtable ポインタを偽テーブルへ向け、仮想呼び出し1回で制御を奪えます。これはメモリ破壊攻撃の原理で見た「制御フローがメモリ上のデータとして存在する」性質の、別の表れ方です。
型混同の発生源は多岐にわたります。
- 不正なキャスト:
reinterpret_castや C スタイルキャストで基底型を別系統の派生型へ強制変換する。実行時型情報(RTTI)を確認しないstatic_castの誤用も同様。 - union の取り違え:あるメンバで書き、別の型のメンバで読む。タグ(どのメンバが有効か)を検査しない実装で起きる。
- 逐次化/逆シリアライズ:受信データの型タグを信用し、想定外の型として復元する。JavaScript エンジンや言語ランタイムで頻出し、JIT の型推測(type speculation)が裏切られる経路が典型。
型混同はポインタだけの問題ではありません。型 A が「長さ 4 のヘッダ+本体」、型 B が「長さ 8 のヘッダ+本体」のようにメタデータのレイアウトが違う場合、B のつもりで読んだ長さフィールドが A の本体バイトを指し、攻撃者制御の巨大な長さに化けます。すると、その長さで行う後続のコピーやインデックス計算が領域外アクセスになります。つまり型混同は、整数あふれと合流してヒープオーバーフローを引き起こすこともあります。
整数あふれと型混同の関係を整理する
両者は独立に見えますが、「信頼できない入力が、サイズや解釈を決める値に流れ込む」という共通構造を持ちます。違いは、壊すのが「数値計算」か「型の解釈」かです。
| 観点 | 整数オーバーフロー / 符号混同 | 型混同(type confusion) |
|---|---|---|
| 壊れるもの | サイズ・長さの数値計算 | メモリ領域の型としての解釈 |
| 典型の入口 | count × size の乗算、負長の符号変換 | 不正キャスト、union 誤読、逆シリアライズ |
| 直接の結果 | 過少確保・過大コピー → 領域外書き込み | vtable ポインタ誤読・誤った長さ参照 |
| 最終被害 | ヒープオーバーフローによるメモリ破壊 | 制御フロー奪取または領域外アクセス |
| 根治の方向 | オーバーフロー検査付き計算・型統一 | 型安全な設計・タグ検査・RTTI |
緩和の発想はメモリ破壊攻撃の原理と地続きで、DEP/NX・ASLR・カナリアは最終段の悪用を高コスト化します。ただし整数あふれ・型混同はより上流の論理欠陥なので、配置のランダム化では「溢れること自体」を防げません。根は計算と型の扱いにあります。
(1) 符号なし整数は折り返す(wrap)ため、count * size の乗算がラップして過少確保になり、後続のチェックは壊れた小さい値を見るので無意味。(2) 符号付きと符号なしの比較は符号付き側が符号なしに変換され、負長が size_t の巨大値に化けて memcpy を暴走させる。(3) 型混同は領域を別型として解釈する欠陥で、C++ では vtable ポインタ誤読が制御奪取に直結。(4) 防御は、長さを size_t で一貫させ下限を検査、乗算前にオーバーフロー検査、calloc/__builtin_*_overflow を使い、型はタグ・RTTI で検証すること。
実務での向き合い方
根治は、**サイズ計算と型の扱いを「信頼できない入力に対して安全」**に設計することです。最終段の緩和に頼り切らない姿勢が要点になります。
- 計算を安全にする:確保サイズは
size_tで統一し、加算・乗算の前にオーバーフローを検査する。calloc(count, size)(内部で乗算検査)や__builtin_mul_overflowを使い、生のcount * sizeを書かない。負長が入り得る経路は< 0を明示的に弾く。 - 型を安全にする:危険なキャスト(
reinterpret_cast、C スタイルキャスト)を避け、多態の境界では RTTI(dynamic_cast)やタグ付き union(std::variant)で型を検証する。逆シリアライズは受信側でスキーマと型タグを必ず検査する。 - 言語・ツールで網を張る:Rust など整数オーバーフローをデバッグ時に panic させ、型安全をコンパイル時に担保する言語へ移す。C/C++ では UBSan(
-fsanitize=integer,bounds)や ASan、fuzzing で開発段階に検出する。
これらは設計と実装で「壊れた値が生まれないこと」を狙うもので、運用面では最小権限の原則でプロセス権限を絞り、万一の破壊が成立しても被害範囲を抑える多層構えが効きます。検出の確からしさはペネトレーションテストによる実測と組み合わせて担保するのが実務の定石です。
セキュリティ Article
整数オーバーフローと型混同の脆弱性原理を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
整数オーバーフロー
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
符号の取り違え(signed/unsigned 混在)も致命的で、負数が unsigned 比較で巨大値に解釈され、size_t を取る memcpy などへ渡ると大量コピーを引き起こす。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「整数オーバーフロー / 型混同」に近いか確認する。
- 強みである「整数オーバーフローは固定幅整数が表現範囲を超えて折り返す現象で、長さ・サイズ計算に紛れ込むと「小さい値」に化けて境界チェックを通過させ、過少確保バッファへの書き込み(ヒープオーバーフロー)を招く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。