Slitherの向こう側:スマートコントラクト監査の「盲点」を突くカスタム検出器の作り方
現場のエンジニア諸君。GitHubで星の数が多い静的解析ツールを走らせて、「脆弱性は見つかりませんでした」というレポートに安心してはいないか?
断言する。汎用的なツールが検知できるのは、教科書に載っているような初歩的なバグだけだ。 本当に恐ろしいのは、ビジネスロジックの隙間を縫う「プロジェクト固有の欠陥」だ。今回は、Slitherを単なるツールから最強の武器へと昇華させる、カスタム検出器の実装と、実務で使える防御論を展開する。
—
1. Slitherの限界を理解する:なぜ汎用ツールだけでは足りないのか
Slitherは非常に優秀だ。再入可能攻撃(Reentrancy)や、初期化されていないストレージ変数などを瞬時に見つけてくれる。しかし、例えば「特定の権限を持つユーザーが、特定の条件下でだけ、特定の関数を呼び出せるべき」というような、そのプロジェクト独自のビジネス要件までは理解していない。
攻撃者は、ソースコードを一行ずつ読み込み、開発者が「まさかここを操作する奴はいないだろう」と油断しているロジックの「隙間」を狙う。この「隙間」を埋めるには、Slitherの基盤である SlithIR(中間表現)を理解し、独自の検知ルールをPythonで記述する必要がある。
—
2. カスタム検出器の作成:ビジネスロジックの「穴」を塞ぐ
Slitherのカスタム検出器は、Pythonで slither.detectors.AbstractDetector を継承して作成する。以下は、特定の機密関数に対して、「適切な修飾子(Modifier)が付与されているか」をチェックする実戦的なテンプレートだ。
from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification
from slither.core.declarations import Modifier
class OnlyOwnerMissingDetector(AbstractDetector):
"""
重要関数に 'onlyOwner' 修飾子が付いているかチェックするカスタム検出器
"""
ARGUMENT = "check-owner"
HELP = "重要関数にアクセス制限がありません"
IMPACT = DetectorClassification.HIGH
CONFIDENCE = DetectorClassification.HIGH
def _detect(self):
results = []
for contract in self.compilation_unit.contracts_derived:
for function in contract.functions:
# 'transfer' や 'withdraw' など、保護すべき関数名を指定
if function.name in ["withdrawFunds", "setAdmin"]:
# 修飾子リストの中に 'onlyOwner' が存在するか確認
modifiers = [m.name for m in function.modifiers]
if "onlyOwner" not in modifiers:
info = [self, "機密関数 ", function, " に所有者制限がありません!\n"]
results.append(self.generate_result(info))
return results
このように、「何が起きたらシステムが崩壊するか」をコードの形に落とし込むことが、監査の質を分ける境界線だ。
—
3. 実践:攻撃シナリオと防御的コーディング
多くのエンジニアが「コントラクトだけ」を気にしているが、現実の攻撃はWebフロントエンドやAPI経由の入力値検証の甘さを突いてくる。
例えば、フロントエンドからの入力値(amount)をそのままコントラクトの payable 関数に渡すような設計は自殺行為だ。以下は、JavaScript側でのバリデーションと、コントラクト側での堅牢な実装サンプルだ。
フロントエンド(JavaScript)でのガード実装
// 入力値が負の数やゼロでないことを厳格にチェック
async function withdraw(amount) {
const amountBigInt = BigInt(amount);
if (amountBigInt <= 0n) {
throw new Error("無効な送金額です。");
}
// トランザクション送信前にCLIやUIで再確認を挟むのが鉄則
await contract.withdrawFunds(amountBigInt);
}
コントラクト(Solidity)での防御的実装
// require文は必ずメッセージを添え、かつ処理の先頭(Checks)に書く
function withdrawFunds(uint256 amount) public onlyOwner {
// 1. Checks: 状態確認
require(amount > 0, "Amount must be greater than zero");
require(address(this).balance >= amount, "Insufficient contract balance");
// 2. Effects: 状態更新(再入攻撃対策として先に更新する)
uint256 balanceBefore = address(this).balance;
// 3. Interactions: 外部呼び出し
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
—
4. 現場のセキュリティリサーチャーからの助言
最後に、ツール以上に重要な「泥臭い習慣」を伝授する。
1. Slitherのレポートを無視するな: どんなに微小な警告(LOW/INFOレベル)でも、それがロジックの綻びを示唆していることは多い。
2. 監査は「コード」ではなく「意図」を読む: コードが正しく動くかではなく、「このコードはどう悪用できるか」を常に考えろ。
3. IAMとクラウド設定を見直せ: Web3プロジェクトのバックエンドが侵害されると、秘密鍵が抜かれる。クラウドの IAM ポリシーは最小権限の原則を徹底し、ローテーション可能なキー管理サービス(KMS)を使用すること。
設定例(AWS IAM Policy 最小権限):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:Sign"],
"Resource": "arn:aws:kms:region:account-id:key/key-id",
"Condition": {
"StringEquals": { "kms:ViaService": "ec2.amazonaws.com" }
}
}
]
}
セキュリティとは、ツールを導入して終わりという「点」の作業ではない。コードの設計思想からデプロイ環境までを俯瞰する「線」の継続的な努力だ。Slitherのカスタムルールを育て、君たちのプロジェクトを鉄壁に守り抜いてほしい。
何かあれば、またこの現場で会おう。健闘を祈る。
コメント