在 Minecraft Beta 1.8 至 1.12.2 版本中,每當一個方塊被破壞時,掉落物品的精確座標就可以揭示另一個玩家的位置。
「Randar」是 Minecraft 的漏洞,它使用 LLL 晶格縮減來破解 Minecraft 伺服器中錯誤重用的java.util.Random
的內部狀態,然後從該狀態反向查找當前加載到世界中的其他玩家。
點擊此處以 YouTube 影片的形式了解 Randar。
觀看縮時攝影:
在此處(以文件形式)或此處(以 YouTube 形式)查看更多縮時攝影。
目標是確定世界上其他玩家的遊戲位置(即座標),無論他們相距多遠。我們在 2b2t 上玩,這是最古老、最著名的「無政府主義」Minecraft 伺服器(這意味著沒有規則,即玩家不會因任何原因被禁止)。做這樣的事情是這個伺服器上的「重點」。在這個伺服器上,唯一能保證你的東西安全的是地圖很大(3.6兆平方格)並且沒有其他人知道你在哪裡。因此,協調利用是一件大事(打破遊戲規則)。 (說到,在 Randar 之前,從 2018 年到 2021 年,我們還對 2b2t、Nocom 進行了另一個協調利用;請參閱此處的文章,HackerNews 線程,YT)
Minecraft 程式碼中的錯誤從版本 Beta 1.8(2011 年發布)到 1.12.2(2017 年發布,但 2b2t 一直保留在該版本上直到 2023 年 8 月 14 日)都存在。錯誤在於,隨機數產生器java.util.Random
的各種實例在程式碼的各個部分中被草率地重複使用(並且它們一開始就是不安全的)。具體來說,在產生地形和遊戲動作(例如採礦區塊)之間重複使用 RNG。
該漏洞利用總結如下:
World.rand
將其種子重置為塊坐標的函數,以便檢查附近的 Woodland Mansion 應該在哪裡(以及是否是這個塊)。World.rand.nextFloat()
呼叫來選擇0 和1 之間的XY 和Z 座標決定。精確的XYZ 值。World.rand
的確切內部狀態。從廣義上講(稍後會詳細介紹),觀察 RNG 的一個輸出可能意味著 RNG 大約 1600 萬種可能的內部狀態中的任何一種。然而,我們不僅對 RNG 的輸出進行了一次採樣,而且連續採樣了三次(掉落物品的 X、Y 和 Z 座標),我們知道內部狀態如何在每次呼叫之間更新(一個簡單的乘法) ,加,然後取模);因此我們可以使用格子方法來立即將其縮小到唯一的可能性。java.util.Random
的內部狀態可以像向前一樣輕鬆地向後步進,並且通過向後步進,我們可以在短短幾千步內找到它(即使在像2b2t 這樣的在繁忙伺服器上,有很多玩家,因此很重) RNG 的使用),它標識 RNG 內部狀態重置的最近時間,以及伺服器上載入的最新區塊的位置。即使您在已更新至較新版本 Minecraft 的伺服器上玩遊戲,或以其他方式修補了 RNG 操作,由於能夠追溯利用 RNG 數據,您的座標仍然面臨來自 Randar 的風險。一些 Minecraft 玩家使用像 ReplayMod 這樣的 mod 來記錄資料包,並且他們可能仍然保留這些日誌檔案。如果有人在你在基地時使用這樣的模組,他們可能(在不知不覺中)記錄了可能洩露你位置的RNG 數據,因為破壞方塊是一種極其常見的行為,很可能在此類記錄中發生,並且每個此類方塊Break 顯示伺服器的 RNG 狀態,從而顯示最近載入的區塊的位置。這意味著 Randar 是一個相當大的問題:由於這種追溯利用的風險,在每個Minecraft 伺服器上,在 Beta 1.8 到 1.12.2 版本中活躍的每個位置都應被視為受到威脅,即使伺服器早已更新到1.12 以上.2 或修補 RNG 作業。
如果您想自己使用 Randar 漏洞,請前往此處,rebane2001 創建了一個網站,您可以在其中拖入 1.12.2 中的 ReplayMod 檔案並查看其他玩家的座標。它在客戶端運行,因此您的錄音不會離開您的瀏覽器。這是該網站實際運行情況的視頻,您可以在此處下載該視頻中的示例 ReplayMod 文件。
Randar 是由 n0pf0x (pcm1k) 發現的。這篇文章是由 leijurv 撰寫的,最後還有一些由 n0pf0x 撰寫的附加評論。漏洞利用者包括 0x22、Babbaj、TheLampGod、leijurv、Negative_Entropy 和 rebane2001。請在此處觀看 TheLampGod 的影片。請在此處觀看 HermeticLock 100% 幽默 0% 事實的影片。
目錄:點擊此處更詳細地了解可利用程式碼,點擊此處了解如何使用晶格縮減,點擊此處了解我們如何保護我們自己的儲存免受Randar 的侵害,點擊此處如果您只想查看完整的利用程式碼,如果您執行的伺服器版本仍在 Beta 1.8 和 1.12.2 之間並且您想要修補 Randar,請按一下此處,或按一下此處以詳細了解 n0pf0x 與我們的做法有何不同。
錯誤圖(PDF):
漏洞利用圖(PDF 格式):
漏洞利用範例圖(PDF 格式):
《我的世界》在整個遊戲過程中都依賴隨機數。我們期望其中大多數實際上是隨機的,例如生物生成和天氣的隨機性,但我們期望其中一些是可預測的,例如我們期望相同位置的相同世界種子產生相同的地形。 2011 年,當 Notch 在 Beta 1.8 期間首次向遊戲添加結構時,他意外地重複使用了本應不可預測的 RNG,以便將村莊放置在世界中。從那時起,直到 1.13,這種草率的程式碼導致世界生成影響了遊戲中幾乎所有其他所謂的隨機事件。直到 2018 年 5 月左右,Earthcomputer 和朋友們才發現這個錯誤,意識到區塊負載以可觀察的方式影響遊戲的 RNG,請參閱此解釋。然而,他們沒有意識到,或者只是從未公開透露,你也可以向後執行此操作,透過觀察 RNG 來確定最近加載的區塊。 Randar 是由 n0pf0x(又名 pcm1k)於 2022 年 10 月 7 日發現的。他主要在 9b9t 上使用該漏洞,在 2b2t 和其他伺服器上只使用了相對少量的漏洞。在 2b2t 中,n0p 定位並探索了各個地點,最終到達了古靈閣的藏匿地點。他被 rebane2001 發現,最初對他如何找到該位置保持沉默。然而,大約一個月後,他開始與 SpawnMasons 討論此事。 n0p 透露他使用了強大的坐標漏洞,並決定在 2023 年 4 月與我們分享解釋,因為泥瓦匠們過去有大規模利用 2b2t 漏洞的經驗,所以看到這種情況再次發生會很有趣,n0p 是無論如何,有點厭倦了。我們接受並開始記錄幾個帳戶的物品掉落座標,這些帳戶已經為不相關的項目 24/7 開採石頭/圓石(因此,行為沒有變化)。我們重複使用了 nocom 的無頭 Minecraft 系統,並新增了 Postgres 資料庫來記錄測量結果。如同本自述文件後面所討論的,我們經歷了幾次軟體迭代來破解 RNG 測量結果,最終選擇了非同步 Cuda 批次作業。隨著破解的測量結果添加到資料庫中,我們還更新了包含熱圖資訊的分析表,該熱圖資訊以所有時間、每天和每小時的間隔對每個座標的點擊次數進行計數。這使得簡單的Plotly Dash UI 能夠從特定時間範圍和粒度中選擇熱圖資料以在瀏覽器中顯示,並且它讓我們能夠透過僅考慮在幾個不同小時內加載的座標來刪除所有Elytra stashhunting 區塊加載垃圾郵件。我們添加了一個簡單的共享註釋系統來追蹤我們在地圖上每個熱點發現的內容。再次從 Nocom 重用,我們有 Baritone 機器人,可以自動執行竊取物品儲藏和對結果進行排序的整個過程,完全 AFK。許多石匠在不知道漏洞利用的情況下使用munmap
和1248_test_user
等帳戶幫助完成了這部分。古靈閣的所有藏品最終增加到了 13 億件,其中大約一半屬於諾科姆,一半屬於蘭達爾。
FitMC 影片中解釋了完整的歷史。
《我的世界》的地圖是根據世界的初始種子按程式生成的,並且本質上是確定性的。當玩家探索地圖時,隨著玩家靠近,新區域會按需生成。由於所有生成都是可重複的(確定性的),因此他們在許多地方使用java.util.Random
是完全合理的。他們希望它是可預測的。這就是使用java.util.Random
的原因,因為它是一個 PRNG(不是真正的 RNG)。 P 在技術上意味著“偽”,但將其視為“可預測的”。可預測的RNG。它產生的數字看起來是隨機的,但在給定相同的起始種子的情況下,它們實際上是可重複的。
Minecraft 具有在世界中生成的各種結構,例如村莊、海洋紀念碑、要塞等。
要理解這一點只需要十幾行 Minecraft 程式碼,我已經對其進行了簡化和註解:
// (chunkX,chunkZ) is being loaded, and this function checks if it should generate a Woodland Mansion
protected boolean canSpawnStructureAtCoords ( int chunkX , int chunkZ ) {
// divide by 80, rounding down, to determine which "Woodland region" (my made up term) we're considering
int woodlandRegionX = Math . floorDiv ( chunkX , 80 );
int woodlandRegionZ = Math . floorDiv ( chunkZ , 80 );
// seed the random number generator deterministically in a way that's unique to this Woodland region
Random random = this . world . setRandomSeed ( woodlandRegionX , woodlandRegionZ , 10387319 );
// pick which chunk within this region will get the Woodland Mansion
int woodlandChunkX = woodlandRegionX * 80 + ( random . nextInt ( 60 ) + random . nextInt ( 60 )) / 2 ;
int woodlandChunkZ = woodlandRegionZ * 80 + ( random . nextInt ( 60 ) + random . nextInt ( 60 )) / 2 ;
// but is it *this* chunk, that we're loading right now?
if ( chunkX == woodlandChunkX && chunkZ == woodlandChunkZ ) {
// and, is this chunk in a biome that allows Woodland Mansions? (e.g. roofed forest)
if ( this . world . getBiomeProvider (). areBiomesViable ( chunkX * 16 + 8 , chunkZ * 16 + 8 , 32 , ALLOWED_BIOMES )) {
return true ;
}
}
return false ;
}
// and here's what it calls in World.java:
public Random setRandomSeed ( int seedX , int seedY , int seedZ ) {
this . rand . setSeed ( seedX * 341873128712L + seedY * 132897987541L + seedZ + this . getWorldInfo (). getSeed ());
return this . rand ; // this.getWorldInfo().getSeed() is the overall seed of the entire map, which has been cracked long ago for 2b2t (it's -4172144997902289642)
}
為了清楚起見,上面已進行了註釋並稍作修改,但它在功能上與真實程式碼是準確的。
所以我們的想法是決定 Woodland Mansion 應該在這個 Woodland 區域(80 x 80 塊)中的哪個位置,檢查那個地方是否就在這裡,如果是,則從這裡開始生成一個 Woodland Mansion。
這段程式碼可能看起來有點傻,你可能會想「對每個區塊進行所有這些檢查是荒謬的,只需選擇每個區域 Woodland Mansions 應該去的地方並完成它」。原因是 Minecraft 區塊是彼此獨立生成的,並且以未知的順序生成,但我們仍然希望從給定的種子生成一個確定性的世界。我們不知道玩家將以什麼順序走遍世界,能夠以無狀態的方式按需生成任何區塊是很好的。這是一次很好的遊戲體驗。因此,像這樣看起來很奇怪的程式碼。
不管怎樣,對於每個區塊加載,對於正在加載的區塊周圍的大正方形中的每個區塊,都會呼叫該程式碼。解釋原因有點複雜,所以我基本上會跳過它(基本思想是這些結構的大小(遠)大於一個塊,因此我們需要檢查許多附近塊中的結構起源,以便生成當前的這一點正確)。
請注意,這僅影響主世界。下界是安全的,因為它的所有結構產生始終使用安全的 RNG。在The End 中加載區塊會受到末地城市的影響,但僅限於它們的初始生成,而不是每次加載時,因此The End 相對安全,因為您基地的每個區塊在您第一次加載時只會影響RNG 一次它。然而,這並不完全萬無一失,因為玩家通常每次前往基地時都會產生新的區塊,並且偶爾會在已經到達基地時產生新的區塊。
問題在於它修改了全域World.rand
的種子。這只是懶惰的編碼。他們所做的就是呼叫nextInt
四次來選擇 X 和 Z 座標。他們可以用 Random random Random random = new Random(the same stuff)
Random random = this.world.setRandomSeed(...
(換句話說,在這裡創建一個新的Random
而不是弄亂其他所有東西使用的現有隨機數?
至關重要的是,呼叫setRandomSeed
是為了檢查Woodland Mansion 應該去哪裡。無論如何,它會在每個區塊負載上、任何地方發生。你不必站在林地大廈或類似的地方/附近。
事實證明, World.rand
在數百個地方使用,並且透過正常玩遊戲可以輕鬆觀察到其中許多地方。例如,當您開採一個區塊時:
/**
* Spawns the given ItemStack as an EntityItem into the World at the given position
*/
public static void spawnAsEntity ( World world , BlockPos pos , ItemStack stack ) {
double xWithinBlock = world . rand . nextFloat () * 0.5F + 0.25D ;
double yWithinBlock = world . rand . nextFloat () * 0.5F + 0.25D ;
double zWithinBlock = world . rand . nextFloat () * 0.5F + 0.25D ;
EntityItem entityitem = new EntityItem ( world , pos . getX () + xWithinBlock , pos . getY () + yWithinBlock , pos . getZ () + zWithinBlock , stack );
world . spawnEntity ( entityitem );
}
同樣,稍作修改,但功能上對於我們正在討論的內容來說是準確的。
這裡的想法是,在《我的世界》中,當你開採一個方塊時,它會掉落一個物品。該項目被丟棄在區塊內的隨機位置。例如,如果該區塊位於(10, 20, 30)
,則該項目將出現在(10.25, 20.25, 30.25)
和(10.75, 20.75, 30.75)
之間的某個位置。
透過連續呼叫world.rand.nextFloat()
三次(針對 X、Y 和 Z)來選擇該項目的確切位置。
這就是 Minecraft 所需的全部程式碼!
現在,我說我們可以用這些nextFloat
呼叫做一些事情。首先,讓我們看看是否可以「向後工作」來看看nextFloat
呼叫的是什麼。這很幸運,但我們實際上可以。注意上面程式碼中:隨機浮點乘以0.5,然後加上0.25。這個想法是從 0 到 1 之間的隨機數字移動到 0.25 到 0.75 之間的隨機數。您可能會擔心,因為如果您將一個整數除以二,您會失去一些訊息,因為結果是向下捨去的。值得慶幸的是,將浮點數乘以 0.5 是完全可逆的,因為它只是減少指數,而尾數不變。然後,將浮點型轉換為雙精度型,這樣精度更高。新增0.25,然後新增區塊座標。然後,它透過網路完全精確地發送到客戶端。結果:整個過程是可逆的,因此我們可以獲得World.rand.nextFloat()
產生的確切的三個浮點數。
java.util.Random
如何產生浮點數?其實這很簡單。它產生 0 到 2^24 之間的整數,然後將其除以 2^24(得到 0 到 1 之間的數字)。它如何獲得該隨機整數?也很簡單!它是一個線性同餘產生器(LCG)。這表示下一個種子是前一個種子乘以某個值,加上其他值,再對其他值取模。
public float nextFloat () {
this . seed = ( this . seed * multiplier + addend ) % modulus ; // update the seed
int randomInteger = ( int ) ( this . seed >> 24 ); // take the top 24 bits of the seed
return randomInteger / (( float ) ( 1 << 24 )); // divide it by 2^24 to get a number between 0 and 1
}
在本例中,乘數為 25214903917,加數為 11,模數為 2^48。
利用由此產生的浮點數,我們可以將其乘以 2^24 以返回 randomInteger,從而獲得 48 位元種子的「上半部」(最高有效的 24 位元)。
簡而言之,從我們的測量中,我們了解到種子位於measuredRandomInteger * 2^24
和(measuredRandomInteger + 1) * 2^24
之間。
我們可以對 X、Y 和 Z 連續執行三次。
我們知道,在 X 和 Y 之間,以及 Y 和 Z 之間,種子是根據newSeed = (oldSeed * 25214903917 + 11) mod 2^48
更新的
我必須提到一個有效的選項是 for 循環,它嘗試所有 2^24 可能的較低位。對於閱讀本文的程式設計師來說,我希望這能清楚地說明問題所在:
for ( long seed = firstMeasurement << 24 ; seed < ( firstMeasurement + 1 ) << 24 ; seed ++) {
// all these seeds will match the first measurement
if ( nextSeed ( seed ) >> 24 == secondMeasurement && nextSeed ( nextSeed ( seed )) >> 24 == thirdMeasurement ) {
// if nextSeed(seed) matches secondMeasurement, and nextSeed(nextSeed(seed)) matches thirdMeasurement
// then we found a seed that matches all three measurements! yay!
return seed ;
}
}
這會起作用,而且確實起作用,但它不是那麼快,也不是那麼有趣。所以我們用格子代替!
然而,我覺得我必須走得有點亂。格子縮減部分確實在這裡出現,但它真的很複雜,我敢打賭它的讀者保留率會很低,我不想失去你。因此,我將只為您提供 for 循環解決方案(該解決方案確實有效),然後繼續進行漏洞的下一步。晶格約簡方法的解釋將在後面介紹:)
一旦我們有了這顆種子,我們該如何處理它?
首先,請注意我們可以將 LCG 向後移動。顯然,加11是可逆的,但是乘以那麼大的數字是可逆的嗎?我們的乘數25214903917
是一個奇數,這意味著它不能被二整除,因此它不與我們的模數 2^48 共享任何因數(因為 2^48 實際上只是一堆二)。由於它與模數互質,我們可以將其求逆,即找到另一個滿足x * 25214903917 - 1
能被 2^48 整除的數字x
。或者換句話說, 25214903917 * x mod 2^48 = 1
。該號碼是246154705703781
。這有助於反轉乘法,因為如果我們有secret * 25214903917
並且我們想要計算出secret
,我們可以只計算secret * 25214903917 * 246154705703781 mod 2^48 = secret * 1 mod 2^48 = secret
。
好的,我們可以向前和向後步進java.util.Random
的內部種子。向前是newSeed = (oldSeed * 25214903917 + 11) mod 2^48
48 ,向後是oldSeed = ((newSeed - 11) * 246154705703781) mod 2^48
。這是有效的,因為這些數字25214903917
和246154705703781
當相乘時,當你取模 2^48 時結果為1
。
現在,當我們向後退一步時,我們希望在每一步中檢查該種子是否意味著最近在世界某個地方執行了 Woodland Mansion 檢查(漏洞利用的全部要點)。我們該怎麼做呢?
Minecraft 世界範圍從 -3000 萬到 +3000 萬塊。每個「林地區域」(世界上隨機放置單一林地大廈的區域,根據前面顯示的代碼)都是 80 x 80 塊,即 1280 x 1280 塊。這是23437.5 個林地區域,但對於我們所有的代碼,我們只是四捨五入到23440,因為它是一個整數,即使你的玩家不能移動超過3000 萬,你只要站在它附近就可以加載超出它的塊,我們只是不想擔心這一切。
因此,X 軸和 Z 軸上的值都是 -23440 到 +23440。這是(23440*2+1)^2
(又名2197828161
)個可能的林地區域,每個區域都會生成一個獨特的「豪宅種子」(定義為表明有人剛剛在某個林地區域加載了一塊的種子)。我們需要能夠檢查某物是否是豪宅種子。我們可以迭代所有 22 億個豪宅種子來檢查每一個嗎?會太慢。可以製作一個包含 22 億個條目的HashSet<Long>
嗎?即使使用像我們在 nocom 中的編年史地圖,也會佔用太多 RAM,甚至在使用abseil-cpp
的 C++ 中,它也使用了 50GB 的 RAM。更不用說另一部分了:我們實際上想了解他們在世界上的哪個位置(這就是重點)。因此,僅了解這是豪宅種子還不夠,我們還想(有效地)了解哪個林地區域導致了它。
回想一下從林地區域到豪宅種子的函數(注意:為了簡單起見,我現在組合了上面程式碼以來的一些常數,該方程式現在專門用於 2b2t 的種子):
seed = x * 341873128712 + z * 132897987541 - 4172144997891902323 mod 2^48
( -4172144997891902323
數字來自-4172144997902289642 + 10387319
,這是2b2t 世界種子+ 用於播種林地區域的魔法值(如前等式所示)。對於任何其他世界,您只需將自己的種子放入此種子可.)
我們對 x 座標無能為力,因為它要乘以偶數。但是 z 座標上的係數是多少?看起來是奇數!讓我們使用與之前相同的技巧再次反轉它,我們得到211541297333629
。
假設我們有一個給定的種子。如果我們可以迭代從 -23440 到 +23440 的所有可能的 X 座標,並且對於每個座標,計算林地區域的 Z 座標(如果它有這個豪宅種子),會怎麼樣。換句話說,如果我們知道x
和z
,上面的方程式給我們seed
,但是如果我們知道seed
和x
,我們可以創建一個方程式給我們z
嗎?答:是的。我們只是重新排列上面的方程,並利用 Z 的係數是可逆 mod 2^48 的事實,因為它是奇數。
方程式為:
z = (seed + 4172144997891902323 - x * 341873128712) * 211541297333629 mod 2^48
因此,這是一個非常好的解決方案,因為我們可以為X 使用一個僅執行46,881 次迭代的for 循環,而不是使用總共22 億次迭代的兩個巢狀循環(一個用於X,一個用於Z)。這是 Java 版本:
private static WoodlandRegionCoord woodlandValid ( long internalSeed ) {
long seed = 25214903917 ^ internalSeed ; // java.util.Random XORs in the multiplier while doing setSeed, so XOR that back out to go from a "this.seed" to what the input to setSeed() would be
for ( int x = - 23440 ; x <= 23440 ; x ++) {
long z = (( seed + 4172144997891902323L - x * 341873128712L ) * 211541297333629L ) << 16 >> 16 ;
if ( z >= - 23440 && z <= 23440 ) {
return new WoodlandRegionCoord ( x , z );
}
}
return null ;
}
(注意:奇怪的<< 16 >> 16
正在執行mod 2^48,但我們實際上希望使用有符號類型來執行此操作,以便當z 在-23440 和0 之間時我們仍然得到正確的答案,這是一種方法將 48 位數字符號擴展為 64 位,用正確的符號位填充高 16 位作為二進位補碼)
所以這確實有效,而且速度相當快......對於單一種子來說。但請記住,我們可能會將 RNG 後退數千步,並在每一步執行此檢查,直到找到匹配項。當時,我們在最低層使用了糟糕的 DigitalOcean Droplet,這實際上使一切都滯後並且無法跟上實時(機器人每秒挖掘許多塊,每個塊需要數千個 rng 步驟才能破解,每個rng 步驟需要23440*2+1 次操作來檢查,將它們相乘,您就可以達到每秒數億次操作,所以您明白為什麼在蹩腳的VPS 上會遇到麻煩,尤其是當該VPS 也在嘗試時運行Minecraft 的多個無頭實例)。
無論如何,我們改用查找表方法,並在 Cuda 中重寫它,以便每隔幾分鐘在我的桌面上作為批次作業運行一次。它實際上每秒可以處理數百萬個任務,因為數千個 cuda 核心中的每一個都可以並行地處理自己的種子。想法是這樣的:查找表的鍵是豪宅種子的低 32 位,值是林地區域的 X 座標。該查找表不會發生衝突,因為不知何故,每個豪宅種子都有唯一的低 32 位元。我不明白為什麼這是真的,這很有趣。你可能會認為這行不通。但我認為係數341873128712
和132897987541
可能是專門選擇的以使這項工作有效?例如,如果你有 22 億個彈珠和 43 億個桶,並且你獨立地將每個彈珠放入一個隨機桶中,那麼每個彈珠獲得自己的桶的幾率是多少?基本上為零。接近尾聲時,每個新彈珠都有超過 50% 的機會擊中已裝滿的水桶。然而,顯然,這些並不是獨立隨機的,所以它以某種方式起作用。諷刺的是,如果您正在閱讀本文並了解其工作原理或為什麼這兩個特定係數能夠實現此目的,請告訴我。無論如何,它有效。查找表有 2^32 個條目,每個條目為 2 個位元組(因為它只是 -23440 到 +23440 之間的數字),因此 GPU 上需要大約 9 GB 的 VRAM。
林地檢查函數現在看起來像(同樣,這是實際的程式碼,但經過簡化,所有幫助器和常量內聯等):
__global__ void computeSteps ( const int16_t * mansionTable, const int64_t * seedsArr, Result* resultArr, size_t numData) {
auto tid = blockIdx. x * blockDim. x + threadIdx. x ;
[[unlikely]] if (tid >= numData) {
return ;
}
auto seed = seedsArr[tid];
int steps = 0 ;
while ( true ) {
auto externalSeed = seed ^ 25214903917 ;
const auto x = mansionTable[externalSeed & (( 1LL << 32 ) - 1 )];
const auto z = ((externalSeed + 4172144997891902323LL - ( int64_t ) x * 341873128712LL ) * 211541297333629LL ) << 16 >> 16 ;
if (z >= - 23440 & z <= 23440 ) {
resultArr[tid] = {. startSeed = seedsArr[tid], . x = ( int16_t ) x, . z = ( int16_t ) z, . steps = steps};
return ;
}
seed = ((seed - 0xBLL ) * 0xdfe05bcb1365LL ) & (( 1LL << 48 ) - 1 ); // prevSeed(seed)
steps++;
}
}
// and that mansionTable was generated like this
// note: mansionTable must be calloc'd before this function because not every entry will be written to, and an x value outside -23440 to 23440 bounds could create a false positive later on while using the table
__global__ void writeToSeedTable ( int16_t * mansionTable) {
auto tid = blockIdx. x * blockDim. x + threadIdx. x ;
if (tid >= ( 23440 * 2 + 1 ) * ( 23440 * 2 + 1 )) return ;
auto x = tid / ( 23440 * 2 + 1 ) - 23440 ;
auto z = tid % ( 23440 * 2 + 1 ) - 23440 ;
auto seed = (( int64_t ) x * 341873128712LL + ( int64_t ) z * 132897987541LL - 4172144997891902323LL ) & (( 1LL << 48 ) - 1 );
mansionTable[seed & (( 1LL << 32 ) - 1 )] = ( int16_t ) x;
}
這在大批量中效果很好,並且可以在3090 上每秒破解一千萬個種子。到這一點它比這更快。 (原因是我們根本無法事先知道哪些種子會採取更多/更少的步驟)。
嗯,就是這樣。給定種子,這就是我們獲得世界上最近發生區塊加載的林地區域的方式。換句話說,我們剛剛了解到,最近一次有人在 2b2t 上行走並加載世界的新區域,是在我們剛剛確定的這個1280 x 1280 塊林地區域內的某個地方。 (這足夠精確,只需幾分鐘的搜尋即可找到它們)
在實務中,需要多少 RNG 步驟?在低端,可靠的測量從 4 個 RNG 步驟開始,低於該步驟的所有內容都是測量誤差/隨機噪聲,我們知道這一點,因為 Woodland Mansion 代碼在我們可以測量它之前立即調用rand.nextInt
四次。平均而言,每個 Woodland 種子之間大約有 128,000 步,但實際上,在 2b2t 上的絕大多數時間裡,我們在幾十步之內發現了一個 Woodland 種子。這是由於《我的世界》滴答聲中發生的事情的順序的細節所致。從技術上講,我們的測量發生在時脈週期的一開始,因為那是處理用於破壞區塊的資料包的地方。一般來說,在上一個tick期間,一個區塊已經被載入到最近的歷史記錄中。然而,有時,一個事件可能會導致中間出現一堆 RNG 步驟。我們認為這個事件是爆炸,例如有人用拳頭將末影水晶炸毀,或者可能是枯萎的頭骨。玩家打孔資料包處理過程中也會發生末地水晶爆炸,RNG 步數也為 1354 步(計算長方體中的方塊傷害為 1352 步)
在此圖中用作工作範例的座標上,上面的程式碼輸出如下:
jshell> crackItemDropCoordinate(0.730696, 0.294929355, 0.634865435)
Item drop appeared at 0.730696 0.294929355 0.634865435
RNG measurements are therefore 16129481 1507579 12913941
This indicates the java.util.Random internal seed must have been 270607788940196
Found a woodland match at woodland region 123 456 which would have set the seed to 261215197308064
Located someone between 157312,583552 and 158591,584831
jshell>
請注意它如何定位 Woodland Region 123,456
,並注意最終的「位於之間的某人」如何包含我們最初輸入的真實座標,即 x=157440 z=583680。此外,RNG 測量結果與紅色的十六進位配對: 0xf61dc9
等於16129481
、 0x1700fb
等於1507579
、 0xc50d15
等於12913941
。對於種子, 0xed92e70ba4a0
等於261215197308064
, 0xf61dc9221ba4
等於270607788940196
。
您可能可以找到用於停用 RNG 操作的修補程式或設定選項,類似的東西可以用於修補 Randar,這可能是最簡單的方法。如果您找不到停用 RNG 操作的簡單方法,這裡是需要在World
類別中調整的程式碼:
易受攻擊的版本:
public Random setRandomSeed ( int seedX , int seedY , int seedZ ) {
this . rand . setSeed ( seedX * 341873128712L + seedY * 132897987541L + seedZ + this . getWorldInfo (). getSeed ());
return this . rand ;
}
如果您想要完美的保護,只需更改此函數即可每次返回不同的隨機數:
補丁版本:
public Random setRandomSeed ( int seedX , int seedY , int seedZ ) {
return new Random ( seedX * 341873128712L + seedY * 132897987541L + seedZ + this . getWorldInfo (). getSeed ());
}
這可能不會有很好的性能,所以如果您願意,您可以引入一個新字段, separateRandOnlyForWorldGen
,它不與其他任何內容共享,例如:
private final Random separateRandOnlyForWorldGen = new Random (); // new field on the World class
public Random setRandomSeed ( int seedX , int seedY , int seedZ ) {
this . separateRandOnlyForWorldGen . setSeed ( seedX * 341873128712L + seedY * 132897987541L + seedZ + this . getWorldInfo (). getSeed ());
return this . separateRandOnlyForWorldGen ;
}
如果您使用 PaperMC 1.12.2 ,則可以套用下列補丁。備用連結。
這將是一個附加部分,我將在其中介紹一些從我的角度來看更有意義的額外內容,因為除了基本想法之外,我們大多是獨立開發的東西。
我想提到的第一件事是從種子定位座標的系統。 Mason 使用大型查找表和 GPU 處理,而我則依靠快取來提高速度。每當發生命中時,其座標以及半徑內的所有座標都會放入 HashMap 中。種子分兩次處理。第一遍將 RNG 後退,並檢查種子是否存在於快取中,或者是否與上次處理的種子相同,這是不同的考慮方式。第二遍僅在第一遍失敗時發生,速度慢得多,並且使用前面描述的相對昂貴的演算法。該系統的一個令人愉快的副作用是,第一次通過有可能「跳過」原本「有效」的位置,但不太可能是正確的位置,這有助於誤報。
這是該代碼:
public class RandarCoordFinder
{
public static final long X_MULT = 341873128712L ;
public static final long Z_MULT = 132897987541L ;
public static final long Z_MULT_INV = 211541297333629L ;
public static final int MANSION_SALT = 10387319 ;
public static final int MANSION_SPACING = 80 ;
public static final int CITY_SALT = 10387313 ;
public static final int CITY_SPACING = 20 ;
// the last seed we processed
public long lastSeed = - 1 ;
// a mapping of seed -> x,z that is updated everytime we get a hit
public final HashMap < Long , Long > hitCache = new HashMap <>();
// set this according to the server's seed
public long worldSeed ;
// change these if you need to use different structures
public int salt