如何打好一次酱油——第四期白帽黑客大赛复盘
一. 规则及代码分析
这是一次11支队伍之间的对抗,大家需要在阻止其他队伍的同时尽可能的通过破解合约取的最多的eth
,这个是本次挑战的规则。
代码分析
-
GamerVerifier
:控制权限,存有所有参赛者地址。-
addGamer(address addr, uint magic)
:可添加新地址,但前提是破解出参数magic
的值。
-
-
CommonWalletLibrary
:钱包的库合约,用来提供运行时的代码流程。-
initializeVault()
:首次调用者可成为本合约的owner
,在lastUpdated
12小时后可发动攻击让本合约自毁,并获得合约所有余额。 -
resolve()
:可自毁合约,需要owner
权限,且需要lastUpdated
12小时后。 -
addToReserve()
:给本合约充值,并更新lastUpdated
为当前时间(续命,使owner
无法自毁合约),但每次需耗费3eth
。
-
-
TimeDelayedVault
:目标合约,一个限制取钱数额和时间的多人共享钱包,存有1eth
,每30min
只能取钱0.001eth
-
resolve()、addToReserve()
的功能与CommonWalletLibrary
中基本一致。 -
addAuthorizedAccount(uint votePosition, address proposal)
:组员投票,授权唯一能进行取钱操作的地址(此地址可为任意地址)。 -
withdrawFund()
:取钱操作,有漏洞,可实施re-entry
攻击,但只有授权地址才能取出钱。 -
initilizeVault()
:首次调用者可成为本合约的owner
,在lastUpdated
12小时后可发动攻击让本合约自毁,并获得合约所有余额。- 因为拼写错误,在构造函数中并未成功调用,可被所有参赛者调用(斗智斗勇关键所在)
-
-
oobserver
:空白合约,需要自由发挥实现observer()
方法,可影响合约取钱的结果
攻防分析
-
TimeDelayedVault
的withdrawFund()
可用来实施re-entry
攻击,但除去gas
,收入不多。需要先投票设置取钱地址,主要考验团队配合。 -
GamerVerifier
的addGamer()
可添加非报名地址,用于各种攻防操作,防追踪;同时可隐藏参赛地址通过addToReserve()
加钱,增加收益。 - 抢占自己和其他小队的
owner
权限,然后等时间到,调用resolve()
收割;但任何人都可调用addToReserve()
刷新lastUpdated
来阻止收割。 -
oobserver
的observe()
会在取钱时触发,若其他人在取钱时不提前更新,将会执行我们注入的代码,如:无限循环耗尽gas
二. 赛事发展
Episode 0:序
比赛开始前一天,自由组1就已经发现了上一期廖大写的大赛复盘(廖大在上一期就因为攻占了大部分小组的owner
,导致比赛提前结束)。
在发现这一情况后,助教Steven
布下了陷阱:用不同的地址来部署不同组的合约实现隔离,然后给每一组又部署了10个傀儡合约,让发现这些傀儡合约的小组以为发现了其他小组的地址。
我们——自由组2
说实话,在比赛开始之前我们一直都是处于懵逼的状态,对于比赛如何攻防完全没有经验。既没有搜集以往的资料,也没有留意其他小队的信息。所谓的准备就是学习总结了Solidity 安全:已知攻击方法和常见防御模式综合列表。
Episode 1:疑云
比赛开始之后,很快好几个组都发现了owner
的漏洞,并且纷纷攻占了各自组傀儡合约的owner
。大家都很高兴,所有的owner
一个都没有被其他组攻破。大家所不知道的是,这些傀儡合约是可以立马自毁的;但是大家都非常开心的自愿等了12个小时。
12小时之后,这些组陆续发现不仅没收到eth
,eth
还转给了一个神秘地址。这时候,大家是一脸懵逼的。
而且,为了防止大家直接resolve()
自毁了自己的真实合约(这样比赛也就提前结束了),助教发布了第一条赛事播报:暗示大家可以赚很多的eth
,不要急着把自己只有一个eth
的合约给resolve()
了。
我们——自由组2
在拿到合约代码之后,简单看了下只发现了re-entry
漏洞,对部分代码也没有完全理解。因为还是新手,也就不管不顾的尝试进行攻击(这么做非常不应该,既浪费了gas
,又增加了自己合约暴露的风险)。
攻击没有什么效果,但是,在对攻击流程进行地址分析的时候,意外的顺着调用发现了CommonWalletLibrary
和GamerVerifier
的地址。
我尝试着对GamerVerifier
的magic num
进行破解,但因为不得要领没有成效(还是在后面,通过tx
追查对GamerVerifier
的调用,将输入数据从16进制转换为10进制之后,就一目了然了)。
因为对CommonWalletLibrary
地址的意外发现,我们顺势占领了owner
;而且还以为自己合约的owner
也在手中(后面才真正发现,我们的owner
早已落入0x158...
的魔爪)。
与此同时,组里的 GB 踩中了助教的陷阱,兴奋地占领了好几个傀儡合约。
Episode 2:觉醒
随着收割傀儡合约失败,陆续有小组开始反应过来合约可能是假的,(助教语)最搞笑的是有一组以为这些傀儡合约是给大家准备的重生用的备用合约,以为自己和自己的备用合约给KO了。
随着越来越多的组发现之前占领合约不对劲,大家开始从其他队伍的账户地址入手,追踪其他的真合约,但是因为大部分组的owner
都已经被自己占了,所以这个影响不大。
F组首先明确了通过magic number
配合脚本接水的撸管策略(比赛是在测试网上进行,使用的eth
都是通过“水管”一滴一滴的获得),随后各组纷纷跟上。
此时,助教又一轮的带节奏,提醒大家可以刷新对手的lastUpdated
,从而阻止对手通过自毁来获得大量的eth
。
我们——自由组2
在这个阶段,我们悲哀的发现自己的合约早已被占,更加伤心的是在不知情的情况下我们已经往里送入了几百eth
。在自己已经无法得到的情况下,只能通过定时的续命来阻止对手获得这些eth
了。
而在撸水管的过程中,由于本小组都不擅长脚本,只能靠手动接取,远远跟不上脚本小组的节奏。好在大家休息时 WZC 和远在西雅图的 XY 弄到了几千个。
唯一让人欣慰的就是CommonWalletLibrary
的owner
在我们手中,在局势还不明朗的情况下,其他人都在瑟瑟发抖的担心我们将其引爆而提前结束,直至在助教的提醒下有人开始给合约续命。
Episode 3:混战
到了这个阶段,有的组开始重生合约,有的组开始狙击合约,有的组在奋力撸水管。
以为自己稳操胜券的 J 组,甚至在引爆第二个合约前(之前通过自爆收获了2000eth
)提前算出了自己组的得分。但是他们因为没有藏好自己的合约,一声枪响数万的eth
就这样化成了泡影。
枪林弹雨中,其他组的合约也纷纷倒下,一场混战后战场又恢复了死寂。
我们——自由组2
我们已经沦为了酱油党,但是却没有放弃让别人打酱油的期望。
我想出了一个利用observe()
来实施DDos
攻击的方案,需要的就是定期注入到其他合约中,只要别的小组取钱时忘记更新,就会因不断循环耗尽gas
而导致调用失败。
同样的点,我又想出了一个不通过目标合约而直接向我们地址转入大量eth
的方案,只是苦于没有脚本撸水管,只能干着急。
Episode 4:狙击
就在大群渐渐没有声音的时候,助教们在 B 组和 H 组的群里居然发现了两个保存完好的合约,并且都已拿到了足以扭转战局的eth
。
他们都是通过使用非参赛地址拿到了合约的owner
,从而完美的隐藏了自己。这时,助教们本着看热闹不嫌事大的心态进行了赛事播报,号召大家狙击最后的两个合约。
B 组的 LQ,通过扫描transaction
快速锁定了 H 组的合约,并且进行了狙击。随着时间的推移,B 组离引爆自己合约的时间越来越近,胜利似乎就在前方。
我们——自由组2
我们在酱油的事业上愈行愈远,见到大群里的加菲猫大佬撸水管撸到了数万eth
,却苦于合约被续命无法拿到;而我们想出了剑走偏锋的方案却撸不到水,于是经过商量,提出了卖方案的想法。
我们达成了5000eth
这一皆大欢喜的协议,就在即将转账成交的时刻,被助教们紧急喊停,在经过他们的讨论之后,宣布我们的方案超出了考核范围:无效。无奈之下,我们梦碎。
Episode 5:终局
最后关头,除了 B 组的所有组几乎都在寻找最后一份合约,希望能够成功狙击;但是因为有很多组为了隐藏自己的真实合约,部署了很多假合约,导致找到 B 组合约非常困难。
就在离引爆合约还有1个小时的时候,J 组同学齐心协力成功找到了 B 组的合约狙击了最后一头巨鲸;同时,J 组也因为在之前成功引爆过自己的合约(收获2000eth
),成为了本次黑客大赛的最终优胜队伍。
三. 收获
在这次比赛中,各路大神展现了惊人的实力,让人印象最深的就是各种脚本用的贼溜,脚本与人肉之间的差距根本就不是在一个维度上,让我们甘拜下风;同时,也向我们指明了后续学习的方向。
本次比赛的主要收获有:
- 掌握了合约调用
call & delegatecall
的差别; - 深刻的理解了区块链上一切都是透明的;
- 认识到了脚本对“战斗力”的提升,也了解到了自己对
JS & web3
掌握的不足; - 思路一定要清晰,一定要稳。就是因为对代码的解读不够,才导致没有掌控自己的
owner
,从而丧失了主动权。