こんにちは。CryptoGamesエンジニアのなつきです。
GameFi関係のスマートコントラクトやAPIの開発を中心に活動しています。
この記事は、Ethereumアドベントカレンダー2023 11日目の投稿です。
さて最近暗号資産市場は急な価格上昇でお祭り状態ですが、ThirdwebやOpenZeppelinが関係した脆弱性の発表などもあり、ブロックチェーンエンジニアはひと時も気を抜いていられないなという気持ちです。
詳細を把握していない方は、こちらの記事が参考になります。
ERC2771とmulticallの組み合わせで起きた脆弱性です。
物騒な世の中ですね。。。
でも大丈夫です。
心強い脆弱性診断ツールを2つ紹介します。
これがあれば、1%ぐらいは気をぬいてもいいかもしれません。
今回紹介するツール
① OpenZeppelin Defender
② Slither
ツールの概要と導入方法について説明します。
そしてChatGPTに考えてもらった最凶のスマートコントラクト「須摩阿突混斗楽土」と対決してもらいます。勝ち負けは、診断結果の雰囲気で僕が決めたいと思います。
須摩阿突混斗楽土
最凶のスマートコントラクトの中身はこちらです。
pragma solidity ^0.8.0;
// 警告:意図的に脆弱に作られたコントラクトです。絶対に使用しないでください。
interface IExternalContract {
function executeAction() external;
}
contract Vulnerable {
mapping(address => uint) public balances;
address private owner;
string private secretPassword; // Vulnerability 6: Hardcoded Secrets
constructor() {
owner = msg.sender;
secretPassword = "123456"; // Never hardcode sensitive information!
}
// 脆弱性 1: 再入攻撃
function withdrawAll() public {
uint amount = balances[msg.sender];
require(amount > 0, "No balance to withdraw");
// External call that can be re-entered before updating the balance
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] = 0; // Balance updated after external call
}
// 脆弱性 2: オーバーフローとアンダーフロー
function transfer(address to, uint amount) public {
// No checks for overflow/underflow
balances[msg.sender] -= amount;
balances[to] += amount;
}
// 脆弱性 3: 不適切なアクセス制御
function updateBalance(address user, uint newBalance) public {
// Any user can update any balance
balances[user] = newBalance;
}
// 脆弱性 4: ガス制限
function batchTransfer(address[] memory users, uint amount) public {
// May run out of gas if the array is too large
for (uint i = 0; i < users.length; i++) {
balances[users[i]] += amount;
}
}
// 脆弱性 5: フロントランニング
function bid() public payable {
// Front-running possible: Miners or other users can see the bid and act before it's processed
// No implementation here, just a placeholder for vulnerability demonstration
}
function getBalance(address user) public view returns (uint) {
return balances[user];
}
// 脆弱性 6: ハードコードされた秘密情報
function validatePassword(string memory password) public view returns (bool) {
return keccak256(abi.encodePacked(password)) == keccak256(abi.encodePacked(secretPassword));
}
// 脆弱性 7: 予測可能なブロック情報
function gamble() public payable returns (bool) {
uint blockValue = uint(blockhash(block.number - 1));
uint betValue = uint(keccak256(abi.encodePacked(blockValue, msg.sender)));
// Using block information to determine a win or lose
if (betValue % 2 == 0) {
balances[msg.sender] += msg.value;
return true;
} else {
balances[address(this)] += msg.value;
return false;
}
}
// 脆弱性 8: 不適切なエラーハンドリング
function transferWithNoChecks(address to, uint amount) public {
// No error handling, unsafe external calls
address(to).call{value: amount}("");
}
// 脆弱性 9: 外部コントラクトへの依存
function externalCall(address externalContract) public {
// Directly calling an external contract without any checks
IExternalContract(externalContract).executeAction();
}
}
どれも一般的な内容で最凶からは程遠いですが、これぐらいの脆弱性でも検知できない場合大きな事故につながるので今回は十分でしょう。
第1回戦:OpenZeppelin Defender
OpenZeppelin Defenderは、スマートコントラクトの開発、運用、保守をサポートするためのプラットフォームです。
秋頃に2.0が発表され大幅にアップデートされました。その中でもinspecterという脆弱性診断を自動で行ってくれる機能がとても便利です。
導入の方法はとても簡単でDefenderに登録してからGithub連携を行うだけです。
Defenderは、許可されたリポジトリ内のPRを監視して、solやpackage.jsonに変更が検知された場合、①レポートの生成と②診断サマリーをPRにコメントする仕様になっています。
では実際の結果です。
①レポート
PRが作成されるとこのようなレポートがDefenderの管理画面に出力されます。
②PRへのコメント
勝敗
想像以上にSeverity Level(リスクレベル)の高い脆弱性が検出されませんでした。少し頼りない気持ちもありますが、5分あれば導入できることや説明がわかりやすいことを考えると、導入しない理由はないでしょう。OpenZeppelinのコントラクトをもっと利用していたら、より有効だったのかもしれません。
1回戦は、須摩阿突混斗楽土の勝ち。
第2回戦:Slither
Slitherは、Solidityのスマートコントラクトに特化した静的解析ツールです。
SlitherやMythrilは、人気なツールなので既に触ったことがある方多いのではないでしょうか。
今回はSlitherをCLIからではなく、github action経由で実行していきたいと思います。
公式が提供しているslither-actionというライブラリを利用します。
slither
slither action
Slither actionはコントラクトの継承関係を図に出力したり、関数の依存関係を整理しくれたり様々な機能がありとても便利です。GitHub Advanced Securityを利用してレポートを出力することもできます。
今回は手軽に実行できるマークダウン形式の診断レポートをPRのコメントに出力する方法で導入していきます。
github actionのコードです。
基本的には公式の内容ですが、issueにあるようにマークダウン形式のgithub-script部分でエラーが起きるので、修正の必要がありました。エラーの理由がわからず90分格闘しました。
issue: https://github.com/crytic/slither-action/issues/59
name: Slither Analysis action
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run Slither
uses: crytic/slither-action@v0.3.0
id: slither
with:
fail-on: none
slither-args: --checklist --markdown-root ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}/
- name: Create/update checklist as PR comment
uses: actions/github-script@v6
if: github.event_name == 'pull_request'
env:
REPORT: ${{ steps.slither.outputs.stdout }}
with:
script: |
const script = require('.github/scripts/comment')
const header = '# Slither report'
const body = process.env.REPORT
await script({ github, context, header, body })
module.exports = async ({ github, context, header, body }) => {
const comment = [header, body].join("\n");
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.number,
});
const botComment = comments.find(
(comment) =>
// github-actions bot user
comment.user.id === 41898282 && comment.body.startsWith(header)
);
const commentFn = botComment ? "updateComment" : "createComment";
await github.rest.issues[commentFn]({
owner: context.repo.owner,
repo: context.repo.repo,
body: comment,
...(botComment
? { comment_id: botComment.id }
: { issue_number: context.payload.number }),
});
};
このgithub actionsのコードはほぼコピペで動きます。
もし動かない場合は、github actionsの権限周りの設定やディレクトリの指定に問題があるかもしれません。
勝敗
網羅性の高い診断が行われた印象です。コードの該当箇所をコメント内で可視化してくれるのも嬉しいですね。
2回戦は、Slitherの勝ち。
最後に
今回の対決は引き分けになりました。
診断結果の詳細には触れませんでしたが、診断ツールを簡単にCI/CDに導入でき、網羅性の高いテストを行えるのは心理的安全性が高まると思うので、まだでしたら是非試してみてください。
では皆さん、
メリークリスマス。
ハッピーニューイヤー。
トラスト、バット・ヴェリファイ。