逆向混淆簽名:90% 都是標準演算法,真正致命的是那 10% 的偏差點

最近更新: 2026-07-30 10:29:28

拿到一個混淆過的簽名 SDK,第一眼往往很容易被唬住:每一行都像在施展看不懂的祕術。

但完整走過一次逆向流程後,通常會發現事實恰好相反:90% 的程式碼都是標準演算法。它用的雜湊是公開雜湊、編碼是公開編碼、串流加密也是公開的串流加密。這些部分根本不必從頭理解;辨認出演算法後,直接從 RFC 或參考實作搬過來,就能做到一個位元組都不差。

真正致命的是剩下的 10%:標準演算法被悄悄動過一點手腳的地方。本來固定的輪數被改成變數、基礎常數被換成相鄰值、某一輪的 word 索引被對調,或是金鑰被藏進密文裡。每個改動都很小,但只要漏掉一個,重寫結果就不會和目標一致;而其餘 90% 明明都正確,反而讓你不知道錯在哪裡。

所以,混淆簽名逆向真正要做的,不是讀懂那 90%,而是定位這 10% 的偏差點。這篇要談的,就是如何讓大型模型幫你搜尋這些偏差,以及為什麼這正是模型最難被取代的一環。

(前提步驟──從常數與結構辨認「這是 ChaCha」、「這是 FNV」這類演算法家族──已在上一篇文章說明。以下假設你已經辨認出整體骨架,直接進入偏差搜尋。)

為什麼偏差點最容易被人眼忽略

人類大腦在辨識模式時有一個強烈傾向:一旦抓到大意,就不再細看

你看到一段程式碼,常數符合 ChaCha 的特徵、結構也像,腦中立刻打勾:「這是 ChaCha20。」然後就往下看。你不會逐一檢查每個輪數、每個索引、每個常數的最低位元,因為「已經辨認出來了」的滿足感,會直接關掉注意力。

混淆作者靠的正是這點。他們不會重寫整套演算法──成本太高,也太容易出錯──而是在標準演算法上做最小幅度的修改:翻一個數字、換一個索引、多加一個步驟。這些變動小到足以躲過模式辨識,卻足以讓你照標準重寫的版本徹底失敗。

這是一場注意力極不對稱的戰爭。作者只要藏好一個偏差;你卻必須找出全部。而人類天生不擅長對「看起來正確的東西」持續保持懷疑。

模型在這裡有一個反直覺優勢:它沒有「我已經認出來了」而鬆懈的滿足感。只要 prompt 明確要求它搜尋偏差,它就能逐項檢查、不會做到一半放鬆──前提是你得用對 prompt,這是第 5 節的主題。先來看這四類偏差通常長什麼樣子。

最常見的四種偏差手法

以下每一類都會先用公開標準演算法說明「標準應該長什麼樣子」,再看「偏差會怎麼出現」。實務上,同一個 signer 裡往往會同時疊加這四類手法。

1. 固定常數變成變數:本該寫死的參數改由執行期決定

標準串流加密的輪數是固定的。ChaCha20 固定跑 20 輪,沒有彈性;所有標準實作都把這個數字直接寫死。RFC 8439在前言也明確指出,文件只描述 20 輪的 ChaCha,8 輪與 12 輪變體則定義於其他文件。輪數原本就是標準裡白紙黑字的常數。

偏差手法是:把固定輪數改成根據金鑰動態計算。quarter-round 邏輯不變,但實際執行幾輪取決於金鑰中的某些位元組;換一把金鑰,輪數也會跟著改變。

人為何會漏掉:你認出 quarter-round 結構,也認出 σ 常數,於是大腦判定「ChaCha20」,直接照 20 輪重寫。你甚至不會去看控制迴圈次數的變數究竟是常數還是運算式,因為你見過的每個 ChaCha,它都是常數。

這類偏差的線索是:你預期應該出現常數的位置,卻看到依賴輸入的運算式。經過良好引導的反證檢查,會專門確認「標準實作在這裡寫死一個值,但這份程式碼是否改成計算得出」。

差分測試的確認方式:刻意使用兩把只差一個位元組的金鑰執行。如果輸出差異遠大於一個位元組理應造成的影響,代表該位元組影響的不是單純 XOR 進 keystream,而是某個全域參數,例如輪數。

2. 常數微調:把基礎常數替換成非常接近的值

