1. 摘要
報告深入探討現代惡意程式所使用的尖端技術,特別是利用 PDFly 和 PDFClick 等客製化 PyInstaller 執行檔來規避偵測與分析的手法。文中強調了從標準 PyInstaller stubs 演進到包含自訂 magic bytes 和多層加密方案的高度修改版本。分析重點在於識別這些修改的技術細節、針對涉及 XOR 運算、zlib 解壓縮和資料反轉的解密過程進行逆向工程,以及開發通用擷取框架。此外,文中也討論了更廣泛的安全性影響,以及針對此類進階混淆方法的潛在緩解策略,並與 Python 架構的後端系統中真實存在的漏洞作比較。
2. 簡介
PyInstaller 是一個被廣泛使用的工具,用於將 Python 應用程式封裝成獨立的執行檔,簡化在各個作業系統上的部署。然而,其合法用途已被 Threat actor 利用於散佈惡意程式,利用該工具將所有必要的相依項目打包成單一檔案的能力。這種做法使傳統的惡意程式分析變得複雜,因為 Python 中介碼(Bytecode)通常在執行檔中被混淆或加密。PDFly 和 PDFClick 範例說明了這種趨勢,由於其客製化的 PyInstaller stubs 和專屬的解密程序,帶來了重大挑戰 [1] 。這份報告目的為提供這些客製化 PyInstaller 執行檔的全面技術分析,詳細說明其擷取與解密的方法。我們將探討對標準 PyInstaller 格式所做的修改、多階段解密演算法,以及對抗這些混淆技術的通用工具開發。來自相關網路安全研究的見解,包括對 Python 架構的惡意程式分析流程的分析以及 Python 驅動系統中的漏洞,將被整合進來以提供對威脅情勢的整體理解 [2][3] 。
3. PyInstaller Stub 修改與 Magic 發現
分析客製化 PyInstaller 執行檔的初步障礙在於它們偏離了標準 PyInstaller 格式。專為傳統 PyInstaller stubs 設計的工具(如
pyinstxtractor-ng
),往往因為 magic bytes 或「cookies」被更改而無法識別這些修改過的樣本。原始文章指出,未找到標準的 PyInstaller magic,即使將其替換後,對 'PYZ\0' 的驗證仍然失敗
[1]
。這表明惡意程式作者刻意努力阻礙自動化分析。識別自訂 magic 的過程涉及使用 IDA Pro 等工具進行手動檢查,其中可以觀察到包含部分垃圾內容和不同 cookie(例如
local_80
)的字串
[1]
。
為了克服這個問題,一種通用的方法涉及動態定位 PyInstaller magic。提供的
find_pyinstaller_magic
函式說明了這一點,它在執行檔的 overlay 中搜尋有效的 cookie 結構。此函式找到所有潛在的位移(offsets),嘗試解析關鍵的 PyInstaller metadata,例如封裝長度(
pkg_len
)、目錄(table-of-contents)位移(
toc_offset
)、目錄長度(
toc_len
)以及 Python 版本(
pyver
)。藉由在合理範圍內驗證這些數值,該函式可以識別出代表客製化 PyInstaller stub 的正確 8 位元組 magic
[1]
。
- def find_pyinstaller_magic(filename):
- print("[+] Testing cookie locations, this may take a bit ...")
- try:
- pe = pefile.PE(filename)
- overlay_offset = pe.get_overlay_data_start_offset()
- if overlay_offset is None:
- print(f"[ERROR] No PE overlay found")
- return None
- except:
- print(f"[ERROR] Could not parse as PE file")
- return None
- with open(filename, 'rb') as f:
- data = f.read()
- # Search from the overlay start to the end of file
- for i in range(overlay_offset, len(data) - 32):
- try:
- # Try to parse as a cookie structure
- # 8 bytes magic + 4 bytes pkg_len + 4 bytes toc_offset + 4 bytes toc_len + 4 bytes pyver
- pkg_len = struct.unpack('!I', data[i+8:i+12])[0]
- toc_offset = struct.unpack('!I', data[i+12:i+16])[0]
- toc_len = struct.unpack('!I', data[i+16:i+20])[0]
- pyver = struct.unpack('!I', data[i+20:i+24])[0]
- # Validate the structure
- if (0 < pkg_len < len(data) and
- 0 < toc_offset < pkg_len and
- 0 < toc_len < pkg_len and
- 20 <= pyver <= 400):
- # This looks like a valid cookie, extract the 8-byte magic
- magic = data[i:i+8]
- print(f"[+] Found valid cookie at: 0x{i:x}")
- return magic
- except:
- continue
- print(f"[DEBUG] No valid magic found in overlay")
- return None
下圖說明了客製化 PyInstaller 擷取程序的流程:
4. 多層解密分析
一旦識別出客製化 PyInstaller stub 並執行初步擷取,下一個挑戰就是解密 PYZ 封存檔的內容,這些內容通常是被加密的。原始文章詳細介紹了涉及 XOR 運算、zlib 解壓縮和資料反轉的多層解密方案
[1]
。這個複雜的過程通常隱藏在
pyimod01_archive.pyc
等模組的 Python 中介碼中,而這些模組本身並未加密。
從反組譯的 Python 中介碼逆向工程出的解密演算法遵循特定順序:
-
第一次 XOR 解密:
資料與一個 key(例如
SCbZtkeMKAvyU)進行 XOR。此運算通常逐位元組進行,透過對索引值執行餘數運算(i % len(xor_key1))來重複套用 key [1] 。 - Zlib 解壓縮: 經過 XOR 處理的資料隨後使用 zlib 函式庫進行解壓縮。這表示資料在第一次 XOR 運算後進行了壓縮,可能是為了減少檔案大小並進一步混淆內容 [1] 。
-
第二次 XOR 解密:
對解壓縮後的資料套用第二次 XOR 運算,使用不同的 key(例如
KYFrLmy)。這增加了另一層保護,使得在不知道這兩個 keys 的情況下直接解密更加困難 [1] 。 -
資料反轉:
資料隨後被反轉(
data[::-1])。這種簡單但有效的技術進一步打亂資料,需要額外的步驟來恢復其原始順序 [1] 。 - Unmarshal: 最後,將反轉且解密後的資料進行 unmarshal,將 Python 中介碼轉換回可執行的 Python 物件。
以下 Python 程式碼片段的分析,展示了這個多層解密程序:
- import zlib
- def decrypt_pyinstaller_data(data, xor_key1, xor_key2):
- # First XOR decryption
- # The data is XORed with the first key, cycling through the key if the data is longer.
- data = bytes(b ^ xor_key1[i % len(xor_key1)] for i, b in enumerate(data))
- # Zlib decompression
- # The XORed data is then decompressed using the zlib library.
- data = zlib.decompress(data)
- # Second XOR decryption
- # A second XOR operation is applied with the second key.
- data = bytes(b ^ xor_key2[i % len(xor_key2)] for i, b in enumerate(data))
- # Data reversal
- # The data is reversed to restore its original order.
- data = data[::-1]
- # Unmarshalling (not shown in snippet, but is the next logical step for Python bytecode)
- # import marshal
- # unmarshalled_code = marshal.loads(data)
- return data
擷取這些 XOR keys 需要解析
pyimod01_archive.pyc
的中介碼。Python 的程式碼物件包含
co_consts
等屬性,其中儲存了程式碼中使用的常數。藉由檢查
ZlibArchiveReader
物件中的
extract
方法並尋找
<genexpr>
常數,即可識別出 XOR Keys
[1]
。這個過程強調了理解 Python 中介碼結構對於有效進行惡意程式分析的重要性。
5. 通用擷取框架
考慮到客製化 PyInstaller 執行檔的多樣性,一個通用的擷取框架對於高效分析至關重要。這樣的框架能自動發現自訂 magic bytes 並擷取解密金鑰。先前討論的
find_pyinstaller_magic
函式是此框架的關鍵組件,能夠在不事先知曉其數值的情況下,動態識別 PyInstaller cookie
[1]
。
構建通用擷取器的下一步涉及以程式化方式從
pyimod01_archive.pyc
中介碼中擷取 XOR keys。這可以透過分析 Python 中介碼中的程式碼物件(
co_consts
)來定位用作 XOR keys 的特定常數來實現。這種方法比依賴 hardcoded keys 更具強健性,因為惡意程式作者可以輕易地在不同樣本之間更改這些數值。
nightMARE 框架(一個用於可擴充惡意程式研究與設定擷取的 Python 函式庫)提供了一個如何構建此類自動化分析流程的範例 [3] 。雖然 nightMARE 專注於整合進階逆向工程框架(如 Rizin)和模擬引擎(如 Unicorn)以進行靜態分析與動態分析,但自動化金鑰擷取和解密程序的原則直接適用於構建通用的 PyInstaller 擷取器。藉由利用中介碼分析和模式比對工具,通用框架可以適應不同的客製化 PyInstaller 修改,大幅減少每個新樣本所需的手動工作量。
6. 安全性影響與緩解策略
使用具有多層混淆的客製化 PyInstaller 執行檔對網路安全構成了重大挑戰。傳統的防毒軟體可能難以偵測這些樣本,因為它們的結構已被改變且 Payload 已加密。主要的安全性影響包括:
- 規避靜態分析: 自訂 magic bytes 和加密的封存檔繞過了標準的 PyInstaller 擷取工具,使得靜態分析嵌入的 Python 程式碼變得困難。
- 增加分析複雜度: 多層解密過程需要對 Python 中介碼進行詳細的逆向工程,增加了分析所需的時程與專業知識。
- 隱匿動態行為: 雖然這裡的重點是靜態擷取,但混淆目的在隱藏惡意程式在執行時的動態行為,使得行為分析變得至關重要。
針對此類尖端 PyInstaller 架構旳惡意程式的緩解策略應包含多方面的手段:
- 強化靜態分析工具: 開發並利用能夠動態識別 PyInstaller cookies 並從 Python 中介碼中擷取解密金鑰的工具。
- 行為分析: 採用沙箱(sandboxing)和動態分析技術來觀察惡意程式的執行時行為,無論靜態混淆如何,這都能揭示其真實功能。
- 威脅情資共享: 分享新 PyInstaller 架構的惡意程式變體的侵害指標(IOCs)和分析技術,以提升集體防禦能力。
- 安全編碼實務: 對於開發者而言,確保 Python 應用程式被安全地封裝,且敏感資訊不被嵌入在易於擷取的格式中。
對 SDN 控制器 RCE 漏洞的分析(其中一個 Python-3.10 後端控制器使用 PyInstaller 封裝)突顯了 Python 架構的系統中漏洞的真實影響 [2] 。儘管該案例側重於命令注入和認證繞過,但它強調了在任何利用 Python 執行檔的系統中,特別是處理關鍵基礎設施的系統中,強健安全實務的重要性。
7. 結論
利用客製化 PyInstaller 執行檔的惡意程式演進(如 PDFly 和 PDFClick 所示),展示了惡意程式作者與安全研究人員之間持續的軍備競賽。自訂 magic bytes、多層加密和資料操縱技術的實施,需要進階的逆向工程能力。藉由理解這些混淆方法的細節,特別是 XOR-zlib-XOR-Reverse-Unmarshal 解密鏈以及 PyInstaller cookies 的動態發現,資安專業人員可以開發更有效的工具和策略來進行偵測與分析。開發能夠適應各種 PyInstaller 修改的通用擷取框架,對於自動化分析過程並在演進中的威脅面前保持領先至關重要。持續研究 Python 中介碼分析並整合自動化分析流程,對於對抗未來 Python 架構的惡意程式混淆的演進將至關重要。