|
本帖最后由 Meng0f 于 2022-3-4 19:54 编辑
浅谈智能合约evilReflex漏洞 0x01 漏洞简述- 漏洞名称:evilReflex漏洞(call注入攻击)
- 漏洞危害:攻击者可以通过该漏洞将存在存在evilReflex漏洞的合约中的任意数量的token转移到任意地址
- 影响范围:多个ERC233标准智能合约
0x02 预备知识
智能合约的外部调用方式-call
- //使用方式
- <address>.call(bytes) //Call消息传递
- <address>.call(函数选择器, arg1, arg2, …)
复制代码- Call消息传递
call()是一个底层的接口,用来向一个合约发送消息,也就是说如果你想实现自己的消息传递,可以使用这个函数。函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约发送这段数据。 - Call指定函数
如果第一个参数刚好是四个字节,会认为这四个字节指定的是函数签名的序号值,从而去调用目标合约中对应的函数。
函数选择器(Function Selector) 该函数签名的 Keccak 哈希的前 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
复制代码
0x03 漏洞原理简单分析
- 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函数的字节数组的,我们就可以这样
- this.call(bytes4(keccak256("secret()")));
复制代码
此时就可以实现调用secret函数,实现了权限绕过。
bytes注入- 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授权
方法选择器注入
- function logAndCall(
- address _to,
- uint _value,
- bytes data,
- string _fallback){
- ……
- assert(_to.call(bytes4(keccak256(_fallback)),msg.sender, _value, _data)) ;
- ……
- }
复制代码 _fallback参数可控,也就意味着可以调用任何函数,但是后面的三个参数如果和目标函数的参数个数,类型不对应怎么办?这里涉及到EVM的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);
- }
- }
复制代码 例子中 test() 函数仅接收一个 uint256的参数,但在 callFunc()中传入了三个参数,由于 call 自动忽略多余参数,所以成功调用了 test() 函数。
0x04 真实案例分析
ATN代币增发
- 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);
- }
- ...
- }
复制代码- 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
 
根据上面的真实案例分析,填入相应的参数,之后执行
 
再次查询,发现合约的拥有者已经改变,攻击成功!
 
0x06 总结
call函数,它提供了不完全公开代码的情况下的ABI调用的方式。除了函数调用,消息传输意外,亦可以实现转账等等。但是其不正确的使用,带来了诸多的问题,本文分析的evilReflex漏洞只是call函数发生的一种,例如重入攻击,dos攻击等等。开发者在开发过程中尽量避免使用call函数,如果确实需要使用,对于传入的参数一定要不能被外部控制。
|
|