作者:隐形人真忙
公众号:https://mp.weixin.qq.com/s/l3QBZwacLjIzu6KlpUvuWw

TL;DR

这是我在先知安全大会上分享议题中的一部分内容。主要介绍了利用对call调用处理不当,配合一定的应用场景的一种攻击手段。

0x00 基础知识

以太坊中跨合约调用是指的合约调用另外一个合约方法的方式。为了好理解整个调用的过程,我们可以简单将调用发起方合约当做传统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;
.
}

0x01 攻击模型

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方法,这样就会造成一定的风险。

0x02 具体场景

这里举两种实际的攻击场景:

(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,所以我们是可以调用成功的。这样就可以将合约账户中的代币授权给我们自己的账户了。

0x03 深远的问题

ERC223标准是为了解决ERC20中对智能合约账户进行转币场景缺失的问题,可以看作是ERC20标准的升级版。但是在很多ERC223标准的实现代码中就带入了call注入的问题:

此外,很多合约在判断权限的时候会将合约自身的地址也纳入到白名单中:

0x04 防护手段

针对本文提到的这个风险,作为开发者来说,需要对ERC223的实现进行排查,不要引入call注入问题,如果非要执行回调,则可以指定方法选择器字符串,避免使用直接注入bytes的形式来进行call调用。对于一些敏感操作或者权限判断函数,则不要轻易将合约自身的账户地址作为可信的地址。

公众号:EnsecTeam


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/624/