[+] Packers and Unpacking
/ 6 min read
Table of Contents
My Journey with Packers and Unpacking
The first time I opened a packed binary in Ghidra, the decompiler showed me garbage. Almost no imports, high entropy everywhere, and a tiny code section that seemed to do nothing meaningful. I did not know it at the time, but I was looking at an unpacking stub - the real code was hidden inside.
Learning to unpack binaries opened up a whole new level of analysis for me. Here is how I got there.
What I Learned About Packers
A packer transforms an executable so that its original code is hidden. The packed binary contains:
- A small unpacking stub (the loader I was seeing)
- The compressed or encrypted original binary
At runtime:
Packed binary starts -> Stub decompresses/decrypts payload ->Writes it to memory -> Jumps to the Original Entry Point (OEP)The key insight: if I can find that moment when it jumps to OEP, I can dump the unpacked code from memory.
My First Unpack - UPX
I started with UPX because everyone said it was the easiest. They were right.
# I could literally just do thisupx -d packed.exe
# Done. Unpacked.But I wanted to understand the process manually. So I loaded a UPX-packed binary in x64dbg and followed the execution:
- It started with a PUSHAD (saves all registers)
- A decompression loop ran for a while
- Then POPAD (restores registers)
- A single JMP to the original code
That JMP target was the OEP. I set a breakpoint there, dumped the process with Scylla, and had my first manually unpacked binary. The feeling was great.
How I Identify Packed Binaries Now
Entropy Check
This is my first indicator. Packed or encrypted data has high entropy:
rabin2 -S suspicious.exe
# What I look for:# Normal .text section: entropy 5.0-6.5# Packed/encrypted: entropy 7.0-8.0Import Table
Packed binaries have almost no imports - usually just:
- LoadLibraryA
- GetProcAddress
- VirtualAlloc / VirtualProtect
That is because the real imports get resolved at runtime after unpacking.
Section Names
Dead giveaways I have seen:
.UPX0,.UPX1(UPX).aspack(ASPack).vmp(VMProtect)- Random garbage characters (custom packers)
Tools I Use for Detection
# Detect It Easy - my go-todie suspicious.exe
# Also check with pestudio for quick triageThe ESP Trick - My Reliable Method
This technique works on most simple packers and I use it constantly:
1. Load packed binary in x64dbg2. Run to entry point3. First instruction is usually PUSHAD or equivalent4. Step over it once5. Note the ESP value6. Right-click ESP -> Follow in Dump7. Set hardware breakpoint on access for those bytes8. Press F9 (run)9. It breaks after POPAD - the packer is done10. Step forward - next JMP or CALL is to OEP11. Follow it, now I am at the real entry point12. Dump with ScyllaI practiced this on maybe 20 different samples before it became second nature.
Fixing the Import Table
After dumping, the binary is usually broken. The imports do not work because the IAT (Import Address Table) still points to the packer’s runtime-resolved addresses.
My fix process:
- In Scylla, set the OEP I found
- Click “IAT Autosearch” - it finds the import table in memory
- Click “Get Imports” - it resolves function names
- Fix any invalid entries manually
- Click “Fix Dump” - patches my dumped file
After this, the unpacked binary runs and loads in Ghidra/IDA properly with all imports visible.
Harder Packers I Have Dealt With
ASPack
Slightly harder than UPX. The ESP trick still works but the unpacking loop is longer and has a few fake jumps that confused me initially. I learned to recognize the final JMP by its distance - it jumps far away from the stub section into .text.
Custom XOR Packers
I see these a lot in CTFs and basic malware. The pattern:
// What the stub doesfor (int i = 0; i < size; i++) { payload[i] ^= key;}// Jump to decrypted payloadI handle these by breaking on VirtualProtect (the stub needs to make the decrypted memory executable) and checking what is in the target buffer after the call.
Multi-Stage Packers
Some malware unpacks in layers:
Stage 1: XOR decrypt -> Stage 2: Decompress (LZMA) -> Stage 3: Resolve imports -> OEPI dealt with a sample that had 3 stages. I had to dump and analyze each layer separately. Memory breakpoints on execute were my friend here - every time execution hit a new region, I knew a new stage started.
Themida
The hardest I have faced. Themida combines packing with:
- Anti-debug that detects x64dbg even with ScyllaHide
- Anti-dump that erases PE headers after unpacking
- Code virtualization on top of the unpacking
I needed to use TitanHide (kernel-level) to hide my debugger, and even then it took multiple attempts. For Themida, I now check if someone has already written an unpacker script before spending days on it.
Anti-Unpacking Tricks I Encountered
Timing Checks
// Packer checks if execution is too slow (debugger attached)LARGE_INTEGER start, end;QueryPerformanceCounter(&start);// ... unpacking code ...QueryPerformanceCounter(&end);if ((end - start) > threshold) exit(0); // detected debuggerI patch these out or use ScyllaHide to fake the timing values.
IsDebuggerPresent
The classic. Some packers check this multiple times throughout unpacking. ScyllaHide handles it, but I also learned to patch the PEB directly:
In x64dbg:PEB -> BeingDebugged byte -> set to 0Anti-Dump
Some packers erase the PE header from memory after unpacking. When I try to dump, the header is zeroed out. My fix: I take a copy of the header before running the packer, then paste it back into my dump.
My Current Workflow
1. Identify: DIE, entropy check, import count2. Snapshot VM3. Load in x64dbg with ScyllaHide active4. Try ESP trick first (works 70% of the time)5. If that fails, break on VirtualAlloc/VirtualProtect6. Find OEP7. Dump with Scylla8. Fix IAT9. Verify unpacked binary loads in Ghidra with imports10. Revert VM snapshotTools I Rely On
| Task | Tool |
|---|---|
| Identify packer | DIE, pestudio |
| Debug | x64dbg + ScyllaHide |
| Dump | Scylla |
| Fix imports | Scylla IAT fixer |
| Hide debugger | ScyllaHide, TitanHide |
| Auto-unpack (simple) | upx -d, unipacker |
| Entropy | rabin2, pestudio |
What I Wish I Knew Earlier
- The ESP trick works on most things. Learn it first.
- Do not fight anti-debug manually - use ScyllaHide from the start.
- Take VM snapshots before every run. I corrupted samples by accident early on.
- The IAT fix step is where most people give up. Scylla makes it manageable.
- Write down the OEP address. I forgot it once after a long session and had to redo everything.
- Start with UPX, then ASPack, then custom packers. Build up gradually.
Unpacking is a patience game but it gets faster with practice. Now when I see a packed binary, I feel excited instead of frustrated.