Open a packed binary in a disassembler and there's almost nothing there — a few hundred bytes of stub, a high-entropy blob, and an entry point that doesn't lead to any recognisable code. The real program only exists at runtime, after the stub decompresses it into memory and jumps to the original entry point (OEP). To analyse it statically, you have to be there at that jump and take a snapshot.
- entropy
- ~7.9 bits/byte (compressed/encrypted body)
- sections
- tiny .text stub + one big rwx blob
- goal
- recover the unpacked program at its OEP
first, rule out the easy case#
Honest UPX round-trips:
When that fails — and on CTF binaries it usually does, because the UPX! magic and
the header fields get scribbled over to break upx -d — you unpack the way that
works on any packer, including custom ones: dynamically.
find the OEP: follow the stub to its tail jump#
The stub does its work, then transfers control to the freshly-unpacked code with a tail jump — a jump to an address far from the stub, often right after a register-restore. Recognising it is the whole skill:
Break there and single-step into it; you land on what looks like a normal program
entry (a _start/__libc_start_main setup). Generic ways to catch the moment:
- Hardware breakpoint on the unpacked region. Mark the rwx blob and break on execute — you stop the instant the stub jumps into decompressed code.
- Break after the big write loop. The decompressor is a tight copy loop; set a breakpoint just past it.
- Watch entropy /
mprotect. Many packersmprotectthe unpacked region executable just before the jump — break on that syscall.
dump and rebuild#
At OEP, the real program is sitting decompressed in memory. Snapshot it:
The dump runs, but a static tool still chokes until you fix two things up: set the ELF entry point to the OEP you found, and repair the imports — packers resolve the GOT/PLT at runtime, so a from-memory dump has the resolved pointers, which you re-point at proper relocation entries (the ELF equivalent of IAT reconstruction).
the unpacked OEP
After the tail jump, the entry resolves to ordinary code — main is reachable,
strings are back, the disassembler has something to chew on:
and the anti-unpacking it'll throw at you#
- Anti-debug in the stub —
ptrace(PTRACE_TRACEME), timing checks,/procself-inspection. Defeat them exactly as in anti-debug:LD_PRELOADa fakeptrace, or patch the check. - Self-checksums / re-packing — the stub may re-encrypt regions after use; dump at the exact OEP moment, before it cleans up.
- Stolen bytes — some packers move the first few OEP instructions into the stub and run them there, so the dumped OEP is missing its head; recover them from the stub and prepend.
Unpacking is the same dance regardless of the packer: let it do the work it was
built to do, freeze the program the instant the real code exists, and walk away
with the snapshot. upx -d is a convenience; the OEP-and-dump method is the one
that never stops working.