FNV-1a 雜湊有兩個公開的 magic number:offset basis 與 prime。每個正確的 FNV-1a 實作都使用這兩個確切數值,標準也有公開記載。共同作者 Landon Curt Noll 維護的FNV reference page明載,32 位元的 offset basis 是 2166136261,prime 是 16777619,一個數字都不能錯。

偏差手法是:將其中一個基礎常數換成只和標準相差極小的鄰近值。雜湊整體結構完全一樣──XOR、乘法、迴圈都沒問題──只有初始基數被做了幾乎看不出的改動。

人為何會漏掉:這是四類中最棘手的一種。你看到 FNV 的結構,再看到一個看似 offset basis 的大常數,就判定「標準 FNV-1a」。已經「認出來」的 magic number,誰會再逐 bit 比對它是否真的符合標準?

這類偏差幾乎只能靠差分測試抓到,因為人眼對大數字極不可靠。做法是:拿已知輸入,同時送進標準實作目標黑盒比對。如果結構相同但輸出不同,問題幾乎肯定出在基礎常數。接著把可疑常數交給模型,讓它和標準值做 diff──機器在這件事上遠比人可靠。

模型的價值非常具體:它記得標準 FNV-1a offset basis 的每一個 bit,但你未必記得。你只要問:「這個常數是否完全等於標準 FNV-1a offset basis?」它立刻就能指出差異。

3. 結構局部改寫:標準演算法中的某一步被動了手腳

每個 ChaCha double round 由 8 個 quarter round 組成:前 4 個處理欄,後 4 個處理對角線。每個 quarter round 要操作哪些 word,其索引組合是標準固定規定的。RFC 8439 §2.3詳細列出 block function,以及每一輪會碰觸哪些 state word。

偏差手法是:在其中某一輪,悄悄交換一兩個 word 索引。絕大部分輪次仍遵循標準,只有中間某處把本應操作的 word 換成另一個。它看起來依然是 ChaCha,也能正常執行,但產生的 keystream 已經和標準 ChaCha 完全不同。

人為何會漏掉:quarter-round 索引是一長串數字,總共八組,例如 (0,4,8,12)(1,5,9,13)…。眼睛掃過去只會確認「嗯,有 column round 和 diagonal round」,不會逐組核對四個數字是否全都放在標準位置。把改動藏在這串本來就讓人眼花的索引裡,效果最好。

模型也不可能一眼看穿這種情況;你必須明確要求它逐輪列出索引,並與標準 ChaCha 進行 diff。這是純機械式檢查,正是模型不易飄掉、人卻容易失焦的工作。讓它輸出一張「標準索引 vs. 實際索引」表格,偏差自然就會浮現。

差分測試可這樣確認:若前兩類都已排除──輪數正確、常數也正確──輸出仍不一致,問題就在結構。逐輪 dump 中間 state;第一個開始偏離標準 ChaCha 的輪次,就是被修改的那一輪。

4. 資料嵌入:把金鑰材料藏在輸出裡

前三類改的是演算法本身;這一類改的是資料的組織方式

常見作法之一,是加密金鑰不走獨立通道傳輸,而是拆開後插入密文本體,接收端再依同一規則取回。更隱密的變體會讓插入位置不是固定值,而是由資料內容計算而來──密文一變,金鑰藏的位置也跟著變。最外層再包上自訂字母表的 Base64 與 marker prefix byte。

人為何會漏掉:你忙著和加密演算法搏鬥,卻沒發現金鑰根本不必破解──它就在你手上的密文裡,只是不知道藏在哪一段。初學者很常卡在這裡,試圖「破解」其實已經攤在眼前的內容。

這類偏差的線索是:資料區塊中有一段統計特徵和周圍不同。金鑰通常是高熵隨機位元組,插在密文中間時會形成可辨識的「外來區段」。模型可以協助分析「輸出的哪個區間,位元組分布和其餘部分不同」,藉此找出嵌入材料的邊界。

確認方式:如果你找得到插入規則,例如某個位元組總和的模數運算,就拿幾組已知輸入/輸出反推出插入位置公式,再正向驗證。模型可以從少量樣本協助歸納規則,但最終是否正確,仍要由 assert 判定。

反證檢查,才是推理層模型真正的戰場

看完這四類偏差,應該能發現一個共通點:辨認演算法家族,也就是提出候選答案,並不難;找出偏差,也就是尋找反證,才是難處。

