【入門編】 Solidityにおける整数オーバーフロー/アンダーフローの歴史的背景と防御 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

こんにちは!セキュリティリサーチャーの私です。普段は工場を動かすOT(制御システム)の裏側や、ブロックチェーン上のスマートコントラクトを隅々まで解体して、泥臭い脆弱性を見つけ出す仕事をしています。

さて、今回はブロックチェーンの世界、特にSolidityにおける「整数オーバーフロー/アンダーフロー」という歴史的なテーマについてお話しします。

「小難しいセキュリティ用語だなぁ…」と思ったそこのあなた、安心してください!今回は、身の回りの防犯の仕組みや「家の鍵」に例えて、一歩ずつ優しく紐解いていきます。新人のIT担当者の方も、セキュリティに初めて触れる開発者の方も、ぜひ最後までリラックスして読んでいってくださいね。

—

1. 泥棒もびっくり?「数字のメーター」が一周しちゃう不思議な世界

まずは、スマートコントラクトの大前提のお話から。ブロックチェーン上で動くプログラム(Solidity)の世界では、私たちが普段使っている無限に大きくなれる数字とはちょっと勝手が違います。

ここで、身の回りの身近な例えを思い出してみましょう。
実家の古い水道メーターや、昔の車のオドメーター(走行距離計)をイメージしてみてください。あれって、99999キロメートルの次はどうなるか知っていますか? そう、綺麗に00000に戻ってしまいますよね。

これが、コンピュータの世界における「桁あふれ(オーバーフロー)」の基本原理です。

Solidityの古いバージョン(0.8.0未満)では、数字を格納する箱(変数)の大きさがきっちり決まっていました。例えば、uint8という箱は、0から255までの数字しか入れられません。
もし、この箱がすでに255でパンパンの状態で、さらに「+1」の命令を与えるとどうなるでしょうか?

  • オーバーフローの瞬間: 255 + 1 = 0 に逆戻り!

逆に、0の状態で「-1」を引こうとすると、今度は反対側にぐるっと回ってしまいます(アンダーフロー)。

  • アンダーフローの瞬間: 0 - 1 = 255 (持っていなかったはずの莫大な残高が突如出現!)

現実の金庫で考えてみてください。1円も入っていない貯金箱から無理やり1円引き出そうとしたら、泥棒が大金持ちになってしまう……そんなSFみたいなバグが、昔のスマートコントラクトの世界では実際に起きていたのです。これが、悪意あるハッカーたちに悪用されてきた「整数オーバーフロー/アンダーフロー」の正体です。

—

2. 昔の現場はどう戦っていた?「SafeMath」という名の頑丈な補助錠

「じゃあ、昔の開発者はどうやってこのバグを防いでいたの?」気になりますよね。

Solidity 0.8.0よりも前の時代、現場の開発者たちは「SafeMath(セーフ・マ、数学の安全装置)」という外部ライブラリを盾にして戦っていました。これは言ってみれば、数字の計算をする前に「おい、この計算をしたらメーターが一周しちまうか?」と毎回厳重にチェックしてくれる「高性能な防犯センサー付きの鍵」のようなものです。

当時のコードをちょっと覗いてみましょう。

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

// OpenZeppelinが提供していた伝説的なSafeMathライブラリ(古いバージョン用)
import "@openzeppelin/contracts/math/SafeMath.sol";

contract OldSchoolVault {
    // ライブラリを uint256 型に紐付ける
    using SafeMath for uint256;

    mapping(address => uint256) public balances;

    function deposit() public payable {
        // SafeMathの .add() を使うことで、オーバーフローを自動検知して爆発(エラー終了)させる
        balances[msg.sender] = balances[msg.sender].add(msg.value);
    }

    function withdraw(uint256 _amount) public {
        // こちらはアンダーフローを防ぐための .sub()
        require(balances[msg.sender] >= _amount, "残高が足りません");
        balances[msg.sender] = balances[msg.sender].sub(_amount);
    }
}

