智能合约的重入攻击

打印 上一主题 下一主题

主题 986|帖子 986|积分 2958

智能合约的重入攻击是一种常见的安全漏洞,特别是在基于以太坊的区块链上,它利用了智能合约筹划或实现中的缺陷。重入攻击的核心在于攻击者可以或许在一个交易的中心阶段,即智能合约尚未完成其预期的内部状态更新时,递归地调用合约的同一或另一个函数。
基本原理:


  • 初始调用:攻击者首先调用易受攻击的智能合约中的一个函数,好比一个提款函数,通常会伴随一些以太币或代币的转移。
  • 状态变更前的外部调用:在智能合约内部,大概有一个点会在更新其状态变量(好比余额)之前举行外部调用,好比使用 .call() 或 .delegatecall() 方法向攻击者的合约转账或实行代码。
  • 递归调用:攻击者经心筹划了自己的合约,当吸收到调用或资金时,会立刻回调易受攻击合约的同一个或另一个存在漏洞的函数。此时,原合约的状态尚未更新,所以攻击者可以再次得到调用权限,并重复实行相同的行动,即再次请求资金转移。
  • 状态更新失败:由于递归调用,原始合约的状态更新(好比淘汰攻击者的余额)大概永世无法实行,由于每次攻击者都可以在状态更新前再次调用合约。
  • 无穷循环或直到资金耗尽:这个递归过程大概会一直持续,直到合约的所有资金都被耗尽,或者直到到达某个外部限定,好比 gas 限额。
重入攻击的关键在于攻击者可以或许利用合约的实行次序和状态更新的时机。为了防止这类攻击,开发者需要确保在举行任何外部调用之前,所有的内部状态更新都已经完成。此外,使用 .transfer() 和 .send() 方法代替 .call() 也可以降低风险,由于它们在默认情况下有较低的 gas 限额,这大概不足以实行复杂的恶意代码。
演示案例

最知名的大概是针对The DAO的攻击,尽管它不是严酷意义上的重入攻击,但它展示了攻击者如何利用合约漏洞来非法获取资金。但是,下面我将给出一个简化的智能合约重入攻击的示例,这通常在教育和研究场景中用来表明重入攻击的概念。
假设我们有一个简单的智能合约,它允许用户存款和提款:
  1. pragma solidity ^0.8.0;
  2. contract SimpleBank {
  3.     mapping(address => uint256) public balances;
  4.     function deposit() public payable {
  5.         balances[msg.sender] += msg.value;
  6.     }
  7.     function withdraw(uint256 _amount) public {
  8.         if (_amount <= balances[msg.sender]) {
  9.             // 这里存在漏洞,因为它先检查余额,然后调用外部合约
  10.             (bool success, ) = msg.sender.call{value: _amount}("");
  11.             require(success, "Transfer failed.");
  12.             balances[msg.sender] -= _amount;
  13.         }
  14.     }
  15. }
复制代码
在这个合约中,withdraw 函数存在一个漏洞,它先检查用户的余额是否足够,然后实验将资金转移到用户账户,最后才更新合约中的余额。如果攻击者有一个恶意合约,它可以在吸收到资金时立刻回调 SimpleBank 合约的 withdraw 函数,由于余额还没有更新,所以攻击者可以无穷次地从合约中提取资金,直到 gas 耗尽。
接下来是攻击者合约的一个简单示例:
  1. pragma solidity ^0.8.0;
  2. contract Attacker {
  3.     address private target;
  4.     constructor(address _target) {
  5.         target = _target;
  6.     }
  7.     function attack() public payable {
  8.         (bool success, ) = target.call{value: msg.value}("");
  9.         require(success, "Attack failed.");
  10.     }
  11.     fallback() external payable {
  12.         // 当接收到资金时,立即回调受害合约
  13.         (bool success, ) = target.call{value: 1 ether}("");
  14.         require(success, "Recursive call failed.");
  15.     }
  16. }
复制代码
在这个攻击者合约中,fallback 函数会在吸收到以太币时自动触发。当攻击者调用 attack 函数并将资金发送给受害合约时,一旦资金到达,fallback 函数就会被触发,从而递归地调用受害合约的 withdraw 函数,试图再次取出资金。
请注意,这个示例是为了展示重入攻击的概念,并不发起在生产环境中使用。在现实世界中,智能合约的开发者会采取多种安全措施来防止此类攻击,比方在外部调用前更新状态,使用原子操纵,或使用更安全的以太坊提供的转移函数 .transfer() 和 .send()。
攻击流程

