「ブロックハッシュは乱数ではない」:スマートコントラクトにおける脆弱性の正体と防御策
こんにちは。セキュリティチーフエンジニアです。
現場で「とりあえず動くコード」を書くことに忙殺されていると、つい見落としてしまうのが「乱数生成の危うさ」です。特にWeb3領域や、IoTデバイスの認証トークン生成において、安易な実装は死を意味します。
今日は、多くの開発者が陥る「ブロックハッシュやタイムスタンプを乱数源にする」という悪癖が、なぜ命取りになるのか。そして、それをどう修正すべきかについて、現場の知見を詰め込んで解説します。
—
1. なぜ「ブロックハッシュ」を乱数に使ってはいけないのか
スマートコントラクトにおいて、block.number や block.timestamp、あるいは blockhash(n) を乱数生成のシードに使うコードをよく見かけます。
「ブロックハッシュなら予測不可能だろう?」
そう思っているとしたら、それは甘い。攻撃者にとって、これらは「公開情報」です。マイナー(バリデーター)は、自分が生成するブロックの内容をある程度操作できます。もし自分が生成するブロックの結果が気に入らなければ、そのブロックを破棄して再計算し、有利な結果が出るまで繰り返すことすら可能です。
これを「Miner Extractable Value (MEV)」の一種として悪用されると、あなたのコントラクト上のゲームや抽選は、開始した瞬間に「負けが確定した博打」になります。
—
2. 攻撃者が狙う「盲点」:PoCの思考プロセス
攻撃者は以下のようなステップであなたのコントラクトをハックします。
1. トランザクションの監視: 攻撃者はMempool(未承認トランザクションの溜まり場)を監視し、あなたのコントラクトが乱数生成を要求するタイミングを特定します。
2. シミュレーション: 攻撃者は自身のノードで、現在のブロックハッシュを使って結果を事前に計算します。
3. 裁定取引(あるいは先回り): 自分のトランザクションを、あなたのトランザクションより高いガス代で優先的に実行させます。
結果として、攻撃者だけが「当選」し、一般ユーザーは常に「ハズレ」を引かされる仕組みが完成します。
—
3. 正解は「外部オラクル」の利用
自分自身のブロックチェーン内部だけで乱数を完結させようとすると、必ずこの「予測可能性」の壁にぶつかります。これを回避する標準的な解は、Chainlink VRF (Verifiable Random Function) を利用することです。
Chainlink VRFは、暗号学的に証明可能な乱数をオンチェーンに提供します。ブロックチェーン外部の安全な乱数源から取得し、その「正当性」を証明するデータとともにオンチェーンに書き込むため、マイナーや攻撃者が介入する余地がありません。
—
4. セキュアな実装サンプル:Chainlink VRF の利用
以下に、Solidityでの実装の骨子を示します。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
contract SecureLottery is VRFConsumerBaseV2 {
VRFCoordinatorV2Interface COORDINATOR;
uint64 subscriptionId;
bytes32 keyHash; // ネットワークごとのキーハッシュ
uint32 callbackGasLimit = 100000;
uint16 requestConfirmations = 3;
uint256 public randomResult;
constructor(address vrfCoordinator, uint64 _subId, bytes32 _keyHash)
VRFConsumerBaseV2(vrfCoordinator)
{
COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
subscriptionId = _subId;
keyHash = _keyHash;
}
// 乱数生成リクエストを送信
function requestRandomNumber() external {
COORDINATOR.requestRandomWords(
keyHash,
subscriptionId,
requestConfirmations,
callbackGasLimit,
1 // 1つの乱数を生成
);
}
// Chainlinkオラクルから呼び出されるコールバック関数
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
// ここに届く randomWords は暗号学的に安全
randomResult = randomWords[0];
}
}
実務上のTips
- ガス代の考慮: Chainlink VRFはオフチェーンでの計算コストが発生するため、リクエスト時にコストがかかります。予算管理を忘れずに。
- Subscription管理: Chainlinkのデベロッパー向け管理画面で、事前にリンク(LINK)トークンをチャージしておく必要があります。
—
5. インフラ側で守るべき「もう一つの乱数」
IoTデバイスからWebサーバーへデータを送る際、セッションIDや一時的なトークン生成に rand() 関数(PHP)や random.random()(Python)をそのまま使っていませんか?
これらは「暗号論的に安全ではない(CSPRNGではない)」可能性があります。OSの /dev/urandom を参照する以下の実装を徹底してください。
Pythonの推奨実装:
import secrets # secretsモジュールを使うのが鉄則
# 安全なトークンを生成(推測不可能)
def generate_secure_token():
return secrets.token_hex(32)
print(generate_secure_token())
PHPの推奨実装:
<?php
// PHP 7以降では random_bytes を使用する
$bytes = random_bytes(32);
$token = bin2hex($bytes);
echo $token;
?>
—
結び:セキュリティは「諦め」から始まる
「自分で乱数アルゴリズムを組んだ方が早いし、外部サービスに依存したくない」というエンジニアのプライドは、往々にして脆弱性の温床になります。
「乱数は自分で作らない」
これが、数々のインシデントを乗り越えてきた我々セキュリティリサーチャーの結論です。Chainlink VRFのような枯れた技術や、OSレベルで提供されている secrets モジュールを信頼して使うことこそが、攻撃者に付け入る隙を与えない、最も堅牢な設計への近道です。
次にコードを書くとき、rand() や block.timestamp を使う前に、一瞬だけ立ち止まって考えてみてください。「これは攻撃者に予測されないか?」と。その疑念こそが、システムの安全を守る最強の防壁になります。
コメント