DAO损失1.2亿美金的“重入漏洞”,重现江湖

降维安全
降维安全 机构得得号

Oct 11, 2018 降维安全旨在打造区块链最全面、最有深度的服务解决方案。

摘要: 以太坊的智能合约出现重入漏洞(Reentrancy)已是老生常谈了,其中最著名的一次莫过于2016年6月发生的the DAO事件,此次事件导致了价值约6千万美元的以太币被盗,并直接促使了当年7月以太坊的硬分叉,即以太经典ETC和目前的以太坊ETH。

最近,降维安全实验室(johnwick.io)监测到成人娱乐系统spankchain的支付通道(payment channel)关联的智能合约 LedgerChannel也遭到了此类攻击。某黑客发现了该支付通道合约的重入漏洞(Reentrancy),并于北京时间2017年10月7日上午8时许创建了恶意攻击合约,随后成功从该合约窃取了165.38 ETH,约合3.8万美元价值的以太币。

相关以太坊地址
攻击者(attacker): 0xcf267eA3f1ebae3C29feA0A3253F94F3122C2199
攻击合约(attack contract):
攻击合约A: 0xc5918a927C4FB83FE99E30d6F66707F4b396900E
攻击合约B: 0xaaaD8d7AE50d5dd6fFA9d29A2531ab2a67803A1f
被攻击合约(victim contract): 0xf91546835f756DA0c10cFa0CDA95b15577b84aA7
相关交易哈希(TxHash)
attacker创建攻击合约A 0xce3a58b81273b3e7735fccdce0ea5f664720d8a23d0c4471379fed01acb4837b
窃取(0.5-0.1) ETH0x84033e0c908cab415359b5a1a54289a533b20b8450836ceb13190848c2aac6a8
窃取(160-5) ETH0x21e9d20b57f6ae60dac23466c8395d47f42dc24628e5a31f224567a2b4effa88
窃取(7-0.5) ETH0xf95e87181d4f0ca831c15e3f401818d06b7c3a281fbccd9544a4669133078099
窃取(1.2-0.1) ETH0x2228e2ac9fe71f517eec12e4d9d68217c725ef21bb407c82d1dda00709137ac1
窃取(1.52-0.76) ETH 0x41af661b529967c83dd61e489a0a0728378fb74a961f15b2c800637fe332c6bc
attacker创建攻击合约B 0x8a18c77dd4a1d4a602d2d7607b2440a88f54d0585e3c4fe7c3d679621bc4c1d5
窃取(2.64-1.32) ETH 0xf120b79aa0af659d23b9824f6a68c8ccfb63cfd63b5e45f8658cee558935b45d
窃取(0.60-0.30) ETH 0xd8d5a14f57925db1b745e2b4427c4fc1d5a59587a6c9288c3b772d7533a68876
攻击者交易截图

分析
被攻击合约
导致payment channel合约发生重入漏洞的函数是 createChannel(bytes32,address,uint256,address,uint256[2])和 LCOpenTimeout(bytes32).

用户可以通过 createChannel向合约存入以太币/代币, 并通过 LCOpenTimeout()让合约返还自己之前存入的以太币/代币,下面我们对这两个函数逐一分析,看看问题出在哪里.

createChannel()

392~395行: 如果用户提供的入参以太币余额 _balance[0]不为0,那么用户需要提供等量的以太币ETH,即 msg.value==_balance[0].

396~400行: 如果用户提供的入参代币余额 _balance[1]不为0,那么合约将调用用户提供的入参ERC20标准合约地址 _token内函数 transferFrom(),将代币存入合约内. 注意这里的 _token是用户可控的

406行: Ledger Channel开启超时 LCopenTimeout由 now和用户提供的入参 _confirmTime相加得到,即用户可控这个超时值.

407行: 将 _balance[0]和 _balance[1]保存到 initialDeposit.

LCOpenTimeout()

我们来看导致重入问题的关键函数.

414行: 需要检查当前块的时间戳 now超过 LCopenTimeout, 如前所述,这个超时值用户可控,直接pass.

416~418行: 如果用户存入的以太币不为0, 那么合约会通过以太坊虚拟机内置的 transfer()函数将所有该用户存入以太币退还给用户. 请注意这里, 转账结束后并没有立即将用户以太币余额 Channels[_lcID].ethBalances[0]清零! 我们接着看.

419~421行: 如果用户存入的代币不为0, 那么将调用用户提供的ERC20标准合约地址 token里的 transfer()函数,将所有该用户存入的代币退还给用户. 注意,如前所述,这里的 transfer()转账函数完全由用户控制.

426行: 终于完成了以太币和代币操作,合约在链上删除用户的信息 Channels[_lcID],包括以太币余额 ethBalances[0]等.

以上是正常的执行流程,如果一个攻击者部署的是一个恶意合约 token,并在其中的 transfer()函数中调用 LCOpenTimeout()函数, 那么 LCOpenTimeout()会在412~421行间不断循环,并不会执行到426行去清除该用户的以太币余额数据,导致合约重复将以太币退还给攻击者,造成重入漏洞攻击.

总结
合约开发者不能信任任何用户提供的数据,包括但不限于public/external函数的入参, 回调合约的地址等.
关键操作必须原子化, 譬如在以太币/代币转账成功后,对应账户的余额修改必须立即执行,假如转账失败,必须通过 revert()等操作抛出异常,回滚状态。

(作者:降维安全,内容来自链得得内容开放平台“得得号”;本文仅代表作者观点,不代表链得得官方立场)

链得得仅提供相关信息展示,不构成任何投资建议
本文系作者 降维安全 授权链得得发表,并经链得得编辑,转载请注明出处、作者和本文链接

更多精彩内容,关注链得得微信号(ID:ChainDD),或者下载链得得App

分享到:

相关推荐

    评论(0

    Oh! no

    您是否确认要删除该条评论吗?

    分享到微信