Skip to content

科學家是怎麼開掛的:揭秘一個價值千萬的 NFT 漏洞

來源: @litangsongyx | 原文連結

日期: Sat Nov 29 09:33:33 +0000 2025

標籤: 智能合約安全 隨機數漏洞 DeFi 攻擊


我來整理這篇關於 NFT 盲盒漏洞的文章。這是一個非常經典的智能合約安全案例,涉及隨機數生成漏洞和區塊鏈存儲的特性。

整理完成的正文如下:


來源@litangsongyx日期:2021 標籤NFT 智能合約安全 隨機數漏洞 Solidity DeFi


事件背景

2021 年底,BSC 鏈上有個叫 Defina 的 GameFi 項目。遊戲上線前流行炒 NFT,NFT 根據稀有度定價,遊戲裡的 SSR 英雄卡二級市場地板價炒到了 6000 USDT 以上,而稀有度通過開盲盒來抽卡。

但在當時大家都很菜的情況下,這開盲盒所謂的「運氣」,其實全是「算術題」。

這場狩獵最終全網獲利接近 1000 萬人民幣。更有意思的是,項目方為了防科學家,連續換了 3 個版本的合約,但最終都被一一擊穿。

聲明:本文複盤的是 2021 年的歷史事件,旨在揭示「代碼即法律」在早期的黑暗森林中的真實博弈。文中提及的漏洞邏輯僅供學習 Solidity 安全機制,請勿在現實項目中模仿。Web3 安全建設不易,Respect developers.


第一回合:V1 版本 — 完全沒有防護

最初作者從 PancakeSwap 打新拿到了這個項目的幣,用幣買了盲盒來開英雄。結果和玩原神一樣,兩千 U 下去,無事發生,只出了張 SR 和幾張 R。

出於好奇,他去看了合約代碼,發現是開源的,就看了隨機函數。

當你看到一個涉及千萬資金的盲盒合約,其隨機數生成邏輯居然是這樣寫的時候,你就知道機會來了:

solidity
function _randModulus(uint mod) internal view returns (uint) {
    uint rand = uint(keccak256(abi.encodePacked(
        block.timestamp,   // <--- 漏洞點 1: 礦工和科學家完全可知
        block.difficulty,  // <--- 漏洞點 2: 基本上也是固定變量
        msg.sender         // <--- 漏洞點 3: 攻擊者自己可控
    ))) % mod;
    return rand;
}

這在 Solidity 編程裡屬於教科書級別的反面教材。因為 block.timestamp(區塊時間戳)和 block.difficulty(區塊難度)在出塊的那一瞬間,對於礦工和科學家來說,是「已知量」。對於 BSC 這條鏈,當時固定的難度和固定的三秒出塊間隔,更讓這兩個數值完全可以預測。

邏輯解析

這段代碼的邏輯是:

使用 區塊時間 和 區塊難度 以及 發起地址 生成一個數字,用這個數字 / 卡池裡卡的數量 得到餘數,這個餘數在卡池中對應的卡牌,就是你抽出的卡牌。

破解方式

項目方也留了一手,他們把存放卡片 ID 的數組 cardIds 設為了 private(私有變量)。他們以為加上 private,大家就看不到這個數組了。

這太天真了。在 EVM 裡,private 只是防其他合約直接讀取,防不了科學家。

Storage 是完全透明的。甚至不需要去翻交易記錄,直接反編譯拿到這個數組存在合約的哪一段存儲插槽中,再用 web3.eth.getStorageAt 遍歷存儲插槽,SSR 到底對應哪個 Index,完全清清楚楚。

在這種情況下,寫一段腳本,卡準時間,開出你想要的 SSR 卡,已經是再簡單不過的事了。


第二回合:V2 版本 — 反而給科學家打助攻

至今都不知道 V2 版本是在什麼情況下上線的,可能項目方發現了有人在開 SSR,他們停用 V1,部署了 V2 合約。

項目方在慌亂之中,不僅沒有修復隨機數源頭的問題,反而把 open 函數裡唯一的防禦機制 onlyEOA 給刪掉了

V1 vs V2 代碼對比

solidity
// V1: 只能用 EOA 地址調用
function open(uint tokenId) whenNotPaused onlyEOA external {
    require(_isApprovedOrOwner(_msgSender(), tokenId), "...");
    burn(tokenId);
    // ... 開箱邏輯
}
solidity
// V2: 居然把 onlyEOA 刪了!合約也能調了!
function open(uint tokenId) whenNotPaused public { // <--- 漏洞!
    require(_isApprovedOrOwner(_msgSender(), tokenId), "...");
    burn(tokenId);
    // ... 開箱邏輯
}

