你想了解黑客的世界嗎?你是否想體驗一把黑客的感覺嗎?如何使用重入攻擊合約去攻擊一個 NFT 項目?菠菜將帶你們體驗黑客的視角呈現如何找到智能合約中的漏洞並進行攻擊
由於本文涉及到智能合約代碼技術,所以菠菜為了照顧不懂技術的小夥伴會將每一行代碼的邏輯都解釋清楚方便小夥伴們理解智能合約並找到其中的邏輯漏洞
謹記:本文案例僅用於科普教育用途,攻擊智能合約是違反道德的行為,它可能會導致用戶在無意中失去其加密資產,也可能會對整個區塊鏈網絡的安全性構成威脅
本文案例來源於斯坦福大學 2021 年的加密技術課程期末考試題,題目鏈接:https://cs251.stanford.edu/hw/final2021.pdf
如果你不懂什麼是智能合約(Smart Contract)和函數(Function),那麼讓菠菜先來解釋一下:智能合約是一種運行在區塊鏈上自動化的、可編程的程序。我們向智能合約輸入某些參數,它會自動根據編寫者編寫的代碼邏輯執行特定的操作,我們熟知的各種 NFT 項目以及各種 Token 就是一個個智能合約。
而函數就是智能合約中的某一個具體功能,當我們調用智能合約中的函數傳入參數就會觸發這個函數的代碼邏輯,比如菠菜在鏈上給韭菜轉了 100USDT 其實就是調用了 USDT 智能合約的 transfer(轉移)函數,圖中我向函數傳入了我要轉的地址和數量等參數,智能合約就會根據我傳入的參數執行操作
那麼什麼是重入攻擊呢?#
區塊鏈中的重入攻擊是指攻擊者在智能合約中重複執行某些函數,從而導致合約的異常行為。重入攻擊是智能合約中最常見的一種攻擊,每年幾乎都會有數次重入攻擊的事件導致各種項目高達千萬美金的損失。
最著名的重入攻擊事件莫過於 2016 年,以太坊上的 The DAO 合約被重入攻擊,黑客盜走了合約中的 3,600,000 個 ETH,這件事情也導致了以太坊將賬本回滾至黑客未攻擊之前,從而誕生了以太坊的硬分叉 — 以太坊經典(ETC),所以有意思的是現在的以太坊賬本中這個重入攻擊事件並沒有發生
那麼現在就讓我們走進黑客的視角,了解黑客是如何找到漏洞進行重入攻擊的
我們來看題目:下面的 Solidity 代碼片段用於一個包含 16384 個非同質化代幣(NFT)的空投。用戶可以通過調用 NFT 合約上的 mintNFT()函數一次領取多達 20 個 NFT。
(下圖為該函數源代碼,懂技術的小夥伴可以先嘗試找找漏洞
問題有三個:
A) 假設已經鑄造了 16370 個 NFT,因此 totalSupply()== 16370。請解釋一個惡意合約如何導致超過 16384 個 NFT 被鑄造。攻擊者可以引起的 NFT 的最大數量是多少?
提示:如果調用地址的 onERC721Received()是惡意的,會發生什麼?仔細檢查鑄造循環,並考慮重入漏洞。
在 A 問中我們需要找到合約中的邏輯漏洞,那麼在 B 問中我們就需要實現具體攻擊的合約代碼
B)編寫一個惡意的 Solidity 合約代碼,實現第(a)部分的攻擊,假設 totalSupply()的當前值為 16370。
在 C 問中我們將修復這個重入攻擊的漏洞
C)您將在上一頁的代碼中添加或更改哪一行 Solidity 代碼,以防止您的攻擊?請注意,單個交易不應鑄造超過 20 個 NFT。
菠菜來總結一下:
這個題目說這是一個 NFT 總供應量上限為 16384 的 NFT 合約的鑄造(mint)函數,一個地址一次性最多鑄造 20 個 NFT,假設現在已經被鑄造了 16370 個 NFT,怎麼做可以讓鑄造出來的 NFT 超過其規定的總量上限?最多可以超多少個?怎麼實現攻擊代碼?怎麼修復漏洞?
接下來咱們開始解題,懂技術的小夥伴可以先自己嘗試一下,那麼為了照顧不懂技術的小夥伴們菠菜對每一行代碼都用中文進行了解釋,並且接下來也會對邏輯進行梳理,所以不必擔心看不懂
首先我們可以看到要想調用 MintNFT 這個函數需要輸入的參數是無符號整數(理解為數字就可以了)並且還有幾個前置要求(require):
1. 調用的時候總供應量(total supply)必須 < 16384
2. 鑄造的數量必須 > 0
3. 一次性不能鑄造 > 20 個
4. 已鑄造數量 + 這次鑄造數量不能超過 16384
5. 價格必須匹配數量
只要滿足了前置要求後,調用函數後就會觸發一個 for 循環,不用管 for 循環是什麼,總之觸發之後它就會先檢查目前 NFT 的數量,然後根據你輸入的參數(數字)去循環調用 SafeMint 這個函數(這個就是打給你 NFT 的那個函數),比如你輸入 14 就會反復調用 14 次 SafeMint 這個函數,你也就得到了 14 個 NFT
那麼 SafeMint 函數被調用後呢,合約就會檢查要鑄造的 NFT 是否已經被鑄造過,沒問題就會給你的錢包地址打 NFT 過去,並且會在目前的 NFT 發行量上 + 1,如果我們正常去鑄造這個合約的話,我們最多只能輸入並調用 16384-16370=14 次 MintNFT 函數,因為每調用一次當前發行量就會 + 1,這個數無法超過上限 16384
那我們要如何操作才可以使得鑄造的數量超過規定的總量 16384 呢?#
我們可以注意到下面還有一個假設條件,如果調用地址是一個合約賬戶的話,就會先檢測這個合約是否接收 ERC721 標準(NFT)的 Token,如果不可以就會被拒絕,那麼同時還會觸發 NFT 合約的 onERC721Received 函數
那麼這個 IERC721Receiver 和 onERC721Received 又是什麼呢?
簡單來說,IERC721Receiver 是一個接口(interface),一個合約必須實現這個接口才可以收到 ERC721 標準(NFT)的 Token,這也是為什麼要先檢測這個合約地址是否可以接收 ERC721,如果對方合約沒有實現這個接口的話是收不到 NFT 的
要想實現 IERC721Receiver 這個接口的話,就必須在合約中加上 onERC721Received 這個回調函數
因為在 ERC721 標準中,一個合約向另外一個合約發送 NFT 時會調用對方的 onERC721Received 函數傳入一些參數如發送方地址、NFT 原始持有者地址、NFT 編號以及附加數據,同時該函數也會返回一個值表示已成功接收
什麼是回調函數呢?#
回調函數通常用於處理接收到的外部數據,例如收到其他合約的轉賬或者收到其他合約的 NFT 時觸發的處理邏輯。簡單來說就是收到轉賬後自動觸發執行邏輯的函數,那麼除了返回一個值告訴對方成功接收外,還可以編寫一些自定義的代碼邏輯
那麼這個回調函數其實就是我們要去攻擊這個 NFT 合約的重點,首先我們知道如果我們使用我們自己的錢包(如 Metamask)去調用 MintNFT 函數的話,我們最多只能鑄造 14 個 NFT。
但如果我們使用智能合約去調用 MintNFT 函數呢?NFT 合約的漏洞在哪?攻擊邏輯是什麼呢?
首先我們需要知道:只要滿足那 5 個前置條件(require)就可以調用 MintNFT 函數,要想找到 NFT 合約中的邏輯漏洞主要就需要從前置條件中下手。
那麼我們要想實現重入攻擊的話,就需要在滿足前置條件的情況下重複去調用 MintNFT 函數使得鑄造出來的 NFT 數量超過規定的總量
攻擊的邏輯為:
在攻擊合約中的回調函數 onERC721Received 中加上攻擊代碼,當攻擊合約收到來自於 NFT 合約的 NFT 時,就會自動觸發回調函數的攻擊邏輯去再次調用 NFT 合約的 mintNFT 函數從而達到重入攻擊的效果。
具體邏輯讓菠菜慢慢道來:
首先攻擊合約會傳入 14(16384-16370=14)這個數去調用 MintNFT 函數,如果填 15 的話就會無法通過前置條件(4. 已鑄造數量 + 這次鑄造數量不能超過 16384),那麼通過前置條件後調用 MintNFT 函數就會進行 for 循環反復調用 14 次 SafeMint 函數,每一次循環調用都會觸發一次攻擊合約的回調函數
每鑄造一個 NFT 轉移到攻擊合約就會觸發一次回調函數的攻擊邏輯,那麼這個回調函數的攻擊邏輯其實就是每收到一個 NFT 就會自動去調用 NFT 合約的 mintNFT 函數一次,說的可能有點繞,我來梳理一下:
1. 攻擊合約傳入 14 這個數字作為參數調用 NFT 合約的 mintNFT 函數
2. 由於 14 這個參數符合前置條件的要求所以可以成功調用,mintNFT 函數就會進行 for 循環調用 14 次 Safemint 函數進行鑄造 NFT,那麼這一步攻擊合約最終會得到 14 個 NFT
3. 由於攻擊合約是智能合約,所以 NFT 合約在鑄造時會調用攻擊合約的 onERC721Received 函數去檢查目標合約是否實現 IERC721Receiver 接口
4. 由於 NFT 合約會執行 14 次 Safemint 函數,所以會調用 14 次攻擊合約的回調函數,所以會觸發 14 次攻擊合約的回調函數攻擊邏輯
5. 具體攻擊邏輯為:收到第 1 個 NFT 後,回調函數觸發邏輯去再次調用 mintNFT 合約,輸入參數為 13,因為已經收到了 1 個 NFT,距離總量上限還差 13 個 NFT,所以參數 13 可以通過前置條件
6. 在收到第 2 個 NFT 後,回調函數繼續觸發攻擊邏輯去調用 mintNFT 函數,輸入參數為 12,12+2=14,之後每收到一次以此類推,只要不超過 14 這個數字就可以通過前置條件的檢測
7. 所以 A 問的答案為:由於前置條件有邏輯漏洞,利用帶有惡意攻擊邏輯的回調函數可以鑄造超過總量 16384 的 NFT
8. 除了原本鑄造的 14 個 NFT 外,攻擊者最多可以鑄造的 NFT 為:13+12+11+10+9+8+7+6+5+4+3+2+1=91 個 NFT
那麼看到這不懂技術的小夥伴可能一臉懵逼,不是已經鑄造了 14 個 NFT 嗎?為什麼還能繼續鑄造?前置條件的漏洞出在哪?#
讓菠菜繼續一一道來
首先大家需要了解一下以太坊的出塊機制,簡單來說,當我們調用智能合約時候傳入的所有參數和合約狀態都會打包成一個事務等待礦工處理出塊,那麼在沒有出塊之前,這些事務本質上並沒有成為 “事實”,也就是說在沒有出塊之前,第一次調用的 14 個 NFT 並沒有真正被鑄造出來
沒有真正被鑄造出來意味著在調用合約的時候,在包含這個事務的區塊出塊之前他的現有發行量會一直保持在 16370 這個數,那麼我們再來看前置條件中的漏洞,我們只需要關注這兩條:
1. 調用的時候總供應量(total supply)必須 < 16384
4. 已鑄造數量 + 這次鑄造數量不能超過 16384
那麼結合出塊機制和前置條件,我們就明白問題出在哪了,由於沒有出塊之前現有發行量會一直保持在 16370 這個數,我們看第一條前置條件,由於沒有出塊前發行量數字是無法變的,所以不管調用幾次都是可以通過第一條的,我們再來看第四條,只要已發行數量 + 鑄造數量不超過 16384 就可以通過
也就是說只要鑄造的數量不超過 14 就可以通過第四條前置條件,那麼這樣邏輯就清晰了:
1. 因為沒有出塊之前數字不會變動(合約狀態)
2. 只要每一次鑄造不超過 14 個 NFT 就可以完美繞過第一條要求和第四條要求進行重入攻擊
3. 攻擊合約每收到一個 NFT 就會重複調用 mintNFT 再次鑄造總數不超過 14 個的 NFT
那麼在出塊後,NFT 合約的最終狀態就是 mintNFT 函數其實被調用 14 次,分別是鑄造了 14+13+12+11+10+9+8+7+6+5+4+3+2+1=105 個 NFT,那麼在 B 問中,我們需要將這次攻擊的合約實現出來,那麼就讓我們開始來設計攻擊合約吧
1. 首先我們需要在攻擊合約中實現 IERC721Receiver 接口,這樣我們就可以接收 ERC721 標準的 NFT 了
2. 我們將 NFT 合約的地址定義為 hashmasks(就是個名字)
3. 然後我們創建一個攻擊函數,開始編寫攻擊的邏輯
4. 我們設 num 為攻擊合約已持有的 NFT 數量
5. 只要 num 這個數 < 14,就會去調用 NFT 合約的 mintNFT 函數,調用傳入的參數為 14-num 不斷遞減
6. 然後我們開始編寫 onERC721Received 回調函數,首先我們按照官方格式將接收的參數格式設置好,然後加上我們自定義的攻擊邏輯:一旦接收到 NFT 就會自動觸發 attack 攻擊函數去調用 NFT 合約的 mintNFT 函數
知道了具體的攻擊邏輯之後呢,那麼讓我們來看看 C 問吧
C)您將在上一頁的代碼中添加或更改哪一行 Solidity 代碼,以防止您的攻擊?
可以將_totalSupply++;
這一行放到_safeMint
方法中驗證 NFT 的調用之後:
這樣,當合約被重入攻擊時,由於 _totalSupply
還沒有增加,因此在第二次進入 mintNFT
函數時 mintIndex
的值是第一次 mint 的值,會導致觸發 'ERC721: token already minted'
這個錯誤,有效保證合約安全,重入攻擊就會失敗。
關於 B 問和 C 問的答案源自於:https://github.com/qiwihui/blog/issues/157
最後在此致敬所有加密行業安全領域的工作者和白帽們,加密世界是一個黑暗森林,同時也是黑客的樂園,每天都有許多安全事故頻頻發生。
在這個充滿挑戰和機遇的行業中,感謝所有為了行業安全做出貢獻的探索者,也希望菠菜的文章能幫屏幕前的小夥伴們提高了認知
如果大家喜歡我的文章可以關注我的微信公眾號啦
如有錯誤的地方歡迎指出~
文章鏈接: