はじめに
C/C++の世界では、バッファオーバーフロー(CWE-120)は攻撃者による任意のコード実行を可能にする脅威の高い脆弱性として知られています。バッファオーバーフローに至る例の多くでは、整数の不適切な取り扱いによる整数オーバーフロー(CWE-190)が原因になっています。最初に次のコードを見てみましょう。
public static void main(String[] args) { long OVERFLOW = 24 * 60 * 60 * 1000 * 1000; System.out.println("One day is :" + OVERFLOW + " microseconds."); } }
これは、1日をマイクロ秒(マイクロ秒は100万分の1秒)に換算する計算を行うコードです。実行すると以下が出力されます。
One day is 500654080 microseconds.
1日24時間は、24×60×60=86,400秒。1秒は1,000,000マイクロ秒なので、86,400×1,000,000=86,400,000,000が出力されるはずでした。なぜこのような結果が出力されてしまったのでしょう。
Javaと整数型
今回問題となるのはJavaにおける整数の取り扱いです。Javaのデータ型をまとめると以下の図のようになります。
Javaの整数型(Integral Type)には、符号付き整数型であるbyte、short、int、longと、符号なし整数型であるcharの4つの型が存在します。前述のコード
long OVERFLOW = 24 * 60 * 60 * 1000 * 1000;
では、整数リテラル同士のかけ算の結果がlong型変数に代入されています。問題はこのかけ算です。Java言語仕様§3.10.1「整数リテラル」には次のように書かれています。
整数リテラルは、ASCII文字Lまたはl(小文字のエル)が末尾にある場合、long型となり、それ以外の場合は、int型となる。
つまり、右辺の24、60、1000といった整数はint型として解釈され、かけ算はint型の幅で演算されるということです。Javaのint型は、2の補数で表現される32ビット符号付き整数なので、表現できる値の範囲は-2,147,483,648から2,147,483,647です。一方、先ほどのかけ算の結果の値86,400,000,000は、int型の最大値を超えるため、整数オーバーフローが発生します。結果として、ラップアラウンドしたint型の値500,654,080が出力されたというわけです。
このような単純な整数リテラル同士のかけ算であれば、オペランドの一つに接尾辞Lをつけるか、あるいはかけ算の一つをlongにキャストすることで、かけ算がlong型のビット幅で行われるようになり、オーバーフローを防ぐことができます(これを「アップキャスト」と呼びます)。
long NO_OVERFLOW = 24L * 60 * 60 * 1000 * 1000; // あるいは (long)24 * 60 * ...
様々な整数型変数が混在する複雑な式になると、整数オーバーフローは発見しづらくなります。また、整数値がプログラムの外部から取得される場合、攻撃に悪用される危険もあるでしょう。