作者:启明星辰ADLab

8月19日,God.Game在以太坊区块链上部署了自己的智能合约(地址位于https://etherscan.io/token/0xca6378fcdf24ef34b4062dda9f1862ea59bafd4d,简称God合约),时隔一天攻击者就盗取了该合约的243个以太币,价值超过6万美元。启明星辰ADLab在监控到该事件后,对该攻击进行了详>,简称God合约),时隔一天攻击者就盗取了该合约的243个以太币,价值超过6万美元。启明星辰ADLab在监控到该事件后,对该攻击进行了详细的分析和重现。

攻击回溯

通过etherscan可以看到攻击者的以太币提取交易:

交易详情如下,即攻击者0xc30e89db73798e4cb3b204be0a4c735c453e5c74(简称攻击者1)调用了God合约的withdraw函数进行提币:

查看攻击者1在God合约中是否持有token,接近20万的数量。

查看攻击者1在withdraw调用之前对God合约的调用,如下:

从攻击者1的交易来看,它发往God合约的最早交易是sell调用,说明在sell之前它就已经有了God合约的token。那么攻击合约在此之前,肯定有其它账户给它转移过token。否则,它不会有可以sell的token。

在追踪攻击者1的token变化过程中,我们发现另外一个攻击者(简称攻击者2,地址为0x2368beb43da49c4323e47399033f5166b5023cda),它调用了一个攻击合约(地址为0x7f325efc3521088a225de98f82e6dd7d4d2d02f8)给攻击者1转移了20万token:

攻击者2调用攻击合约的transfer函数,目标地址为攻击者1,数量为20万。由于攻击合约并没有开放源码,因此这里的transfer函数仅仅是函数签名匹配的结果(有一定几率是其它名字)。那么,攻击合约的20万token是从哪里得到的?

继续跟踪攻击者2和攻击合约,发现攻击合约是由攻击者2创建的,且攻击者2对攻击合约的调用就是在God合约被攻击提现的时间窗口中。

从攻击者2的交易行为,可以看出他先给攻击合约转入4.3 token,然后从攻击合约转出4.3 token。此时,攻击合约的token为0。随后,攻击合约直接转20万token给攻击者1(还转移了21万给另外一个地址),这表明攻击者在调用reinvest函数时应该使攻击合约的token发生了某种变化。

接着分析这个reinvest交易,它是直接调用不开源的攻击合约,其内部机制我们并不清楚。但是,这个交易过程会触发God合约的两个事件,onTokenPurchase和onReinvestment:

通过这个事件的记录数据,可以看到该reinvest调用使得合约判定购买token的以太代币数量为一个大数,并且远远超出以太币发行总量。这个信息也反应出reinvest函数内部逻辑一定产生了某种非预期的行为。

通过分析God合约源码,发现onReinvestment事件仅在God合约的reinvest函数中触发:

可见,onReinvestment的以太币参数的最终计算方式为:

这一行代码明显存在整数溢出的理论可能,因为它没有使用SafeMath等类似安全运算操作。但这里溢出的值并不是经典0xfffff…等类似的大数,而是一个够大但又远不及uint256极大值的数。

仔细观察发现,magnitude变量是2的64次方,然后我们做一个等式变换:

这样我们就找到了经典整数溢出的第一现场指纹,只需要上面减法操作的第一操作数比第二操作数略小即可。很显然,第一操作数又可划分为两个子操作数的乘法,只要任一个为零即导致结果为零。此时,第二操作数只要是任意一个小正数即可产生上面的经典指纹。

继续构造上述的操作数。首先,tokenBalanceLedger_[_customerAddress]在合约调用的上下文中表示调用者持有的token。因此,只要调用者不持有合约token,这个值就是零。此时无论profitPerShare_值为多少,乘法结果都为零。这样减法的第一操作数为零的条件,就轻易构造出来了,即调用者不持有God合约token。然后,payoutsTo_是一个mapping对象,合约调用者的初始值为零,需要使其为一个正数。

分析God合约中修改payoutsTo_的代码有:

攻击合约在reinvest调用之前只执行过transfer调用和withdraw调用。其中transfer调用从攻击合约转token到外部账户,所以不会修改合约的payoutsTo_值,但withdraw函数会直接修改合约的payoutsTo_值。因此,只要在reinvest之前调用一次withdraw函数就可以使得减法的第二操作数为一个正数。

最后,第一个操作数为零值,第二个操作数为正数,并且减法结果强制转换为无符号整数,在没有运用安全运算库的前提下直接使用减法操作就会导致溢出,结果为一个很大的正数。至此,攻击者的完整攻击过程如下:

Remix复现

在分析了完整攻击路径后,我们可以构造出如下的攻击合约:

在remix中按照如下步骤进行操作:

1)部署God合约(为了方便追踪内部数据结构的变化,直接把全部成员和函数都重新定义为public);

2)用1eth,购买第一次token,引用地址设置为0x00…;

3)再用相同的参数来购买一次(一定要再来一次,因为此时合约的profitPerShare_仍然是零值,这会导致withdraw调用的函数修饰符失败);

4)部署攻击合约Test(传递God合约地址给Test);

5)调用God合约的Transfer给Test发送Token(这里直接把购买的全部token都发送过去);

6)调用攻击合约Test的withdraw函数,攻击合约的payoutsTo_已经被修改为大数;

7)调用攻击合约Test的transfer函数把token全部给创建者,Test此时拥有的token为0,payoutsTo_为大数;

8)调用攻击合约的reinvest函数,在日志中可以看到记录购买token的eth为海量,并且成功购买了大量token;

9)攻击合约Test通过溢出获得了大量token,攻击者就可以从这个合约给其它地址转移token,并进行售卖套取eth。

小结

God合约被攻击的漏洞点比较简单,即标准的整数溢出。它的复杂在于整数溢出的利用有多个约束条件,并且是在不同的业务逻辑中:

  1. 在溢出攻击的业务逻辑中,攻击者必须没有God的token,且payoutsTo_值必须为正数;

  2. 要使payoutsTo_为正数,攻击者就必须在其它业务逻辑中修改,比如withdraw;

  3. 要执行withdraw,攻击者就必须持有God的token(最终溢出时又不能持有token)。

因此,攻击者需要通过多次触发God合约的不同业务逻辑才能最终造成整数溢出。

God合约的代码编写存在多处缺陷:

  1. 给管理员留下任意地址的token操控能力,并且操控不触发事件。这意味着修改是悄无声息的,除非有人去轮询监控每个地址的token变化;

  2. Token的某些转移过程没有调用标准ERC20事件接口,导致etherscan上看到的token变化是极度不准确的,不利于公开透明监督;

  3. 代码中不考虑限制循环,无意义的gas浪费(这也导致了在Remix调试中经常崩溃);

  4. 合约中的业务逻辑没有说明规范,仅开放合约代码并不能等价于项目透明。


启明星辰积极防御实验室(ADLab)

ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员。截止目前,ADLab通过CVE发布Windows、Linux、Unix等操作系统安全或软件漏洞近400个,持续保持国际网络安全领域一流水准。实验室研究方向涵盖操作系统与应用系统安全研究、移动智能终端安全研究、物联网智能设备安全研究、Web安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/684/