一、背景 2018年9月7日早上1点左右,许多以太坊账户都收到了一种名为blockwell.ai KYC Casper Token
转账消息,其中有的是收到了这种代币,而有的用户是支出了这种代币。
得到的用户以为受到了新币种的空投,满心欢喜的打开之后发现并没有获得任何代币。转出的用户着急打开钱包,以为是钱包被盗转走了代币,实际上却毫无损失。
在回过神来看看代币的名字,忍不打开blockwell.ai
查看原因,一次成功的广告诞生了。
二、事件回顾 回到导致事件发生的blockwell.ai合约中,合约地址为
1 https:// etherscan.io/address/ 0 x212d95fccdf0366343350f486bda1ceafc0c2d63
实际转账到账户的交易信息
可以看到通过调用这个合约,发起了一笔代币转账,在event logs里可以看到实际的交易
然后具体的交易地址为
1 https:// etherscan.io/tx/ 0 x3230f7326ab739d9055e86778a2fbb9af2591ca44467e40f7cd2c7ba2d7e5d35
整笔交易花费了244w的gas,价值2.28美元,有针对的从500个用户转账给了500个用户。
跟踪到转账的from地址,可以看到一个很有趣的问题
1 https:// etherscan.io/address/ 0 xeb7a58d6938ed813f04f36a4ea51ebb5854fa545
所有的来源账户本身都是不持有这种代币的。
跟踪一下也可以发现,无论是发起交易者还是接受交易者,都没有发生实际代币的变化。
但交易却被记录了下来,这是怎么回事呢?
三、漏洞分析 智能合约是一种于1994年被提出的,在没有第三方的情况下进行的可信交易。而以太坊在区块链上实现了一种图灵完备的语言solidity,允许人们在区块链上编写代码来实现智能合约。而智能合约的成熟催生了合约代币的产生,合约代币中只有遵守以太坊ERC20标准的合约代币才会被承认为ERC20代币,ERC20代币会直接被交易所承认。
在ERC20标准中规定,transfer函数必须触发Transfer事件,事件会被记录在event log中,而各大平台与交易所也是通过解析event log来获取交易信息。
会触发事件的转账代码
1 2 3 4 5 6 7 8 9 function transfer(address _to , uint256 _value ) public returns (bool) { require(_value <= balances[msg.sender]); require(_to != address(0 )); balances[msg.sender] = balances[msg.sender].sub(_value ); balances[_to ] = balances[_to ].add(_value ); emit Transfer(msg.sender, _to , _value ); return true ; }
“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。HaoTian全新的引入了对opcode的反编译审计模块,正在逐步应用到对智能合约的审计中。
这里我们使用HaoTian最新的反编译功能针对该智能合约进行简单的恢复源码。
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 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) { 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 ) { 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) { return balances[_owner ] ; } function renounceOwnership() { 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事件的函数。
1 2 3 4 5 6 7 8 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代币交易信息,是通过event log事件获取的呢?我们来测试一下。
事件复现 首先我们需要编写一个简单的ERC20标准的代币合约
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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进行部署(由于需要交易所获得提示信息,所以我们需要部署在公链上)
测试合约地址
1 https:// etherscan.io/address/ 0 xd69381aec4efd9599cfce1dc85d1dee9a28bfda2
然后直接发起交易
然后我们的imtoken提示了消息
回看余额可以发现没有实际转账诞生。
五、总结 回顾前面的测试,可以发现这是一件有趣的利用ERC20标准本身被信任的问题,攻击者花费了约2.28美元的gas,对1000个用户有针对的发送了广告,花费小额的gas完成了广告这个过程。
事件的核心就在于,ERC20作为标准要求要遵守才可以被承认,但交易所/平台却盲目信任符合ERC20标准的合约,将平台本身原理上的bug利用到发放小广告上,是一次比较特别的体验。
可以说,这是智能合约的一次极为特殊的漏洞利用,本身不涉及盗币,但却会对现在的交易所已经平台造成严重的危害,而且其本身底层逻辑bug难以从底层修复,只能在上层做修复,但黑名单的过滤方式很难真正奏效,一个属于区块链广告的黑暗时代到来了…