升級版破解方式

V1 時,還需要精準卡時間戳,如果卡慢了還得虧盲盒費。到了 V2,因為沒了 onlyEOA,完全可以寫個合約去調用開盲盒合約:

  1. 第一步:卡時間調用 open() 開箱
  2. 第二步:檢查開出來的 NFT ID
  3. 第三步:如果不是 SSR,直接 revert() 回滾交易

直接零成本試錯,最多虧個 gas,但 BSC 上的 gas 可以忽略不計


第三回合:V3 版本 — 依然沒理解底層問題

被 V2 坑慘後,項目方急眼了。

這次他們學「聰明」了,上線了 V3 版本,並且採取了兩個看起來很硬核的措施:

  1. 閉源:不再公開合約代碼
  2. 人工注入種子:不再單純依賴時間戳,而是用腳本每隔 5 分鐘調用一次合約,注入一個外部隨機數種子,然後通過一系列計算,得出最終的隨機數種子

他們的邏輯是:「我不開源,你不知道我怎麼算的;我人工注入,你沒法預測我輸入什麼。」

依然可破解

不管你的計算邏輯多複雜,也不管是機器生成的還是人工餵的,最終生成的那個用來做隨機數的 Seed,只要你存在合約的 Storage 裡,科學家就能拿到

破解路徑:

  1. 反編譯:雖然沒有源碼,但把 V3 的字節碼拉下來反編譯,分析出它是如何讀寫 Storage 的
  2. 定位插槽:找到了那個存儲「人工種子」的 Slot 位置
  3. 內存監控:腳本加一段代碼,實時監控這個 Slot 的值

滑稽的結果

場面變得很滑稽:

  • 項目方每 5 分鐘辛苦地去鏈上餵一次數據
  • 而腳本以 0.1 秒的頻率監控著那個位置
  • 種子一更新,立刻讀出來,結合時間戳,繼續開想要的 SSR

這場遊戲最終以項目方向 Chainlink 請救兵而告終。

他們在官方 Medium 發了一篇長長的報告,裡面有一句話讓人印象深刻:

"We had a theory about how the hacker exploited the contract but we are not able to repeat the action to prove the theory. Thus, we are offering a Bounty Program to the first person who could repeat his exploits on the testnet."

(我們推測了黑客的攻擊方式,但我們無法複現該操作。因此,我們推出漏洞賞金計劃,獎勵第一個能夠在測試網上重現該漏洞的人。)

來源:https://defina-finance.medium.com/important-notice-to-definians-a5f4a69e0ff5

最終,Defina 被迫遷移到了 V4 版本,並接入了 Chainlink VRF。這雖然貴,但是唯一的正解。

要不是有個哥們開得太猛太喪心病狂了,社區也不會發現這件事。

獲利規模

後來大概統計,全網的科學家獲利應該在千萬人民幣左右,早期 SSR 的價格達到 4000 Fina,而 Fina 的價格曾一度達到 10U。


技術總結

版本防護措施漏洞破解方式
V1private 數組 + onlyEOA可預測隨機數 + Storage 可讀反編譯讀取 Storage,卡時間戳開盲盒
V2刪除 onlyEOA合約可調用 + 可預測隨機數合約調用 + 不滿意就 revert,零成本試錯
V3閉源 + 人工注入種子種子存在 Storage反編譯 + 監控 Storage Slot,實時讀取種子
V4Chainlink VRF無明顯漏洞-

核心教訓

  1. 鏈上隨機數生成的根本缺陷block.timestampblock.difficultymsg.sender 這些都是可預測或可控的變量,不能用於生成安全的隨機數
  2. Storage 完全透明private 關鍵字只能防合約間調用,防不了鏈下讀取
  3. 人工注入種子治標不治本:只要種子最終存在鏈上,就能被讀取
  4. 唯一正解是 Chainlink VRF:真正的隨機數需要鏈下預言機支持

在這個黑暗森林裡,代碼即法律,漏洞即利潤

Curation Desk

這篇文章要放去哪一層?

AI Priority59
待審 預設狀態:待審 · 已寫入文章 metadata

我來整理這篇關於 NFT 盲盒漏洞的文章。這是一個非常經典的智能合約安全案例,涉及隨機數生成漏洞和區塊鏈存儲的特性。

先檢查外部連結是否值得保留,再決定是否轉入精選。