このコードでは、通常の足し算記号(+)や引き算記号(-)の代わりに、.add()や.sub()という専用の関数を使っていますよね。もし計算結果がおかしくなりそうになると、このライブラリが瞬時にトランザクションを拒否(revert)して、泥棒の侵入をピシャリと防いでくれていたのです。

ただ、現場のエンジニアとしては、全ての足し算・引き算のたびに.add()や.sub()を書くのが正直ちょっと面倒くさかったり、書き忘れてヒヤッとしたりすることも少なくありませんでした。人間だもの、うっかりミスはどうしても起きちゃいますよね。

—

3. Solidity 0.8.0からの大進化:標準装備された「自動防犯システム」

「うっかりミスを防ぎたい!」という世界中の開発者の願いに応えるかのように、2020年末、Solidityのバージョン0.8.0で歴史的な大改修が行われました。

なんと、コンパイラ自体にオーバーフロー/アンダーフローのチェック機能が標準搭載(デフォルトで有効)されたのです!

これにより、現代のSolidity(0.8.0以降)では、特別なライブラリを使わなくても、普通の演算子がそのまま安全な防犯システムとして機能するようになりました。

実際の書き方を見てみましょう。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; // 0.8.0以降の安全な世界

contract ModernVault {
    mapping(address => uint256) public balances;

    function deposit() public payable {
        // 普通の「+」演算子でOK!
        // もしオーバーフローしそうになったら、自動的に処理が中断(Revert)されます
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 _amount) public {
        // 普通の「-」演算子でOK!
        // アンダーフローしそうになっても自動でブロックされます
        balances[msg.sender] -= _amount;
    }
}

すごいですよね!開発者が特別なライブラリをインポートし忘れる心配もなくなりました。家の玄関に、最初から超高性能なオートロックと防犯カメラが標準で備え付けられているようなものです。

—

4. 「でも、あえて鍵を外したい時もある?」ガス代と例外処理のリアル

さて、ここで現場のプロならではの裏話をひとつ。
「じゃあ、もう昔の危険な計算方法は完全に忘れていいんだね?」と思われるかもしれませんが、実はそうでもありません。

現代のSolidity 0.8.0以降でも、あえて自動チェックをオフにしたいシチュエーションが存在します。それが「unchecked(チェックなし)ブロック」です。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GasOptimizedCounter {
    uint256 public counter = 0;

    function incrementLoop() public {
        // わざとオーバーフローさせることが絶対に安全だと分かっているループ処理など
        unchecked {
            // ここの中では、オーバーフローの自動チェックが行われません
            // その分、ブロックチェーン上で消費する「ガス代(手数料)」を節約できます
            counter += 1;
        }
    }
}

なぜわざわざそんな危険な真似をするかと言うと、ブロックチェーンの世界では「計算の安全確認=ガスの消費量(手数料の増加)」に直結するからです。例えば、ループ処理の中で「どう転んでもオーバーフローしない」と数学的に証明できるカウンターを回す時などは、チェックを外してガス代を安く抑えるテクニックが使われます。

ただし、これは「家の頑丈な二重ロックを、自分の責任で一時的に外す」ようなもの。現場のエンジニアリングとしては、本当に安全が確証されている場所以外でuncheckedを使うのは禁忌とされています。

—

5. 一歩ずつ、安全なスマートコントラクト開発へ

今回は、整数オーバーフロー/アンダーフローの歴史的背景から、SafeMathの役割、そして最新のSolidityにおける防御の仕組みまでを紐解いてみました。

  • 古いバージョン(〜0.7.x): SafeMathライブラリを使って手動で守る必要があった。
  • 新しいバージョン(0.8.0〜): 自動でチェックしてくれるが、ガス代節約のためにuncheckedを使う場合は細心の注意が必要。

セキュリティの世界は日進月歩ですが、根本にある「どこにリスクが潜んでいて、どうやって備えるべきか」という考え方は、現実世界の防犯とまったく同じです。

焦らず、一歩ずつコードの仕組みを理解しながら、安全で堅牢なスマートコントラクトを一緒に作っていきましょう!それではまた次回のセキュリティ解説でお会いしましょう。

コメント

タイトルとURLをコピーしました