候選答案這一步,幾乎任何模型都做得到。ChaCha 的 σ 常數、FNV 的結構,凡是見過訓練資料的模型都能辨認。真正拉開差距的是反證檢查:模型是否願意且有能力,持續挑剔自己剛剛辨認出的演算法,指出「但這一部分和標準不符」。

較弱的模型常在這裡失去力道。它辨識出「這是 ChaCha20」後,反證段落往往退化成換句話說候選結論:「此實作遵循標準 ChaCha20 結構,採用了經典 quarter round……」整段只是在重述候選答案,沒有真正檢查任何偏差。這種反證檢查無法指引你的下一步。

強推理模型的反證檢查則完全不同。它會寫:「候選演算法為 ChaCha20,但此實作有三處偏離標準:第一,輪數由依賴金鑰的運算式控制,標準 ChaCha20 固定為 20 輪;第二,第 N 輪的 word 索引與標準 diagonal round 不符;第三……」每一點都指向具體且可驗證的偏差。這種反證檢查本身就是你在 Stage 3 要寫的差分測試清單。

這就是為什麼演算法家族辨識值得交給推理層模型,也值得在正式投入前自行測試。不同模型的反證品質,會直接決定你要寫多少無效測試、繞多少冤枉路。

我會把這套流程分成四層:

階段

需要的能力

選擇

model id

拆分後的結構映射

長上下文,可一次讀完整個模組

Kimi K3

kimi-k3

演算法家族辨識與反證

強推理能力,敢於反駁自己

Claude Opus 5

claude-opus-5

大量符號重新命名

價格低、併發高

Claude Sonnet 5

claude-sonnet-5

差異歸因

中等推理能力,能針對特定位元組說明

GPT-5.6 Sol

gpt-5.6-sol

第二層是本文的核心。不要只聽我說該選哪個模型,自己測一次。流程很簡單:

  1. 從自己的混淆 bundle 挑 2–3 個 leaf function,其中至少一個你已知答案,作為對照組。

  2. 使用上一篇文章的「候選/證據/反證」三段式 prompt,分別將相同輸入交給 claude-opus-5gpt-5.6-sol

  3. 只看反證檢查那一段:它真的有逐項檢查偏差,還是只用不同說法重述候選答案?對照組裡,兩個模型各自抓到多少偏差?

  4. 抓到的偏差數量與品質,就是你的選型依據。

跑一次就能看出差異,比任何 benchmark 排行榜都更直接。

真正的阻力,是切換模型的成本

三家供應商、四個模型。最直覺的做法,是在不同階段串接三套 SDK、三種認證機制、三組錯誤處理;大多數人算一算覺得不值得,最後就用同一個模型跑完整個流程。結果是在演算法家族辨識時使用反證能力較弱的層級,繞了一大圈還不知道原因。

AIReiter把這一層複雜度壓平:一把 key、一套 OpenAI-compatible 介面,四個模型都在後面;切換模型只要改 request body 裡的 model 欄位。

# 演算法家族辨識 + 反證:使用推理層
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<three-part prompt + leaf function + constants>"}]
  }'

# 大量符號重新命名:只需改一個欄位
#   "model": "claude-sonnet-5"
# 差異歸因:
#   "model": "gpt-5.6-sol"

已經在使用 OpenAI SDK?把 base_url 指向 https://aireiter.com/api/v1,其他都不用改。使用 Anthropic SDK?以同一把 key 呼叫 POST /api/v1/messages 即可。

價格方面,Claude 模型提供定價 7 折,GPT 模型則是半價。這套流程最吃得到折扣的,正是成本最集中的環節:演算法家族辨識階段需要反覆迭代 prompt,對同一個 function 跑許多輪,也是整個流程呼叫最密集的部分;大量符號重新命名則從數百次呼叫開始。這兩項佔了主要成本。

結語

混淆簽名逆向的真相是:絕大部分程式碼都是可原樣照抄的標準演算法;真正的工作,是找出標準演算法被悄悄改動的地方。

固定常數變成變數、常數微調、結構局部改寫、資料嵌入,這四類偏差都有同樣特性:小到足以被人類的模式辨識忽略,卻大到讓整個重寫版本徹底失敗。人類不擅長持續懷疑看起來正確的東西,而這正是模型在良好 prompt 引導下的強項。

不過,模型只能提出懷疑,不能完成確認。每一個偏差點假設,最後都必須轉成差分測試──這正是差分測試文章的主題。模型交給你一份「可能被改過的位置」清單;真正判定哪些確實改過的,是 assert