iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Security

《Nyahello!從零開始的 30 天 malware 學習日誌》系列 第 30 篇

【Day 30】Packers and Unpacking:惡意程式的加殼與脫殼

  • 分享至 

  • xImage
  •  

前言

前面進行 Malware Analysis 時,我們常會遇到一個問題:明明拿到的是 PE 執行檔,丟進 IDA Pro 後卻看不到多少正常程式碼,Imports 也少得異常。

這時很可能遇到的就是 Packed Malware。

Packer 原本可以用來壓縮執行檔、減少檔案大小,但 Malware 作者也會利用它隱藏原始程式碼、增加分析難度,甚至搭配 Anti-Debugging、Anti-VM 等技術。

因此,遇到 Packed Malware 時,通常需要先進行 Unpacking(脫殼),才能繼續有效地做 Static Analysis。

【聲明】由於時間問題本日文章由 AI 生成QQ

然後今天是鐵人賽最後一天,推甄資料弄得差不多睡了一整天,祝大家中秋快樂~


Packer 是什麼?

Packer 的基本概念是:

將原本的 Executable 經過壓縮、加密或其他轉換,再包進另一個可以執行的 PE 檔案。

可以簡化成:

Original Executable
        ↓
      Packer
        ↓
Packed Executable

Packed Executable 通常包含兩個重要部分:

  1. 被壓縮或加密的原始程式
  2. Unpacking Stub

真正執行時,不是直接執行原本的 Malware,而是先進入 Unpacking Stub。


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 主要做三件事情:

  • 將原始程式解壓或解密到 Memory
  • Resolve 原始程式需要的 Imports
  • 將執行流程轉移到 Original Entry Point(OEP)

這也是為什麼直接對 Packed Malware 做 Static Analysis 效果不好:我們看到的主要是 Stub,而不是真正的 Malware Code。


Resolving Imports

正常 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,進一步增加分析難度。


Tail Jump 與 OEP

當 Stub 完成 Unpacking 後,最後必須把控制權交回原始程式。

這個轉移通常稱為:

Tail Jump

最單純的情況可能是:

Unpacking Stub
      ↓
    JMP OEP
      ↓
Original Program

但為了增加分析難度,也可能使用:

RET
CALL
NtContinue
ZwContinue

因此 Unpacking 很重要的一個工作就是:

找到 Stub 結束的位置,以及原始程式真正開始執行的 OEP。


如何判斷程式被 Packed?

分析 Malware 時,可以從幾個現象判斷。

Imports 很少

例如只看到:

LoadLibrary
GetProcAddress

甚至完全沒有 Imports。

IDA Pro 只辨識出少量 Code

因為真正的 Code 還是壓縮或加密資料,IDA 無法正常 Disassemble。

Section 名稱異常

例如:

UPX0
UPX1

看到這類 Section Name,可以懷疑使用 UPX。

Section Size 異常

例如:

SizeOfRawData = 0
VirtualSize != 0

代表 Disk 上沒有正常 Code,但執行後 Memory 中卻需要一塊空間,可能就是準備放 Unpacked Code。


Entropy

另一個常見判斷方式是:

Entropy(熵)

壓縮或加密後的資料通常更接近 Random Data,因此:

Normal Code
    ↓
Lower Entropy

Compressed / Encrypted
    ↓
Higher Entropy

所以如果某個 Section Entropy 異常高,也可能代表:

  • Compression
  • Encryption
  • Packing

但要注意,高 Entropy 只能作為 Indicator,不能單獨證明程式一定被 Packed。


Unpacking 的三種方式

大致可以分成:

  1. Automated Static Unpacking
  2. Automated Dynamic Unpacking
  3. Manual Dynamic Unpacking

Automated Static Unpacking

最簡單的方法就是使用針對特定 Packer 的 Unpacker。

例如 UPX:

upx -d malware.exe

優點是:

  • 快
  • 不需要執行 Malware
  • 可以直接恢復程式

但缺點是通常只能處理特定 Packer。

如果 Malware 使用修改版 Packer 或 Custom Packer,就可能失敗。

因此實務上:

能自動脫殼就先自動脫殼,不行再進入 Manual Unpacking。


Automated Dynamic Unpacking

另一種方法是實際執行 Malware:

Run Malware
     ↓
Stub 自己 Unpack
     ↓
Dump Memory
     ↓
Rebuild Imports

問題在於工具必須判斷:

Stub 到底什麼時候結束?

如果判斷錯誤,就無法正確 Dump。

而且 Dynamic Unpacking 會真的執行 Malware,因此一定要在隔離的 Analysis Environment 中操作。


Manual Unpacking

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


Finding the OEP

OEP(Original Entry Point)就是程式被 Packing 前真正的 Entry Point。

沒有任何一種找 OEP 的方法可以處理所有 Packer,因此通常需要交替使用不同技巧。


方法一:找 Tail Jump

最直觀的方法就是找 Stub 最後跳回原始程式的 Jump。

典型情況:

unpacking code
unpacking code
unpacking code

JMP 00401000    ← Tail Jump

00
00
00
00

Tail Jump 常有幾個特徵:

  • 位於 Stub 尾端
  • Jump 距離異常遠
  • Jump 後面出現大量 Padding
  • Target 在執行前不像正常 Code
  • Unpacking 完成後 Target 變成正常 Instructions

