前面進行 Malware Analysis 時,我們常會遇到一個問題:明明拿到的是 PE 執行檔,丟進 IDA Pro 後卻看不到多少正常程式碼,Imports 也少得異常。
這時很可能遇到的就是 Packed Malware。
Packer 原本可以用來壓縮執行檔、減少檔案大小,但 Malware 作者也會利用它隱藏原始程式碼、增加分析難度,甚至搭配 Anti-Debugging、Anti-VM 等技術。
因此,遇到 Packed Malware 時,通常需要先進行 Unpacking(脫殼),才能繼續有效地做 Static Analysis。
【聲明】由於時間問題本日文章由 AI 生成QQ
然後今天是鐵人賽最後一天,推甄資料弄得差不多睡了一整天,祝大家中秋快樂~
Packer 的基本概念是:
將原本的 Executable 經過壓縮、加密或其他轉換,再包進另一個可以執行的 PE 檔案。
可以簡化成:
Original Executable
↓
Packer
↓
Packed Executable
Packed Executable 通常包含兩個重要部分:
真正執行時,不是直接執行原本的 Malware,而是先進入 Unpacking Stub。
正常 PE:
Windows Loader
↓
Original Entry Point
↓
Program Code
Packed PE:
Windows Loader
↓
Unpacking Stub
↓
Unpack Original Code
↓
Resolve Imports
↓
OEP
因此 Packed File 的 Entry Point 通常指向 Unpacking Stub,而不是原始程式。
Unpacking Stub 主要做三件事情:
這也是為什麼直接對 Packed Malware 做 Static Analysis 效果不好:我們看到的主要是 Stub,而不是真正的 Malware Code。
正常 PE 的 Import Table 會告訴 Windows Loader:
需要哪些 DLL?
需要哪些 API?
但如果 Import Table 也被 Pack,就無法直接讓 Loader 使用。
因此常見做法是讓 Stub 保留:
LoadLibrary
GetProcAddress
接著執行時:
LoadLibrary
↓
載入 DLL
↓
GetProcAddress
↓
找 API Address
這也是為什麼分析一個樣本時,如果發現 Imports 少得異常,而且主要只有 LoadLibrary、GetProcAddress,就要懷疑它可能被 Packed。
有些 Packer 甚至會完全移除 Imports,進一步增加分析難度。
當 Stub 完成 Unpacking 後,最後必須把控制權交回原始程式。
這個轉移通常稱為:
Tail Jump
最單純的情況可能是:
Unpacking Stub
↓
JMP OEP
↓
Original Program
但為了增加分析難度,也可能使用:
RET
CALL
NtContinue
ZwContinue
因此 Unpacking 很重要的一個工作就是:
找到 Stub 結束的位置,以及原始程式真正開始執行的 OEP。
分析 Malware 時,可以從幾個現象判斷。
例如只看到:
LoadLibrary
GetProcAddress
甚至完全沒有 Imports。
因為真正的 Code 還是壓縮或加密資料,IDA 無法正常 Disassemble。
例如:
UPX0
UPX1
看到這類 Section Name,可以懷疑使用 UPX。
例如:
SizeOfRawData = 0
VirtualSize != 0
代表 Disk 上沒有正常 Code,但執行後 Memory 中卻需要一塊空間,可能就是準備放 Unpacked Code。
另一個常見判斷方式是:
Entropy(熵)
壓縮或加密後的資料通常更接近 Random Data,因此:
Normal Code
↓
Lower Entropy
Compressed / Encrypted
↓
Higher Entropy
所以如果某個 Section Entropy 異常高,也可能代表:
但要注意,高 Entropy 只能作為 Indicator,不能單獨證明程式一定被 Packed。
大致可以分成:
最簡單的方法就是使用針對特定 Packer 的 Unpacker。
例如 UPX:
upx -d malware.exe
優點是:
但缺點是通常只能處理特定 Packer。
如果 Malware 使用修改版 Packer 或 Custom Packer,就可能失敗。
因此實務上:
能自動脫殼就先自動脫殼,不行再進入 Manual Unpacking。
另一種方法是實際執行 Malware:
Run Malware
↓
Stub 自己 Unpack
↓
Dump Memory
↓
Rebuild Imports
問題在於工具必須判斷:
Stub 到底什麼時候結束?
如果判斷錯誤,就無法正確 Dump。
而且 Dynamic Unpacking 會真的執行 Malware,因此一定要在隔離的 Analysis Environment 中操作。
Manual Unpacking 最實用的想法其實不是自己逆向整個 Compression Algorithm,而是:
讓 Malware 自己把自己解開。
流程可以簡化成:
Packed Malware
↓
Run in Debugger
↓
Stub Unpacks Code
↓
Find OEP
↓
Dump Memory
↓
Repair Import Table
↓
Unpacked Binary
其中最困難的通常是:
Finding the OEP
OEP(Original Entry Point)就是程式被 Packing 前真正的 Entry Point。
沒有任何一種找 OEP 的方法可以處理所有 Packer,因此通常需要交替使用不同技巧。
最直觀的方法就是找 Stub 最後跳回原始程式的 Jump。
典型情況:
unpacking code
unpacking code
unpacking code
JMP 00401000 ← Tail Jump
00
00
00
00
Tail Jump 常有幾個特徵:
例如原本 OEP Memory 可能看起來都是:
00 00 00 00...
但 Stub 執行完成後變成:
CALL ...
TEST EAX,EAX
JNZ ...
PUSH ...
CALL ...
代表原始 Code 已經被寫回 Memory。
很多 Unpacking Stub 開頭會:
PUSHAD
保存 Registers,結束時再:
POPAD
因此可以觀察 Stack。
基本概念:
PUSHAD
↓
Unpacking
↓
Unpacking
↓
POPAD
↓
Tail Jump
↓
OEP
可以在 Stack 中保存 Register 的位置設置 Hardware Breakpoint on Read。
當 Stub 最後執行 POPAD 讀取這些資料時,Breakpoint 被觸發,通常表示已經接近 Tail Jump。
Stub 通常需要 Resolve Imports,因此可能大量呼叫:
GetProcAddress
可以直接在 GetProcAddress 設 Breakpoint。
這樣就能跳過 Stub 前面大量的解壓或解密流程,直接停在 Unpacking 後半段。
接著再尋找:
Import Resolution
↓
Tail Jump
↓
OEP
Windows Program 在 OEP 附近通常會執行一些初始化工作。
例如 Command-line Program 可以觀察:
GetVersion
GetCommandLineA
GUI Program 可以觀察:
GetModuleHandleA
當 Breakpoint Hit 後,再往回查看是哪段 Code 呼叫它,就有機會找到 OEP 附近。
如果原始 .text Section 還存在,可以監控:
什麼時候第一次開始執行
.text裡的 Code?
因為 Stub 執行完後通常會進入原始 .text Section。
因此第一次執行該區域的位置,很可能就是 OEP。
找到 OEP 後,代表原始 Malware Code 已經存在 Memory。
下一步就是:
Dump Process
例如使用 OllyDump:
Find OEP
↓
Dump Debugged Process
但直接把 Memory Dump 出來還不一定能正常執行,因為 PE Header 仍可能保留 Packed File 的資訊。
至少需要處理兩件事:
如果 OllyDump 無法正確修復 Import Table,可以使用:
Import Reconstructor(ImpRec)
基本流程:
找到 OEP
↓
計算 OEP RVA
↓
IAT AutoSearch
↓
Get Imports
↓
Fix Dump
例如:
Image Base = 0x400000
OEP = 0x403904
則:
OEP RVA = 0x3904
如果工具仍無法恢復 Import Table,就可能需要手動辨識 Imports。
程式執行時真正需要的是 API Address,因此即使 Import Name 被移除,Memory 中還是可能看到:
CALL DWORD PTR [401244]
而:
[401244] = 7C4586C8
接著到 Debugger 查看:
7C4586C8 → WriteFile
就可以把:
401244
標記成:
imp_WriteFile
這樣即使無法完整修復 PE,也可以繼續 Static Analysis。
重點是:
Malware Analysis 的目的不是完美還原原始檔案,而是取得足夠資訊理解 Malware Behavior。
UPX 是最常見也最適合學習 Unpacking 的 Packer。
特點:
upx -d
因此很適合拿來練習 Manual Unpacking。
PECompact 比 UPX 複雜,可能加入:
分析時需要注意 Debugger 對 Exception 的處理。
ASPack 會使用:
Self-Modifying Code
因此 Software Breakpoint 可能被覆蓋或造成程式異常。
比較實用的方法是利用:
PUSHAD
↓
Hardware Breakpoint on Stack
↓
POPAD
↓
Tail Jump
找到 OEP。
Petite 同樣包含 Anti-Debugging,而且可能使用 Single-step Exception。
因此需要正確設定 Debugger 的 Exception Handling。
尋找 OEP 時也可以利用 Stack Hardware Breakpoint。
UPack 會刻意讓 Tail Jump 很難辨識。
例如不用:
JMP OEP
而是:
PUSH OEP
RET
這其實等同於:
EIP = OEP
此外,Stub 與 OEP 可能位於同一個 Section,因此 Section Hop 方法可能失效。
這時可以從 GetProcAddress、GetModuleHandleA 或 GetCommandLineA 等位置慢慢往回找。
Themida 是本章中比較複雜的 Packer。
它包含大量:
而且 Themida 的 Code 可能在原始程式執行期間持續存在,不像簡單 Packer 在 Unpacking 完成後就把控制權完全交出去。
如果 Debugger 很難處理,可以考慮直接使用 Process Dump 工具取得執行中的 Memory,再針對 Dump 出來的內容分析。
這其實是本章很重要的一個觀念:
Malware Analysis 的目標不是「成功做出一個完美的 Unpacked EXE」,而是理解 Malware。
即使:
仍然可能:
如果這些資訊已經足以回答分析問題,就沒有必要花大量時間追求完美 Unpacking。
DLL 也可以被 Packed。
DLL 同樣存在 OEP,只是對 DLL 而言,OEP 對應的是原始 DllMain 的開始位置。
概念仍然一樣:
Packed DLL
↓
Unpacking Stub
↓
OEP
↓
Original DllMain
因此 EXE 使用的許多 Unpacking 思路,同樣可以套用在 DLL。
Chapter 18 最重要的不是記住每一種 Packer 的脫殼方法,而是理解整個 Packing / Unpacking 流程:
Original Program
↓
Packing
↓
Packed Program
↓
Unpacking Stub
↓
Restore Code
↓
Resolve Imports
↓
Tail Jump
↓
OEP
分析 Packed Malware 時,可以先觀察 Imports、Section、Entropy 等特徵判斷是否被 Packed。
如果有現成 Unpacker,先嘗試 Automated Unpacking;失敗後再進入 Debugger,讓 Stub 自己完成 Unpacking。
Manual Unpacking 最重要的三件事情就是:
找到 OEP、Dump Memory、修復 Imports。
而實際分析時也不需要執著於完全還原原始 Executable。只要能取得足夠的 Code、Strings、Imports 與執行行為,讓我們理解 Malware 在做什麼,就已經達到 Malware Analysis 的目的。
