call函数介绍
在Solidity中,可以使用call、delegatecall、callcode三个函数实现跨合约的函数调用功能。
这里主要介绍call函数的使用,call函数的调用模型:
<address>.call(...) returns (bool)
call函数可以接受任何长度、任何类型的参数,其传入的参数会被填充至 32 字节最后拼接为一个字符串序列,由 EVM 解析执行。
在call函数调用的过程中,Solidity中的内置变量 msg
会随着调用的发起而改变,msg
保存了调用方的信息包括:调用发起的地址,交易金额,被调用函数字符序列等。
使用call函数进行跨合约的函数调用后,内置变量 msg
的值会修改为调用者,执行环境为被调用者的运行环境(合约的storage)。
通过下面的例子,演示call函数的调用:
pragma solidity ^0.4.0;
contract A {
address public temp1;
uint256 public temp2;
function fcall(address addr) public {
temp1 = msg.sender;
temp2 = 100;
addr.call(bytes4(keccak256("test()")));
}
}
contract B {
address public temp1;
uint256 public temp2;
function test() public {
temp1 = msg.sender;
temp2 = 200;
}
}
在Remix中进行部署、调用测试:
-
部署合约,A合约地址:
0x100eee74459cb95583212869f9c0304e7ce11eaa
, B合约地址:0xe90f4f8aeba3ade774cac94245792085a451bc8e
-
调用A合约的fcall函数,使用B合约的地址作为参数
分别查看A合约和B合约的temp1和temp2变量的值:
- A合约的temp1:0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c
- A合约的temp2:100
- B合约的temp1:0x100eeE74459CB95583212869f9c0304e7cE11EAA
-
B合约的temp2:200
通过上面的例子,可以看出:
在A合约中,msg.sender = address(调用者)
;在A合约中调用B合约的函数,函数中, msg.sender = address(A合约地址)
。
同时,在A合约中调用B合约的函数,调用的运行环境是被调用者的运行环境,即是B合约的运行环境。
call函数滥用漏洞说明
利用call函数滥用漏洞,可以执行 call注入 攻击。call注入 是一种新的攻击方式,原因是对call调用处理不当,配合一定的应用场景的一种攻击手段。
通常情况下,跨合约间的函数调用会使用call函数,由于call在相互调用过程中内置变量msg会随着调用方的改变而改变,这就成为了一个安全隐患,在特定的应用场景下将引发安全问题,被恶意攻击者利用,施行 call注入 攻击。
call函数的调用方式:
<address>.call(function_selector, arg1, arg2, ...)
<address>.call(bytes)
call函数拥有极大的自由度:
- 对于一个指定合约地址的call调用,可以调用该合约下的任意函数
- 如果call调用的合约地址由用户指定,那么可以调用任意合约的任意函数
同时,call函数调用,会自动忽略多余的参数,如下:
pragma solidity ^0.4.0;
contract A {
uint256 public aa = 0;
function test(uint256 a) public {
aa = a;
}
function callFunc() public {
this.call(bytes4(keccak256("test(uint256)")), 10, 11, 12);
}
}
call函数调用中的参数11,12将会被自动忽略,test函数中 aa = 10
。
call注入攻击模型
下面的例子展示了call注入模型:
contract A {
function info(bytes data) public{
this.call(data);
//this.call(bytes4(keccak256("secret()"))); //利用代码示意
}
function secret() public{
require(this == msg.sender);
// secret operations
}
}
在合约A中存在 info()
和 secret()
函数,其中 secret()
函数只能由合约自己调用,在 info()
中有用户可以控制的call调用,用户精心构造传入的数据(将注释转为字节序列),即可绕过 require()
的限制,成功执行 secret()
下面的代码。
call注入攻击引起的安全问题
权限绕过
function callFunc(bytes data) public {
this.call(data);
//this.call(bytes4(keccak256("withdraw(address)")), target); //利用代码示意
}
function withdraw(address addr) public {
require(isAuth(msg.sender));
addr.transfer(this.balance);
}
function isAuth(address src) internal view returns (bool) {
if (src == address(this)) {
return true;
}
else if (src == owner) {
return true;
}
else {
return false;
}
}
通过精心构造 callFunc()
的传入参数(如:this.call(bytes4(keccak256("withdraw(address)")), target);
),恶意攻击者可以绕过函数 withdraw()
的权限验证。
窃取代币
在代币合约中,往往会加入一个call回调函数,用于通知接收方以完成后续的操作。但由于call调用的特性,用户可以向call传入 transfer()
函数调用,即可窃取合约地址下代币。
function transfer(address _to, uint256 _value) public {
require(_value <= balances[msg.sender]);
balances[msg.sender] -= _value;
balances[_to] += _value;
}
function callFunc(bytes data) public {
this.call(data);
//this.call(bytes4(keccak256("transfer(address,uint256)")), target, value); //利用代码示意
}
漏洞预防
预发call函数滥用漏洞的最好方式是尽量减少使用call函数。
如果合约逻辑无法避免跨合约的函数调用,可以采用 new
合约,并指定 function_selector
的方式,指定调用的合约及合约方法,并做好函数参数的检查。
constructor() {
b = new B();
}