Strings and Unpacking
When strings comes back empty and entropy reads 8.0, the real binary is hiding under a packer.
What you will learn
- -Recognize a packed binary from entropy, section names, and a bare import table
- -Unpack UPX and dump self-unpacking samples from memory
- -Pull the useful indicators out of the unpacked code
You run strings on a sample and a handful of Windows names and nothing else. No URLs, no filenames, no readable code. The import table lists three functions. That emptiness is the finding: the sample is packed, meaning the real is compressed or encrypted inside a small stub that unpacks it in memory at runtime. Packing is how authors beat and shrink their file. Your job is to get past the stub to the code underneath.
Packing is not automatically malicious, plenty of legitimate software uses UPX, but on a suspected sample it's a strong signal that someone wanted to hide what's inside.
Spot the packer
Three tells, and you rarely need all three. High across the code section is the first: a normal .text runs 5 to 6.5 bits per byte, and a packed one reads close to 8.0 because compressed and encrypted data looks random. Odd section names are the second: instead of .text and .data you see UPX0, UPX1, .aspack, or a random string. A near-empty import table is the third: a packed binary only needs to import enough to unpack itself, often just LoadLibrary, GetProcAddress, and VirtualAlloc, then it resolves the real imports at runtime.
$python3 -c "import pefile,sys;pe=pefile.PE(sys.argv[1]);[print(s.Name.decode().rstrip('\x00'),round(s.get_entropy(),2)) for s in pe.sections]" sample.exe.vir$die sample.exe.virDetect It Easy (die) is worth installing because it identifies most common by and saves you the guesswork. If it says UPX, you're in luck, because that one unpacks with a single command.
The easy case: UPX
UPX is the most common packer and it ships with its own decompressor. If Detect It Easy or the UPX section names confirm it, unpacking is one line.
$upx -d sample.exe.vir -o sample_unpacked.exeSome authors deliberately corrupt the UPX so upx -d refuses to run. When that happens, you drop to the general technique below, which works regardless of packer.
The general case: dump from memory
Any self-unpacking sample has to reveal its real code in memory before it can execute it. That's the weakness you . You let the sample run just far enough to unpack itself, freeze it, and dump the now-decrypted code out of RAM. This is dynamic unpacking, and it beats custom packers that no static tool understands.
The manual route is a like x64dbg: set a on the moment execution jumps from the unpacking stub into the freshly written code (the "tail jump"), then dump the process with the Scylla plugin and fix the imports. It's fiddly the first few times and worth practicing on a known UPX sample where you already know the answer.
The faster route for is a memory-dump-based scan. Detonate in your instrumented VM, and while the sample is live, scan its memory for the and IOCs the packed file hid.
$procdump64.exe -ma sample.exe C:\analysis\sample.dmp$strings -n 8 C:\analysis\sample.dmp | grep -iE 'http|\.onion|HKEY|\.exe|powershell'Comparing the two sets of strings is instructive: the file-on-disk had almost nothing, the has the , the dropped filenames, and the commands. That difference is the packer's whole job, undone.
Dumping from memory requires letting the sample execute, so do it only inside the isolated, snapshotted VM from the safe-handling lesson. The sample is fully live during a memory dump. Confirm your isolation before you start, and revert the snapshot the instant you have your dump.
Which set of observations most reliably indicates a sample is packed?
Take any binary, pack it yourself with upx sample.exe, and confirm the packing: run strings before and after packing and note how the readable output shrinks, then check the section entropy. Now run upx -d to unpack it and confirm the strings come back. Write down the entropy of the code section in the packed versus unpacked state.