2023.01.12
Uniswap のRe-entrancy 攻撃可能な脆弱性について
こんにちは。次世代システム研究室のT.M です。
はじめに
先日、最も利用されているDeFi のプロトコルであるUniswap に脆弱性があったことが発表されました。幸いなことにセキュリティ監査会社Dedaub が発見し、早急に修正されたため、攻撃に利用されることがありませんでした。この脆弱性は、Re-entrancy 攻撃が可能なものであり、昨年11月にリリースされたUniversal Router という機能に存在しました。限定的ですが資産を盗み出すことが可能でした。この脆弱性は重大度としてはmedium と判定され、バグバウンティプログラムとして発見者に4万ドルが支払われました。本稿では、その脆弱性について解説をします。
Uniswap とは
取引量が1.2兆ドルを超えた最も利用されているDEX です。Ethereum 上でスマートコントラクトを利用して自動で取引を行います。ETH やERC20, NFT などの取引に利用できます。
Universal Router とは
2022年11月にUniswap にリリースされた機能です。ETH、ERC20、NFTのswap routerであり、プロトコル間の取引を集約し、ユーザーが柔軟性の高い取引ができるようになります。柔軟なコマンドスタイルで、ETH をWETH に交換をして、特定のマーケットプレイスにあるNFT との取引をする、といったことを一つのトランザクションで実行できます。
Re-entrancy 脆弱性
よく知られた脆弱性の一つであり、コントラクトが別の悪意あるコントラクトの関数を呼び出した際に、そのコントラクトにあるETH など盗み出すことができるものです。例として以下のコードがあります。
contract EtherStore { mapping(address => uint) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw() public { uint bal = balances[msg.sender]; require(bal > 0); (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to send Ether"); balances[msg.sender] = 0; } function getBalance() public view returns (uint) { return address(this).balance; } } contract Attack { EtherStore public etherStore; constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); } fallback() external payable { if (address(etherStore).balance >= 1 ether) { etherStore.withdraw(); } } function attack() external payable { require(msg.value >= 1 ether); etherStore.deposit{value: 1 ether}(); etherStore.withdraw(); } function getBalance() public view returns (uint) { return address(this).balance; } }
withdraw 関数は預け入れたETH を全額引き落とす関数です。処理の流れは以下の通りです。
- 残高確認
- ETH 送金
- 残高を0に変更
この時、送金先のコントラクトのfallback 関数でwithdraw 関数が呼び出されていた場合、以下の処理の流れになります。
- 残高確認
- ETH送金
- 残高確認
- ETH送金
- 残高確認
- ETH送金
- …
再帰的な呼び出しになり、残高を0に変更する前に何度もETH を送金することになり、コントラクトが所有しているETH を全額引き落とすことができます。
この脆弱性の対策として、ロックを利用することが広く知られています。
contract ReEntrancyGuard { bool internal locked; modifier noReentrant() { require(!locked, "No re-entrancy"); locked = true; _; locked = false; } }
noReentrant modifier をwithdraw 関数に付けることで再帰的な呼び出しを防ぐことができます。
Uniswap の脆弱性
Universal Router のコードは以下の通りです。
function execute(bytes calldata commands, bytes[] calldata inputs) public payable { bool success; bytes memory output; uint256 numCommands = commands.length; if (inputs.length != numCommands) revert LengthMismatch(); for (uint256 commandIndex = 0; commandIndex < numCommands;) { bytes1 command = commands[commandIndex]; bytes memory input = inputs[commandIndex]; (success, output) = dispatch(command, input); if (!success && successRequired(command)) { revert ExecutionFailed({commandIndex: commandIndex, message: output}); } unchecked { commandIndex++; } } }
パラメータとして、複数の取引のためのコマンドとその入力値があります。コマンドを順次実行します。
攻撃者はonERC721Received 関数内でtransfer やsweep する処理を記述したトークンコントラクトを受け取るコマンドを実行することで、資産を盗み出すことができます。
この脆弱性の対策として、以下のコードが実装されました。
contract ReentrancyLock { error ContractLocked(); uint256 private isLocked = 1; modifier isNotLocked() { if (isLocked != 1) revert ContractLocked(); isLocked = 2; _; isLocked = 1; } }
一般的なRe-entrancy 攻撃の対策と同様にロックのmodifier が実装され、それがUniversal Router のexecute 関数に付けられました。
まとめ
Uniswap のUniversal Router というコントラクトにおいて、Re-entrancy の脆弱性が発見されました。Re-entrancy の一般的な脆弱性について解説をしました。また、Universal Router の実際のコードで、対策がされていないことを確認しました。
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
参考
https://github.com/Uniswap/universal-router/pull/189
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD