漏洞概述
在 solidity 中合约之间的相互调用有两种方式:
solidity 提供了 call()、delegatecall()、callcode() 三个函数来实现合约直接的调用及交互,这些函数的滥用导致了各种安全风险和漏洞。在使用第二种方式时,如果处理不当很可能产生致命的漏洞 —— 跨合约调用漏洞,主要就是 call() 注入函数导致的
call() 函数对某个合约或者本地合约的某个方法的调用方式:
通过传递参数的方式,将方法选择器、参数进行传递,也可以直接传入一个字节数组(bytes要自己构造)
举一个简单的例子
contract sample_1{
function info(bytes data){
this.call(data);
}
function secret() public{
require(this == msg.sender);
//secret operations
}
}
合约的两个函数中 secret 函数必须是合约自身调用的,然而有个 info 函数,调用了 call(),并且外界是可以直接控制 call 函数的字节数组的
this.call(bytes4(keccak256("secret()")));
这样就调用了 secret
第二个例子
contract sample2{
...
function logAndCall(address _to,uint _value,bytes data,string _fallback){
...
assert(_to.call(bytes4(keccak256(_fallback)),msg.sender,_value,_data));
...
...
}
在 logAndCall 函数中,我们的 _falback 参数可以控制,所以我们可以控制 _to 的任何方法。另外 assert 有三个参数,我们没必要调用完全符合三个参数类型的合约,因为在 EVM 中,只要找到了方法需要的参数,就会去执行,其他参数就会被忽略,不会产生任何影响
漏洞分析
function transferFrom(address _from,address _to,uint256 _amount,bytes _data,string_custom_fallback) public returns (bool success){
//Alerts the token controller of the transfer
if(isContract(controller)){
throw;
}
require(super.transferFrom(_from,_to,_amount));
if(isContract(_to)){
ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
receiver.call.value(0)(bytes4(keccack256(_custom_fallback)),_from,_amount,_data);
}
ERC223Transfer(_from,_to,_amount,_data);
return true;
}
function setOwner(address owner_) public auth{
owner - owner_;
LogSetOwner(owner);
}
modifier auth{
require(isAuthorized(msg.sender,msg.sig));
_;
}
function isAuthorized(address src,bytes4 sig) internal view returns (bool){
if(src==address(this)){
return true;
} else if (src == owner){
return true;
} else if (authority == DSAuthority(0)){
return false;
} else {
return authority.canCall(src,this,sig);
}
}
核心漏洞代码片段
function transferFrom(address _from,address _to,uint256 _amount,bytes _data,string_custom_fallback) public returns (bool success){
//Alerts the token controller of the transfer
if(isContract(controller)){
throw;
}
require(super.transferFrom(_from,_to,_amount));
if(isContract(_to)){
ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
receiver.call.value(0)(bytes4(keccack256(_custom_fallback)),_from,_amount,_data);
}
代码含义:如果目标地址是智能合约,就调用目标的 custom 回退函数,并依次填入参数 _from,_amount,_data,这些都是我们可控的,另外 _to 参数也仅仅进行了是否是合约地址的判断,所以我们可以通过 _to 来控制合约本身,并调用该合约的任意 public 函数
代码调试
在这里复制代码到 Remix IDE
https://cn.etherscan.com/address/0x461733c17b0755ca5649b6db08b3e213fcf22546#code
由于这个合约的计算比较多,所以在 Gas limit 值加上个 0 让他大一点
点击 owner 查看合约所有者的地址,返回了默认账户的地址
0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c
调用带有 _custom_fallback 参数的 transferFrom() 函数,我们的目的是让合约属于第二个账户,所以填写如下参数:
再看一下,合约所有者已经成了第二个账户的地址了
漏洞防范
虽然 call()、delegatecall()、callnode() 三个函数为合约间调用提供了很大的便利,但是存在很大隐患,所以防范跨合约调用漏洞的方法就是减少对这三个函数的使用。很多功能都可以用高级函数来实现