作者:隐形人真忙
公众号:https://mp.weixin.qq.com/s/l3QBZwacLjIzu6KlpUvuWw
这是我在先知安全大会上分享议题中的一部分内容。主要介绍了利用对call调用处理不当,配合一定的应用场景的一种攻击手段。
以太坊中跨合约调用是指的合约调用另外一个合约方法的方式。为了好理解整个调用的过程,我们可以简单将调用发起方合约当做传统web世界的浏览器,被调用的合约看作webserver,而调用的msg则是http数据,EVM底层通过ABI规范来解码参数,获取方法选择器,然后执行对应的合约代码。
当然,实际上智能合约的执行一般在打包交易或者验证交易的时候发生,上面的比喻只是方便理解。
在solidity语言中,我们可以通过call方法来实现对某个合约或者本地合约的某个方法进行调用。
调用的方式大致如下:
<address>.call(方法选择器, arg1, arg2, …) <address>.call(bytes)
如上所述,可以通过传递参数的方式,将方法选择器、参数进行传递,也可以直接传入一个字节数组,当然要自己去构造msg.data的结构。
Solidity编程中,一般跨合约调用执行方都会使用msg.sender全局变量来获取调用方的以太坊地址,从而进行一些逻辑判断等。
比如在ERC20标准中的transfer方法的实现中,就是使用msg.sender来作为扣款方:
function transfer(address _to, uint256_value) returns (bool success) { …. balances[msg.sender]-= _value; balances[_to] += _value; …. }
Call方法注入漏洞,顾名思义就是外界可以直接控制合约中的call方法调用的参数,按照注入位置可以分为以下三个场景:
1. 参数列表可控 <address>.call(bytes4 selection, arg1, arg2, ...) 2. 方法选择器可控 <address>.call(bytes4selection, arg1, arg2, ...) 3. Bytes可控 <address>.call(bytesdata) <address>.call(msg.data)
简单举个例子,比如存在一个合约B,代码如下:
contract B{ function info(bytes data){ this.call(data) ; } function secret() public{ require(this ==msg.sender); // secret operations } }
其中有info和secret方法,secret方法中判断必须是合约自身调用才能执行。然而这里的info方法中有个call的调用,并且外界可以直接控制call调用的字节数组,因此如果外界精心构造一个data,这个data的方法选择器指定为secret方法,那么外部用户就可以以合约身份调用到这个secret方法,这样就会造成一定的风险。
这里举两种实际的攻击场景:
(1) bytes注入
在合约代码中,有个approveAndCallcode方法,这个方法中允许调用_spender
合约的某些方法或者传递一些数据,通过引入了_spender.call
来完成这个功能。
如果外界调用中指定_spender
为合约自身的地址,就可以以合约的身份去调用合约中的某些方法。比如如果我们使用合约的身份去调用transfer方法:
只需要自己去构造bytes即可,比如把transfer的_to
参数指定为我们自己的账户地址。这样其实就可以直接把合约账户中的代币全部转到自己的账户中,因为通过call注入,在transfer方法看来,msg.sender其实就是合约自己的地址。
(2) 方法选择器注入
比如这里有个logAndCall方法:
function logAndCall(address _to, uint _value, bytes data, string_fallback){ ….. assert(_to.call(bytes4(keccak256(_fallback)),msg.sender, _value, _data)) ; …… }
这里我们对_fallback参数可控,也就是说我们可以指定调用_to地址的任何方法,但是后面跟了三个参数,分别是msg.sender
,_value
, _data
,类型分别为address,uint256以及bytes。那么我们是不是只能调用参数类型必须为这三个的方法呢?当然不是。这里涉及到EVM在处理calldata的一个特性。
比如Sample1合约中有个test方法,这个方法中有三个参数,都是uint256类型的。而Sample2通过call调用了Sample1的test方法,这里传入了5个参数,同样是可以调用成功的。这是因为EVM在获取参数的时候没有参数个数校验的过程,因此取到前三个参数1,2,3之后,就把4,5给截断掉了,在编译和运行阶段都不会报错。
利用这个特性,我们其实有很多攻击面,比如我们可以通过logAndCall中的call注入来调用approve方法:
这里的approve方法有两个参数,而且类型为address和uint256,所以我们是可以调用成功的。这样就可以将合约账户中的代币授权给我们自己的账户了。
ERC223标准是为了解决ERC20中对智能合约账户进行转币场景缺失的问题,可以看作是ERC20标准的升级版。但是在很多ERC223标准的实现代码中就带入了call注入的问题:
此外,很多合约在判断权限的时候会将合约自身的地址也纳入到白名单中:
针对本文提到的这个风险,作为开发者来说,需要对ERC223的实现进行排查,不要引入call注入问题,如果非要执行回调,则可以指定方法选择器字符串,避免使用直接注入bytes的形式来进行call调用。对于一些敏感操作或者权限判断函数,则不要轻易将合约自身的账户地址作为可信的地址。