作者:启明星辰ADLab
近期,启明星辰ADLab联合电子科技大学的陈厅副教授【1】追踪到了以太坊token中的首个自动化薅羊毛攻击事件。被攻击的合约地址为0x86c8bf8532aa2601151c9dbbf4e4c4804e042571【2】,token名称为Simoleon (SIM),截止目前有接近57万账户持有该合约的token:
经过深入分析,我们发现攻击者对以太坊的理解非常深刻,其通过部署攻击合约获得了超过700万的token,从57万账户中脱颖而出,一举成为该合约token的第四大持有者:
在token发行过程中,为增加人气发行方可能会选择空投,即在一定时间窗口和投放总量的条件下免费给参与地址发送一定数量的token。Simoleon合约也加入了空投能力,在总量发放完之前,任何未接受过该token空投的账户都可以调用本合约的transfer函数给指定账户地址免费转账token:
我们分析攻击账户的token来源,通过etherscan查看该账户的token交易记录,一共分为两项:709万和1万,分别来自不同的账户地址:
继续跟踪709万token的交易记录,其来源有三项:550万,155万和3万,都来自同一个账户地址0x73fa30b609249166fa425f34b7142591c92b5548:
分析发现地址0x73fa30b609249166fa425f34b7142591c92b5548是一个闭源智能合约,由账户0x0075fd99426911b2e9650114bedc1195ad027f21创建,该合约总共有23笔交易:
Simoleon合约对已经发放token的地址都进行了记录,因此每个地址只能免费获得1万的token。要获得更多的数量,就需要创建很多新账户,然后利用这些新账户调用Simoleon合约的transfer函数给指定账户来获得大量token。如果通过创建EOA账户来进行薅羊毛,则每个账户都需要一些以太币,否则EOA账户就没有gas去调用Simoleon合约的transfer函数,也就无法薅羊毛。显然,给大量EOA账户发送以太币会增加额外的交易手续费。
攻击者在这里采用了一种精妙的攻击手法:首先部署一个恶意的攻击合约,地址为0x73fa30b609249166fa425f34b7142591c92b5548;然后调用攻击合约中签名为0x2b6cab44的函数来动态生成临时合约;最后临时合约在构造函数中直接调用Simoleon合约的transfer函数给攻击合约发送token。因为动态生成的临时合约都是未领取过token的账户,所以都能自动获得空投份额。攻击者只需要给攻击账户发送以太币就有足够的gas调用攻击合约,同时攻击者还能控制临时合约的生成数量。
通过分析攻击合约的23个交易数据,得到攻击者对0x2b6cab44函数的调用过程如下:
其中第一笔的交易(0x7f87268e06b76644ad4b8e162fb76d1c6e2e0480029b14db0e013f7d7d195e2f)细节如下,攻击者通过0x2b6cab44函数的参数来控制只生成2个临时合约,2个临时合约分别获取1万的token,然后发送给攻击合约。临时合约最后还进行了自动销毁,使自己的代码从区块链上消失。
攻击者前面4笔的交易中,每次生成的临时合约数量越来越大。其中第4次生成了80个临时合约,但是执行失败了,查看原因是gas不足。
攻击者在后续的交易中,每次都选择只生成50个临时合约。理论上攻击者可以增大允许的gas消耗量,从而一次性创建80个临时合约来加快薅羊毛,但攻击者却选择一次50个的搬砖模式。通过深入分析,我们发现攻击者对以太坊的底层机制相当熟悉,比如这里的gas消耗问题。
在以太坊中,每一笔交易都会消耗一定量的gas,实际消耗量由交易的复杂度决定,但不会超过发送者指定该交易能允许的最大消耗量。逆向分析0x2b6cab44函数后发现它是在循环创建临时合约,显然循环次数越多,这个函数调用的交易复杂度就越高,实际的gas消耗量就越大,允许的gas消耗量也就必须设置为越大。临时合约数量设置为50的时候,gas实际消耗量为3096224;临时合约数量设置为80时,gas实际消耗量为6174290。
然而,每个区块又有最大gas消耗量的阈值,并且每个区块至少要容纳1笔交易,因此以太坊链上任何一笔交易的gas消耗量都不允许超过区块的阈值。这进一步导致,在矿工最优化收益方案中,如果一个交易允许的gas消耗量过大,就会倾向性把这个交易排除在区块外,从而导致该交易失败。攻击者通过探测尝试找到最佳临时合约数量为50(因为80个临时合约的gas消耗已经超过允许的gas消耗导致交易失败),从而实现一次调用可以薅更多token,同时又不会触发gas问题。
行家一出手,就知有没有!不愧是有知识的攻击者。
在追踪分析过程中,我们发现了另外一起问题,即etherscan对token的变化记录是不准确的,甚至可能完全是错误的。如下是攻击合约的第一笔token收入,2个临时合约总共获得2万token,然后发送给了攻击合约:
紧接着攻击合约产生了第一笔token支出,发送了3万token给攻击者:
根据etherscan中的交易记录,攻击合约的收入只有2万,结果支持了3万。多支出的1万出自哪里,是否凭空产生?为何这1万的变化,etherscan没有任何记录?
经过分析,找到原因就在Simoleon合约的transfer函数,合约在空投的时候也会对目标地址进行免费发送token,但是这个发放却没有调用Transfer事件:
于是,攻击合约的token变化过程如下:
(1)攻击合约被创建,token为零。
(2)攻击合约0x2b6cab44函数第一次被调用,创建2个临时合约:
a)第1个临时合约初始化时调用Simoleon的transfer函数,该函数首先给临时合约空投initialize(msg.sender),此时临时合约的token为1万;然后给攻击合约空投initialize(_to)
,此时攻击合约的token为1万;最后把临时合约的1万token发送给攻击合约,同时调用事件Transfer记录临时合约给攻击合约的发送量为1万。至此,攻击合约的token总计为2万,由空投的1万加上临时合约发送的1万构成。
b)第2个临时合约初始化时调用Simoleon的transfer函数,该函数首先给临时合约空投initialize(msg.sender),此时临时合约拥有的token为1万;然后把临时合约的1万token发送给攻击合约,同时调用事件Transfer记录临时合约给攻击合约的发送量为1万。至此,攻击合约的token总计为3万,由先前2万加临时合约发送的1万构成。
(3)攻击者从攻击合约提取全部token,提取总量token为3万。
因此,从攻击合约账户自身的收支平衡来看,并没有凭空产生token,只是etherscan未能监控到攻击合约因为空投获得的1万token。这说明了etherscan对token变化的监控是基于Transfer事件,而不是基于每个账户的实际token变化。这会带来一些潜在的威胁,因为Transfer事件在本质上只是一个道德约束,EVM本身对其没有任何限制能力,攻击者可以不调用事件,也可以任意修改该事件的参数,实现诸如:实际转移的token量比事件通知的数量不匹配。对于频繁交易token的节点来说,这是一个新的挑战,因为该节点将难以通过链上数据追踪每笔token的来源,因为Transfer事件不再是准确的。
Simoleon合约能被自动化的薅羊毛,最根本的原因在于其合约代码的空投奖励函数没有任何权限控制,任意未领过空投的账户都可以直接调用Simoleon合约来获取。因此,攻击者可以利用攻击合约来创建大量账户来领取。一种缓解办法是给空投奖励函数设置权限控制,比如只有合约创建者才能给目标地址发放token。
区块链的特性决定了合约代码一经部署就无法修改,无论是合约的开发者,还是投资者,还是交易平台,都应当对合约进行多重安全审计,尽量在上链前消除安全风险。
参考链接:
【1】Ting Chen, University of Electronic Science and Technology of China.
http://faculty.uestc.edu.cn/chenting/zh_CN/index.htm
【2】Simoleon (SIM).
https://etherscan.io/address/0x86c8bf8532aa2601151c9dbbf4e4c4804e042571
启明星辰积极防御实验室(ADLab)
ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员。截止目前,ADLab通过CVE发布Windows、Linux、Unix等操作系统安全或软件漏洞近400个,持续保持国际网络安全领域一流水准。实验室研究方向涵盖操作系统与应用系统安全研究、移动智能终端安全研究、物联网智能设备安全研究、Web安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。