例如原本 OEP Memory 可能看起來都是:

00 00 00 00...

但 Stub 執行完成後變成:

CALL ...
TEST EAX,EAX
JNZ ...
PUSH ...
CALL ...

代表原始 Code 已經被寫回 Memory。


方法二:Stack Breakpoint

很多 Unpacking Stub 開頭會:

PUSHAD

保存 Registers,結束時再:

POPAD

因此可以觀察 Stack。

基本概念:

PUSHAD
   ↓
Unpacking
   ↓
Unpacking
   ↓
POPAD
   ↓
Tail Jump
   ↓
  OEP

可以在 Stack 中保存 Register 的位置設置 Hardware Breakpoint on Read。

當 Stub 最後執行 POPAD 讀取這些資料時,Breakpoint 被觸發,通常表示已經接近 Tail Jump。


方法三:GetProcAddress Breakpoint

Stub 通常需要 Resolve Imports,因此可能大量呼叫:

GetProcAddress

可以直接在 GetProcAddress 設 Breakpoint。

這樣就能跳過 Stub 前面大量的解壓或解密流程,直接停在 Unpacking 後半段。

接著再尋找:

Import Resolution
      ↓
   Tail Jump
      ↓
      OEP

方法四:從常見 API 往回找

Windows Program 在 OEP 附近通常會執行一些初始化工作。

例如 Command-line Program 可以觀察:

GetVersion
GetCommandLineA

GUI Program 可以觀察:

GetModuleHandleA

當 Breakpoint Hit 後,再往回查看是哪段 Code 呼叫它,就有機會找到 OEP 附近。


方法五:Run Trace

如果原始 .text Section 還存在,可以監控:

什麼時候第一次開始執行 .text 裡的 Code?

因為 Stub 執行完後通常會進入原始 .text Section。

因此第一次執行該區域的位置,很可能就是 OEP。


Dump Process

找到 OEP 後,代表原始 Malware Code 已經存在 Memory。

下一步就是:

Dump Process

例如使用 OllyDump:

Find OEP
   ↓
Dump Debugged Process

但直接把 Memory Dump 出來還不一定能正常執行,因為 PE Header 仍可能保留 Packed File 的資訊。

至少需要處理兩件事:

  1. Entry Point 改成 OEP
  2. Rebuild Import Table

Rebuilding Import Table

如果 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。


手動辨識 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。


常見 Packers

UPX

UPX 是最常見也最適合學習 Unpacking 的 Packer。

特點:

  • Open Source
  • Compression 為主
  • Unpacking Stub 相對單純
  • 通常可以直接 upx -d
  • Tail Jump 與 OEP 相對容易找

因此很適合拿來練習 Manual Unpacking。


PECompact

PECompact 比 UPX 複雜,可能加入:

  • Anti-Debugging
  • Exceptions
  • Obfuscated Code

分析時需要注意 Debugger 對 Exception 的處理。


ASPack

ASPack 會使用:

Self-Modifying Code

因此 Software Breakpoint 可能被覆蓋或造成程式異常。

比較實用的方法是利用:

PUSHAD
  ↓
Hardware Breakpoint on Stack
  ↓
POPAD
  ↓
Tail Jump

找到 OEP。


Petite

Petite 同樣包含 Anti-Debugging,而且可能使用 Single-step Exception。

因此需要正確設定 Debugger 的 Exception Handling。

尋找 OEP 時也可以利用 Stack Hardware Breakpoint。


WinUpack / UPack

UPack 會刻意讓 Tail Jump 很難辨識。

例如不用:

JMP OEP

而是:

PUSH OEP
RET

這其實等同於:

EIP = OEP

此外,Stub 與 OEP 可能位於同一個 Section,因此 Section Hop 方法可能失效。

這時可以從 GetProcAddress、GetModuleHandleA 或 GetCommandLineA 等位置慢慢往回找。


Themida

Themida 是本章中比較複雜的 Packer。

它包含大量:

  • Anti-Debugging
  • Anti-Analysis
  • Anti-VM
  • Kernel Component

而且 Themida 的 Code 可能在原始程式執行期間持續存在,不像簡單 Packer 在 Unpacking 完成後就把控制權完全交出去。

如果 Debugger 很難處理,可以考慮直接使用 Process Dump 工具取得執行中的 Memory,再針對 Dump 出來的內容分析。


不一定要完全 Unpack

這其實是本章很重要的一個觀念:

Malware Analysis 的目標不是「成功做出一個完美的 Unpacked EXE」,而是理解 Malware。

即使:

  • Import Table 沒完全修好
  • Dump 出來的程式不能執行
  • PE Header 不完整

仍然可能:

  • 用 IDA Pro 看 Code
  • 分析 Strings
  • 找 API
  • 搭配 Dynamic Analysis
  • 分析 Memory 中已經解開的 Code

如果這些資訊已經足以回答分析問題,就沒有必要花大量時間追求完美 Unpacking。


Packed DLL

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 的目的。

參考資料

  • Practical Malware Analysis (Chapter 18)
  • chatGPT

本日貓味

https://ithelp.ithome.com.tw/upload/images/20260925/20183878bXD1kTjAiO.jpg


上一篇
【Day 29】Anti-Debugging:惡意程式如何發現自己正在被 Debug?
系列文
《Nyahello!從零開始的 30 天 malware 學習日誌》 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言