浅谈智能合约evilReflex漏洞

0x01 漏洞简述

  • 漏洞名称:evilReflex漏洞(call注入攻击)
  • 漏洞危害:攻击者可以通过该漏洞将存在存在evilReflex漏洞的合约中的任意数量的token转移到任意地址
  • 影响范围:多个ERC233标准智能合约

0x02 预备知识

文章首发先知社区

智能合约的外部调用方式-call

1
2
3
//使用方式
<address>.call(bytes) //Call消息传递
<address>.call(函数选择器, arg1, arg2, …)
  • Call消息传递

    call()是一个底层的接口,用来向一个合约发送消息,也就是说如果你想实现自己的消息传递,可以使用这个函数。函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约发送这段数据。

  • Call指定函数

    如果第一个参数刚好是四个字节,会认为这四个字节指定的是函数签名的序号值,从而去调用目标合约中对应的函数。

    函数选择器(Function Selector) 该函数签名的 Keccak 哈希的前 4 字节

    1
    2
    3
    4
    function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
    sha3.keccak_256(b'baz(uint32,bool)').hexdigest()[0:8]
    //cdcd77c0
    如果我们想调用baz函数,此处的函数选择器就是0xcdcd77c0

    想要深入了解参考文章1文章2

0x03 漏洞原理

简单分析

1
2
3
4
5
6
7
8
9
10
11
contract evilreflex{
function info(bytes data){
this.call(data);
}
function secret() public{
require(this == msg.sender);
// this 当前实例化的合约对象
// secret operations
……
}
}

在这个示例中如果我们想要使用secret函数,但是该函数只能合约本身调用,显然我们无法满足require条件,我们就没办法使用secret函数。但是我们发现在info函数中使用了call函数,并且外界是可以直接控制 call函数的字节数组的,我们就可以这样

1
this.call(bytes4(keccak256("secret()")));

此时就可以实现调用secret函数,实现了权限绕过。

bytes注入

1
2
3
4
5
6
7
8
9
10
11
12
function approveAndCallcode(
address _spender,
uint256 _value,
bytes _extraData)
returns (bool success) {
allowed[msg.sender][_spender] = _value;
Approval(msg.sender,_spender,_value);
if(!_spender.call(_extraData)){
revert();
}
return true;
}

上述函数的功能是用来完成approve操作时发出相关的调用通知,但是使用了call函数,且参数_spender,_extraData可控,通过预备知识我们可以通过消息传递的方式去调用合约上的任何函数。比如

  • adress.tranfer() 让合约向指定地址转token
  • approval() 实现任意token授权

方法选择器注入

1
2
3
4
5
6
7
8
9
function logAndCall(
address _to,
uint _value,
bytes data,
string _fallback){
……
assert(_to.call(bytes4(keccak256(_fallback)),msg.sender, _value, _data)) ;
……
}

_fallback参数可控,也就意味着可以调用任何函数,但是后面的三个参数如果和目标函数的参数个数,类型不对应怎么办?这里涉及到EVM的call函数簇的调用特性函数簇在调用函数的过程中,会自动忽略多余的参数,这又额外增加了 call 函数簇调用的自由度。

简单演示

1
2
3
4
5
6
7
8
9
10
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);
}
}

例子中 test() 函数仅接收一个 uint256 的参数,但在 callFunc() 中传入了三个参数,由于 call 自动忽略多余参数,所以成功调用了 test() 函数。

0x04 真实案例分析

ATN代币增发

1
2
3
4
5
6
7
8
function transferFrom(address _from, address _to, uint256 _amount, bytes _data, string _custom_fallback) public returns (bool success) {
...
if (isContract(_to)) {
ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function setOwner(address owner_) public auth {
owner = owner_;
emit 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);
}
}

transferFrom() 函数中危险的使用了call函数,同时_custom_fallback_from

参数可控,我们就可以去调用该合约上的任何函数,同时_to传入的参数要求是一个合约地址,我们就可以传入该合约的地址,被实例化的receiver执行call函数就能实现合约上的任意函数使用。

setOwner()函数可以设定合约的管理员,我们就能在调用transferFrom()时的参数设定为

  • _custom_fallback setOwner(adress)
  • _from 自己的账号地址
  • _to 当前合约地址

与此同时,call 调用已经将 msg.sender 转换为了合约本身的地址,也就绕过了 isAuthorized() 的权限认证

0x05 复现

代码地址在remix上部署到rinkeby测试链之后,查询owner

截屏2021-07-20 18.02.28

根据上面的真实案例分析,填入相应的参数,之后执行

image-20210721185931721

再次查询,发现合约的拥有者已经改变,攻击成功!

image-20210721185859308

0x06 总结

call函数,它提供了不完全公开代码的情况下的ABI调用的方式。除了函数调用,消息传输意外,亦可以实现转账等等。但是其不正确的使用,带来了诸多的问题,本文分析的evilReflex漏洞只是call函数发生的一种,例如重入攻击,dos攻击等等。开发者在开发过程中尽量避免使用call函数,如果确实需要使用,对于传入的参数一定要不能被外部控制。