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

原创
2213 天前
2125
事件

以太坊的智能合约出现重入漏洞(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行去清除该用户的以太币余额数据,导致合约重复将以太币退还给攻击者,造成重入漏洞攻击.

总结

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