こんにちは!Web3やIoT/OT(制御システム)のセキュリティ現場で、日々スマートコントラクトのコードや物理デバイスと格闘しているセキュリティリサーチャーです。
「ブロックチェーン」や「スマートコントラクト」と聞くと、なんだか難しそうで、一歩引いてしまう方も多いのではないでしょうか?でも、安心してください。スマートコントラクトのセキュリティは、私たちが普段行っている「家の防犯対策」と全く同じ考え方で理解できるんです。
今回は、スマートコントラクト開発(Solidity)の世界でかつて大流行し、今でも形を変えて潜み続ける「整数オーバーフロー・アンダーフロー」という脆弱性について、初心者の方にも分かりやすく、身近な例えを交えて一歩ずつ丁寧に解説していきます!
—
そもそも「オーバーフロー・アンダーフロー」ってなに?
まずは、この難しそうな言葉を、私たちの身の回りにあるもので例えてみましょう。
ダイヤル式の自転車の鍵を思い出してみよう
みなさんは、自転車の「ダイヤル式のワイヤーロック」を使ったことはありますか?
「0」から「9」までの数字が書かれたホイールが並んでいる、あの鍵です。
ここで、ちょっとした実験をイメージしてみてください。
- ダイヤルが「9」のときに、さらに1つ右に回すとどうなりますか?
── そう、「0」に戻りますよね。これが「オーバーフロー(桁あふれ)」です。
- 逆に、ダイヤルが「0」のときに、1つ左に回すとどうなりますか?
── 「9」になりますよね。これが「アンダーフロー(桁不足)」です。
数学の世界では「9の次は10」「0の前はマイナス1」ですが、コンピューター(プログラム)の世界、特にスマートコントラクトで扱う数値には「しまえる箱の大きさ(上限と下限)」が決まっています。
この上限を超えて値が最小値に戻ってしまったり、下限を突き抜けて最大値になってしまったりする現象を、それぞれオーバーフロー、アンダーフローと呼びます。
泥棒(ハッカー)はどうやってこれを狙うの?
もし、あなたのお財布(口座残高)のプログラムにこの仕組みが悪用されたらどうなるでしょうか?
例えば、あなたの口座残高が「0円」だったとします。
ここで、悪意のあるプログラムや操作によって「1円」を引かれてしまった(アンダーフローした)とします。
本来なら「マイナス1円」になるはずですが、プログラムがマイナスの数を取り扱えない設定(uint という符号なし整数型)だった場合、なんと残高は「コンピューターが扱える最大値(例えば、数京円や数無量大数円!)」に跳ね上がってしまうのです!
泥棒(ハッカー)は、この「一瞬で残高が無限に増えるバグ」を狙って、スマートコントラクトを攻撃し、巨額の暗号資産を盗み出してきました。これまでに、この単純な計算ミスだけで数億、数十億円規模の被害が出たインシデントが実際にいくつも発生しています。
—
昔の対策:わざわざ後付けしていた補助錠「SafeMath」
Solidityというプログラミング言語のバージョンが「0.8.0」より前だった時代、このオーバーフローやアンダーフローは、言語の仕様上「自動的には防げない」ものでした。
つまり、開発者が自分で気をつけて防犯対策(コードのチェック)を書く必要があったのです。
そこで登場したのが、SafeMath(セーフマス)というライブラリ(防犯用の補助錠パーツ)でした。
SafeMathを使った昔の書き方
開発者は、足し算や引き算を行うたびに、普通の + や - を使わず、以下のように特別な命令を呼び出していました。
// 【昔の書き方(Solidity 0.8未満)】
// SafeMathという補助錠をインポートして使っていました
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract OldToken {
using SafeMath for uint256; // uint256型にSafeMathを適用するよ、という宣言
mapping(address => uint256) public balances;
function transfer(address _to, uint256 _value) public {
// 普通のマイナス(-)ではなく、.sub() という安全な引き算関数を使う
balances[msg.sender] = balances[msg.sender].sub(_value);
// 普通のプラス(+)ではなく、.add() という安全な足し算関数を使う
balances[_to] = balances[_to].add(_value);
}
}
この sub や add という関数は、計算を行う前に「もし引き算して0を下回ったら、処理を強制終了して元に戻す(リバートする)」というチェックを裏側で毎回行ってくれる優れものでした。
しかし、すべての計算箇所にこの補助錠を取り付けるのは、開発者にとって非常に面倒で、一箇所でも付け忘れると、そこが泥棒の侵入口(脆弱性)になってしまうというリスクがあったのです。
—
Solidity 0.8以降の革命:標準装備された「オートロック」
ここで大きな転換期が訪れます。
2020年12月にリリースされた Solidity 0.8.0 以降、この状況はガラリと変わりました。
なんと、コンパイラ(プログラムを翻訳するシステム)自体に、「オーバーフロー・アンダーフローの自動チェック機能」が標準装備されたのです!
これは例えるなら、「すべての家のドアに、最初から超強力なオートロック機能が標準で組み込まれた」ようなものです。
現代の書き方(Solidity 0.8以降)
今では、面倒な SafeMath をインポートする必要は一切ありません。普通の + や - を使って計算を書くだけで、システムが自動的に見張ってくれます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; // Solidity 0.8.0 以降を指定
contract ModernToken {
mapping(address => uint256) public balances;
// 残高を減らす関数
function withdraw(uint256 _amount) public {
// もし残高(balances[msg.sender])が _amount より少ない場合、
// Solidity 0.8以降では、引き算を実行した瞬間に自動的にエラーとなり、
// トランザクション(処理)全体が安全にキャンセル(リバート)されます!
balances[msg.sender] -= _amount;
}
}
特別なライブラリを呼ばなくても、-= や += の計算で異常が起きれば、プログラムが「おっと、これ以上は引き算できないよ!」と自動的に処理を止めてくれます。
これで、開発者がうっかり鍵をかけ忘れて、泥棒に資産を盗まれる心配は劇的に減りました。
—
例外的なケース:あえてオートロックを外す unchecked
「じゃあ、もうセキュリティ対策は完璧だね!何も気にしなくていいんだ!」と思うかもしれません。
しかし、スマートコントラクトの開発には、ブロックチェーン特有の「ガス代(手数料)」という大きな壁が存在します。
実は、Solidity 0.8が自動で行ってくれる「オートロック(計算の事前チェック)」は、非常に安全ですが、その都度「裏側でコンピューターがチェックするためのガス代(手数料)」を消費しています。
「ここは100%絶対にオーバーフローが起きないって分かっている場所なんだけどな……毎回チェックされるのは手数料がもったいないな……」
そんな時に使うのが、unchecked(アンチェックド)ブロックです。
unchecked の使い方
unchecked で囲まれた部分は、一時的に「オートロック(自動安全チェック)」が無効化され、Solidity 0.8未満と同じように、ガス代を節約して高速に計算を行うことができます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GasOptimized {
// 1から10まで順番に足していくような、絶対にオーバーフローしないループ処理
function sumToTen() public pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 1; i <= 10; i++) {
// ここは絶対に値が爆発しない(上限を超えない)と分かっている
// そのため、あえて unchecked を使ってガス代を節約します
unchecked {
sum += i;
}
}
return sum;
}
}
【現場リサーチャーの警鐘】unchecked を使うときの注意点
unchecked は、いわば「防犯カメラやオートロックを一時的に止めて、風通しを良くする行為」です。
現場のインシデント(事故)対応でも、「少しでもユーザーのガス代を安くしてあげたい」という開発者の親切心から unchecked を多用し、その中に外部から操作できる変数が混ざっていたために、結局ハッカーにアンダーフローを突かれて数億円を盗まれてしまった……という、本末転倒なケースが後を絶ちません。
実務で unchecked を使う際は、以下のルールを徹底しましょう:
1. ユーザーが入力できる値(引数など)が計算に関わっていないか?
2. 直前で、値の範囲を制限する require などのチェック(バリデーション)が確実に行われているか?
3. 本当にガス代を節約しなければならないほど、頻繁に実行される処理なのか?
これらが確信を持てない場合は、「ガス代が少し高くても、安全な標準仕様(unchecked を使わない)」を選択するのが、プロの開発者としての賢い選択になります。
—
まとめ:一歩ずつ対策を学んでいきましょう!
今回のポイントを整理してみましょう。
- オーバーフロー・アンダーフローは、計算の箱の限界を超えて数値がループしてしまうバグ。
- Solidity 0.8未満では、
SafeMathという補助錠を自分で付ける必要があった。 - Solidity 0.8以降では、言語が「オートロック」を標準装備してくれたので、普通に書くだけで安全。
uncheckedは、ガス代を安くするための「オートロック解除機能」。使うときは細心の注意が必要!
スマートコントラクトのセキュリティと聞くと、SF映画のような高度なハッキング技術を想像しがちですが、実はその多くが、こうした「日々の防犯への意識」や「基本的な仕組みの理解」で防げるものばかりです。
「これって本当に安全かな?」と立ち止まって考えるその姿勢こそが、最強のセキュリティ対策になります。
これからも、焦らず一歩ずつ、安全で楽しいWeb3・スマートコントラクト開発の知識を深めていきましょうね!
コメント