我们一步步解析攻击合约的代码,以便更好地理解它是如何实现重入攻击的。
首先,这是攻击合约的构造函数:
  1. constructor(address _target) {
  2.     target = _target;
  3. }
复制代码
这里的 _target 参数是指向受害者的智能合约地址,也就是我们上面提到的 SimpleBank 合约。在部署攻击合约时,你需要提供这个地址,这样攻击合约就知道要攻击哪个合约了。
接下来是 attack 函数:
  1. function attack() public payable {
  2.     (bool success, ) = target.call{value: msg.value}("");
  3.     require(success, "Attack failed.");
  4. }
复制代码
这个函数吸收以太币作为参数(通过 payable 关键字)。当你调用这个函数并发送以太币时,它会把这笔钱转给 _target,也就是 SimpleBank 合约。这里使用了低级的 .call() 方法,它可以实行任意数据的调用,包括转移以太币。
如今,关键的部分来了,fallback 函数:
  1. fallback() external payable {
  2.     // 当接收到资金时,立即回调受害合约
  3.     (bool success, ) = target.call{value: 1 ether}("");
  4.     require(success, "Recursive call failed.");
  5. }
复制代码
在 Solidity 中,fallback 函数是在合约吸收到没有指定函数调用的数据或以太币时自动实行的函数。在我们的案例中,当 SimpleBank 合约实验将资金退还给攻击者时,它实际上是在调用攻击合约的 fallback 函数。
在 fallback 函数内部,攻击者合约立刻再次调用 SimpleBank 合约的 withdraw 函数,试图再次提取资金。由于 SimpleBank 合约在退款后才更新余额,这意味着攻击者合约可以不断地重复这一过程,直到所有的以太币都被抽走或者交易的 gas 被耗尽。
总结一下,攻击流程如下:

  • 攻击者调用 attack 函数并发送以太币到 SimpleBank 合约。
  • SimpleBank 合约的 withdraw 函数被调用,实验退款给攻击者。
  • 在退款过程中,fallback 函数在攻击者合约中被触发,由于它吸收到了以太币。
  • fallback 函数立刻回调 SimpleBank 合约的 withdraw 函数,试图再次提款。
  • 这个过程可以反复举行,直到所有资金被耗尽或交易结束。
这就是重入攻击的基本原理。在实际应用中,攻击者合约大概需要一些额外的逻辑来制止无穷循环,并确保攻击成功。此外,今世的智能合约开发实践会使用更安全的方法来制止这类攻击,好比先扣除余额再转账,或者使用 .transfer() 或 .send() 方法,它们会立刻抛出异常而不会继续实行剩余的代码。
fallback 函数

fallback 函数在Solidity智能合约中是一种特别类型的函数,它会在以下几种情况下自动实行:

  • 当合约吸收到Ether(以太币):如果有人向你的合约发送以太币,且没有指定任何函数调用,那么fallback函数就会自动实行。
  • 当吸收到一个未知的函数调用:如果发送到合约的消息包罗了函数调用数据,但该函数签名并不匹共同约中的任何函数,那么fallback函数会被调用。
  • 当使用.call()方法:当你的合约使用低级别的.call()、.delegatecall()或.staticcall()方法调用另一个合约时,如果目标合约没有返回任何数据,那么目标合约的fallback函数将会被实行。
需要注意的是,在Solidity 0.6.0版本之后,fallback函数被分为两个部分:fallback和receive。receive函数只处置惩罚纯Ether的吸收,没有附加数据的情况,而fallback函数则处置惩罚带有数据的Ether吸收或未知函数调用。这意味着在新版本的Solidity中,如果你只想处置惩罚纯Ether的吸收,你可以使用receive函数,而不需要写任何代码体,它会自动吸收Ether而不做其他操纵。
比方,一个简单的receive函数可以这样界说:
  1. receive() external payable {}
复制代码
这表示你的合约可以吸收Ether,而不会触发任何额外的操纵。而一个fallback函数大概会包罗更多的逻辑,比方处置惩罚吸收到的数据或实行某些业务逻辑。
在你提到的攻击合约示例中,fallback函数正是利用了这个特性,自动实行并发起递归调用来耗尽目标合约的资金。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

石小疯

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表