以太坊網(wǎng)絡(luò)上備受矚目的游戲 Fomo3D(Fomo3D:Long)其第一輪在昨天(北京時(shí)間 8 月 22 日下午 3 點(diǎn)左右)結(jié)束了。最終,地址為 0xa169... 的玩家獲得了 10469.66 Eth 的獎(jiǎng)金,其取款交易被記錄在了 6191962 區(qū)塊中,該玩家在游戲中的總投入不到 0.8 Eth。那么,是不是這個(gè)玩家真的是靠運(yùn)氣“中了大獎(jiǎng)”呢?當(dāng)然不是,這是個(gè)有計(jì)劃、有預(yù)謀的、精心設(shè)計(jì)的“技術(shù)性攻擊”所取得的勝利結(jié)果。
本文將為大家講解這次“技術(shù)性攻擊”的原理、過程和攻擊中的關(guān)鍵技術(shù)細(xì)節(jié)。本文的講解假設(shè)讀者了解以太坊、智能合約、礦工/礦池、交易打包確認(rèn)以及 gas 等術(shù)語的基本概念。
從游戲設(shè)定看達(dá)成”攻擊“所需的必要條件
Fomo3D 是近一個(gè)多月以太坊上最火爆的應(yīng)用,也是個(gè)賭博游戲,本文的目的是做技術(shù)分析,所以這里只介紹其結(jié)束的設(shè)定:
- 游戲啟動(dòng)后從 24 小時(shí)開始倒計(jì)時(shí);倒計(jì)時(shí)結(jié)束時(shí),最后一個(gè)夠買 key 的玩家將獲得獎(jiǎng)池中 48% 的獎(jiǎng)金;
- 每有一個(gè)玩家購買 key,倒計(jì)時(shí)會增加 30 秒。
所以,獲勝條件實(shí)際上很簡單:在自己購買 key 之后到游戲倒計(jì)時(shí)結(jié)束,不再有其他人購買 key。在現(xiàn)實(shí)世界中,要做到這點(diǎn)不那么容易,除非所有玩家都沒錢了;但在區(qū)塊鏈的世界中,具體到以太坊上,是可以通過“技術(shù)手段”做到不讓其他人購買的(也就是不讓其他人的“購買交易”得到“網(wǎng)絡(luò)確認(rèn)”)。這就是大家耳熟能詳?shù)摹熬芙^服務(wù)攻擊”(Denial of Service,DoS)。
攻擊的原理
在目前成熟的 Web 服務(wù)技術(shù)里,制造 DoS 攻擊一般是通過大量的并發(fā)請求和/或大數(shù)據(jù)量的獨(dú)立請求,將 Web 服務(wù)的帶寬/服務(wù)資源占滿,而使其無法再相應(yīng)正常的數(shù)據(jù)請求。在以太坊中,則可以通過制造大量的“垃圾合約調(diào)用”來達(dá)到同樣的效果。
這里需要來講一個(gè)機(jī)制了:交易池(transaction pool)。在礦工/礦池節(jié)點(diǎn)上,通常都會有一個(gè)交易池,網(wǎng)絡(luò)上廣播的所有新的交易都會被首先加入這個(gè)“池”,而后再由礦工/礦池選擇那些“經(jīng)濟(jì)性更好”的交易優(yōu)先打包確認(rèn)。這里說的“經(jīng)濟(jì)性”,即由交易發(fā)送者在交易數(shù)據(jù)中指定的 gasPrice,gasPrice 越高,執(zhí)行交易所附帶的合約代碼的執(zhí)行費(fèi)用也就越高,而這些費(fèi)用通常是會作為手續(xù)費(fèi)支付給礦工的。所以,礦工/礦池會從交易池中選取那些 gasPrice 明顯高于其他交易的交易來優(yōu)先打包執(zhí)行(確認(rèn))。并且,礦工并不能從技術(shù)上判斷一個(gè)交易中附帶的程序代碼是否是“垃圾合約調(diào)用”(它們也沒有這個(gè)“責(zé)任”),它們僅僅選取那些執(zhí)行費(fèi)用更高的交易來優(yōu)先執(zhí)行。基于這個(gè)原理,就允許了攻擊者通過調(diào)高包含了“垃圾合約調(diào)用”的交易的 gasPrice,來在短時(shí)間內(nèi)用這些“無效交易”占用區(qū)塊的可用 gas,以使其他“正常交易”無法被打包進(jìn)區(qū)塊。
這里還有幾個(gè)基礎(chǔ)知識需要科普一下:
- 以太坊中的區(qū)塊可包含的交易(計(jì)算量)是由區(qū)塊的 gasLimit 來控制的,而并不是像比特幣那樣用數(shù)據(jù)大小來限制;以太坊中目前區(qū)塊的 gasLimit 上限是 800 萬 gas,礦工可以做 5% 以內(nèi)的上下浮動(dòng);區(qū)塊內(nèi)能包含多少交易,是看這些交易執(zhí)行所消耗的總 gas 是否達(dá)到這個(gè)區(qū)塊的 gasLimit;
- 以太坊中執(zhí)行交易的費(fèi)用,是用交易基礎(chǔ)執(zhí)行費(fèi)用的 21000 gas,加上交易中附加的代碼的字節(jié)大小的費(fèi)用(這里有一個(gè)折算公式,不詳細(xì)講了)以及實(shí)際執(zhí)行代碼所消耗的 gas 的總和乘以交易中指定的 gasPrice 來計(jì)算的;這個(gè)交易費(fèi)用,會從交易發(fā)送者賬戶中自動(dòng)扣除;如果交易發(fā)送者賬戶余額不足,交易不會被打包進(jìn)區(qū)塊;
- 以太坊中的交易的實(shí)際執(zhí)行所要消耗的 gas 是可以根據(jù)交易執(zhí)行時(shí)的“世界狀態(tài)”明確知道的,也就是這個(gè)交易的實(shí)際執(zhí)行費(fèi)用是明確知道的,礦工就是據(jù)此來判斷打包交易的“經(jīng)濟(jì)性”。
在 Fomo3D 游戲的后期(即獎(jiǎng)池金額已經(jīng)很高),大多數(shù)玩家都會選擇在倒計(jì)時(shí)的最后數(shù)分鐘內(nèi)才去購買 key,以讓游戲能繼續(xù)下去。這時(shí),如果有一個(gè)機(jī)會,在攻擊者自己購買了 key 之后(這只會給剩余時(shí)間增加 30 秒),能在其后數(shù)分鐘內(nèi)讓網(wǎng)絡(luò)不再確認(rèn)其他人的購買交易,攻擊者就可以讓游戲結(jié)束從而贏得大獎(jiǎng)。
這里還有一個(gè)需要科普的就是所謂“30 秒規(guī)則”。以太坊網(wǎng)絡(luò)目前是基于 PoW 共識的,節(jié)點(diǎn)之間是通過“競爭”來決定記賬權(quán),這會導(dǎo)致區(qū)塊鏈末端的“不穩(wěn)定”,也就是會分叉。所以實(shí)際上某個(gè)交易會包含在哪個(gè)區(qū)塊是可能在短時(shí)間內(nèi)變化的,但基于過往的經(jīng)驗(yàn)數(shù)據(jù),如果合約中用區(qū)塊的時(shí)間戳來判斷,那么這個(gè)時(shí)間的精度大概會有 30 秒的誤差,這就是所謂的“30 秒規(guī)則”。
因?yàn)?Fomo3D 合約中的結(jié)束時(shí)間是使用 now(也就是當(dāng)前區(qū)塊的時(shí)間戳)來判斷的,所以如果要攻擊的話,一定要多攻擊 30 秒。比如攻擊者在倒計(jì)時(shí) 2 分鐘時(shí)購買 key,這會使倒計(jì)時(shí)增加 30 秒,然后基于 30 秒規(guī)則,就需要保證在之后的 3 分鐘內(nèi)沒有其他玩家的交易被打包確認(rèn)。實(shí)際的攻擊也是這樣進(jìn)行的。
攻擊的過程
下面我們就來根據(jù)區(qū)塊鏈瀏覽器中可以查到的實(shí)際數(shù)據(jù)來看看這個(gè)攻擊是如何發(fā)生的:
- 區(qū)塊號 6191896:確認(rèn)了一個(gè)由 0xa169… 到 Fomo3D:Long 的交易,調(diào)用了 buyXid 函數(shù)(即購買了若干 key);這個(gè)區(qū)塊的時(shí)間戳是 06:48:22(UTC)。
- 區(qū)塊號 6191897 到 6191902:攻擊者開始使用“垃圾合約調(diào)用”來填充區(qū)塊(大概占用了這幾個(gè)區(qū)塊中的一半左右的可用 gas),但沒有刻意調(diào)高 gasPrice(使用的是平均水平,20 GWei 左右),這是個(gè)非常有耐心、也非常大膽的處理,在看到 6 個(gè)區(qū)塊的時(shí)間內(nèi)沒有其他人購買 key 之后,攻擊者知道機(jī)會來了!
- 區(qū)塊號 6191903 到 6191908:從 6191903 開始,攻擊者將“垃圾合約調(diào)用”交易的 gasPrice 提高到了 190 GWei,即平均水平的 8 倍以上,后續(xù)交易更是設(shè)置了 500 GWei 的超高 gasPrice,開始了真正的 DoS 攻擊!直到 6191908 區(qū)塊,這 6 個(gè)區(qū)塊中只包含了不到 10 個(gè)簡單的轉(zhuǎn)賬交易(即不包含合約執(zhí)行的簡單交易,固定消耗 21000 gas),其他可用 gas 完全被這些高 gasPrice 的“垃圾交易”占用。
- 區(qū)塊號 6191909:網(wǎng)絡(luò)狀況恢復(fù)正常。這個(gè)區(qū)塊的時(shí)間戳是 06:51:17(UTC)。在這個(gè)區(qū)塊中,我們可以看到數(shù)個(gè)調(diào)用了Fomo3D:Long 的 buyXaddr 和 buyXid 的交易,但因?yàn)橛螒蚝霞s內(nèi)的時(shí)間戳判定條件已經(jīng)達(dá)到游戲結(jié)束,所以這些購買當(dāng)然就沒有效果了。
值得一提的是,在區(qū)塊 6191907 中,我們會看到一個(gè) gasPrice 高達(dá) 5559.7 GWei 的調(diào)用 Fomo3D:Long 的 buyXaddr 函數(shù)的交易,但很可惜,這個(gè)交易的 gasLimit 設(shè)置過低(僅設(shè)置了 379000)導(dǎo)致發(fā)生了 out of gas(即交易觸發(fā)的合約執(zhí)行實(shí)際 gas 消耗超過交易的 gasLimit)的錯(cuò)誤,而白白花費(fèi)了 2.1 Eth 的手續(xù)費(fèi),卻沒有搶到最終大獎(jiǎng)!這應(yīng)該是某個(gè)大神在讀秒階段發(fā)現(xiàn)了攻擊者的企圖,但由于時(shí)間過于緊張,沒有將 gasLimit 設(shè)置到合理范圍(大概是手誤少輸入了一個(gè) 0)。是不是有點(diǎn)兒看黑客大片的即視感???
可以看到,攻擊者的計(jì)劃、準(zhǔn)備周密,很有耐心,且技術(shù)處理上幾乎無懈可擊,完美地達(dá)成了必要的 DoS 攻擊(短時(shí)間內(nèi)阻止了其他玩家的交易被確認(rèn)),從而“技術(shù)性獲勝”。
攻擊中的幾個(gè)技術(shù)細(xì)節(jié)
首先,我們可以看到在上邊提到的這十幾個(gè)區(qū)塊中包含了很多“失敗”的交易,這些失敗的交易有個(gè)共同的特點(diǎn),都是由 Bad Instruction 導(dǎo)致的。這里的 Bad Instruction 也就是以太坊協(xié)議里預(yù)設(shè)的 EVM 操作碼 0xfe(無效指令)。
這里再科普一個(gè) Solidity 語言的技術(shù)細(xì)節(jié):
Solidity 中有三個(gè)指令可以撤銷本次合約執(zhí)行中的所有狀態(tài)修改并導(dǎo)致合約執(zhí)行“異常停止”:require、revert 和 assert。根據(jù) EVM 的指令設(shè)計(jì),require 和 revert 實(shí)際上最終都是使用了 EVM 操作碼 0xfd(停止執(zhí)行,但會返還交易執(zhí)行所剩余的 gas,也就是會返還一部分執(zhí)行費(fèi)用),它們實(shí)際上都是 revert,只不過 require 指令在執(zhí)行 revert 之前做了一個(gè)條件檢查;而 assert,則在條件滿足時(shí)會使用 EVM 操作碼 0xfe(無效指令,會消耗交易附帶的所有可用 gas) 。
攻擊者用來完成 DoS 攻擊的合約源代碼并不是公開的,但我們可以從實(shí)際的合約字節(jié)碼中看到一些端倪(因?yàn)檫^于技術(shù)化,這里不再展開討論)。
然后,從這些“垃圾交易”的整體設(shè)計(jì)上看,也是很有學(xué)問的。這些交易的 gasLimit 并不都是一樣的,而是從十幾萬、幾十萬到幾百萬這樣的離散值。這是因?yàn)樵趩?dòng)攻擊的時(shí)候,網(wǎng)絡(luò)狀況仍然是正常狀況,所以各大礦工/礦池可能已經(jīng)有了打包了一半的區(qū)塊,這時(shí),當(dāng)它們收到了新交易之后,除了判斷經(jīng)濟(jì)性以外,還會判斷其 gas 消耗能否在當(dāng)前區(qū)塊的剩余可用 gas 中包含。比如有些礦池打包的區(qū)塊中已經(jīng)只剩不到 50 萬 gas,這時(shí)那些超過百萬的大交易自然就不能包含進(jìn)去;這樣,如果沒有適合的 gas 量的“垃圾交易”來填充,就有可能讓其他玩家的正常購買交易填充進(jìn)去。所以,從攻擊的角度講,這些 gasLimit 比較小的“垃圾交易”同樣是非常重要也是非常必要的!我們不得不佩服攻擊者思路的縝密。
最后,要完成這樣精確的攻擊,攻擊者需要很多技術(shù)準(zhǔn)備。
他們需要若干能連接到前五乃至前十礦池(或者能連接到與這些礦池節(jié)點(diǎn)在“網(wǎng)絡(luò)上”非常接近的全節(jié)點(diǎn)),這一點(diǎn)非常重要。因?yàn)橐獙?shí)施這樣的攻擊,你必須具備能實(shí)時(shí)獲知各大礦池節(jié)點(diǎn)最新區(qū)塊數(shù)據(jù)的能力,以便在發(fā)起最終的 DoS 攻擊之前能確定沒有其他人的正常購買交易被打包!也就是剛剛提到的 6191897 到 6191902 區(qū)塊的等待期,在越多的大礦池節(jié)點(diǎn)數(shù)據(jù)中得到確認(rèn),攻擊成功的幾率越高。
然后,在發(fā)起攻擊的時(shí)候,一定要在短時(shí)間內(nèi)將用來攻擊的數(shù)十個(gè)“垃圾交易”同時(shí)發(fā)送到前五乃至前十礦池,讓他們把這些交易加入“交易池”;以最大限度地避免因?yàn)榫W(wǎng)絡(luò)延遲導(dǎo)致其他玩家的購買交易被某個(gè)大礦池先打包的情況;這同樣對攻擊的完成至關(guān)重要!
以上這兩點(diǎn),需要攻擊者同時(shí)擁有數(shù)個(gè)可以聯(lián)動(dòng)的定制化的客戶端,并且有相應(yīng)的程序進(jìn)行監(jiān)控(檢查區(qū)塊數(shù)據(jù))并發(fā)起實(shí)際攻擊(連續(xù)發(fā)送數(shù)十個(gè)預(yù)設(shè)的交易),這大概不是通過單個(gè)客戶端或者簡單地用幾個(gè)腳本就可以做到的。
小結(jié)
從 Fomo3D:Long 第一輪游戲的結(jié)束來看,雖然我們可以搞懂整個(gè)過程以及其中的技術(shù)細(xì)節(jié),但能不能先于別人實(shí)施、考慮到盡可能多的細(xì)節(jié)、盡量提高成功的概率就是個(gè)純粹的技術(shù)活兒了;也需要大量的時(shí)間和精力以及資金支持。不過這個(gè)例子也給了我們更大的動(dòng)力去研究技術(shù)、去學(xué)習(xí)細(xì)節(jié),只有掌握了足夠多的細(xì)節(jié)才能做到一擊必中!不是么?
攻擊者在這次攻擊中的總投入成本當(dāng)然不是開頭說的在合約上花的那點(diǎn)兒錢,這些“垃圾交易”的執(zhí)行費(fèi)用是非常高的,包括攻擊者先前在主網(wǎng)上做的各種試水,總成本粗略估計(jì)在 40 Eth 以上。貌似也不是我等屌絲能負(fù)擔(dān)的啊……。