反推亂數種子:讓 CPython 吐出指定的偽隨機序列
GitHub – frazerpearce/TimeLord · 2026-09-28
只要把 Mersenne Twister 的 twist 與 temper 運算改寫成二元體 GF(2) 上的線性方程式,就能反推出一個整數種子,讓 CPython 的 random.Random() 剛好吐出事先寫好的輸出——連續一百次正面、甚至一整段自訂文字。這個做法收在 GitHub 專案 TimeLord 裡,作者 frazerpearce 的技術說明於 2026 年 9 月 28 日經 Lobsters 討論串傳開,示範對象是 CPython 的 random 模組所使用的 MT19937 演算法。
背景:機率論證為何靠不住
MT19937 的內部狀態約有兩萬個位元(README 原文為「roughly 20,000 bits」),一般人看到「連續一百次正面、機率只有 2⁻¹⁰⁰」這種說法,直覺會當成偽隨機序列沒被動過手腳的證據。TimeLord 想戳破的正是這個直覺:只要種子是在已經知道想要的結果之後才選出來的,這串「極不可能」的序列就只是可重現的確定性輸出,不能拿機率低當成公正或運氣的證明。這其實是後選偏誤(post-selection bias)在偽隨機數上的具體案例。
CPython 的 random 模組本來就不是密碼學安全的隨機源,這點是公開共識;但 TimeLord 把「不安全」具體化成一個可執行的反推工具,而不只是一句警語。
核心手法:把 twist/temper 拆成 GF(2) 方程式
作法分五步:先把 MT19937 狀態的每個位元符號化,接著讓這些符號位元依序通過 twist 與 temper 這兩個運算——README 強調這兩個運算「是二元體 GF(2) 上的線性運算」,因此可以整段改寫成 XOR 方程式;再把「輸出要等於某個值」的要求轉成一組位元限制式;解這個 GF(2) 上的線性方程組;最後把沒被限制到的位元自由填滿。工具進一步反推 CPython 在 Modules/_randommodule.c 裡的 init_by_array 播種流程,把解出的狀態換算成一個真正可以丟進 random.seed() 的整數種子。
約束的位元數視輸出型態而定:每丟一次人頭只需要兩個位元限制,因為 randrange(2) 取的是頂端位元,要求它恰好是 01;若要輸出 ASCII 文字,因為 128.bit_length() 是 8,randrange(128) 每個字元就需要八個位元限制。專案提供的指令很直接:
python3 find_heads_seed.py 100 # 算出讓連續 100 次人頭成立的種子
python3 demo_heads.py 100 # 用該種子實際跑一次驗證
python3 find_text_seed.py "MESSAGE"
python3 demo_text.py "MESSAGE"其中 timelord_mt.py 是共用的 GF(2) 求解與種子反推邏輯,整包工具只依賴標準函式庫。
實際效果:一千次人頭與 2,490 字元的極限
兩萬個狀態位元對上一千次人頭只吃掉兩千個限制式,README 形容剩下的空間是「enormous freedom」,代表遠不只人頭遊戲能造假。實測上,在 CPython 3.9.6 上驗證了一段重複 2,490 字元的訊息可成功求解,到 2,491 字元就出現不一致;README 特別指出這個上限取決於限制式是否自洽,而不是訊息長度本身有固定天花板。種子輸出刻意存成十六進位整數,是為了避開 Python 3.11 對整數轉字串加上的四千三百位數上限。
對誰有影響:任何拿「亂數序列看起來多不可思議」當作公正性證明的場景都要重新檢視——例如讓使用者自行指定種子的抽獎展示、教學示範,或用 random.Random() 產出「巧合」當賣點的內容,都不能再用機率低當作沒動手腳的依據。反過來說,只要種子本身是由系統端以不可預測的來源產生、且不對外公開,TimeLord 這套反推法就無用武之地;風險只出現在種子可被攻擊者選擇或已知的情境下。