From Packed Binary to Readable Code: A Hands-On Walkthrough of Unpacking, Shellcode Analysis, and Memory Forensics A developer documented a full malware analysis lab session, unpacking and analyzing packed binaries, shellcode, and memory forensics. The walkthrough covers static analysis, manual unpacking with a debugger, multi-stage shellcode extraction, code injection patterns, API hooking, and memory forensics with Volatility. The exercise was conducted on isolated virtual machines using teaching specimens. A few weeks ago I spent a full lab session doing something that sounds simple on paper and is genuinely satisfying in practice: taking a packed, obfuscated piece of malware and peeling back every layer until I could see what it actually does. This post is my write-up of that session. It's long, because the lab itself covered a lot of ground — static analysis, manual unpacking with a debugger, multi-stage shellcode extraction, code injection patterns, API hooking, and finally memory forensics with Volatility. I'm documenting it the way I wish more "intro to malware analysis" posts were documented: with the actual commands, the actual reasoning behind each step, and the dead ends along the way. If you're getting into reverse engineering or malware analysis, this should give you a realistic feel for what a packed-malware investigation actually looks like end to end — not just the highlight reel. A quick but important note: this entire exercise was done on isolated, throwaway virtual machines a Windows analysis VM with no network access beyond an internal isolated segment, plus a REMnux Linux VM using known teaching specimens. Never run unknown executables, run unpackers, or experiment with shellcode on a machine connected to a real network or containing real data. Everything here assumes a fully isolated, snapshot-able VM setup. Modern malware rarely ships as a plain, readable executable. Authors wrap their code in packers like UPX to shrink the file and make static analysis harder, and they layer in techniques like shellcode, code injection, and API hooking to evade detection and persist on a system. As an analyst, your job is to answer a chain of questions: This walkthrough tackles that chain using three teaching specimens: a UPX-packed sample brbbot.exe , a multi-technology dropper that chains JavaScript → PowerShell → shellcode PDFXCview.exe , and a code-injecting, API-hooking sample analyzed both statically and via a memory image great.exe / great.vmem . Two VMs, both reverted to clean snapshots before starting: pescanner.py , diec , strings , SpiderMonkey js , base64dump.py , Volatility vol.py Both VMs were on an isolated internal network segment so the Windows VM and REMnux VM could talk to each other for file transfer and the JavaScript dropper's local web server without any route to the internet. First pass: load the suspicious binary in PeStudio and check three things — imports, section names, and strings. A packed file typically shows: .text , .rdata , .data , you'll see something like UPX0 and UPX1 All three were true here, and the UPX naming convention in the section headers was a strong hint about which packer was used. On REMnux, pescanner.py measures the entropy of each section. High entropy close to random is a hallmark of compressed or encrypted data: pescanner.py brbbot.exe | more The tool flagged sections as "SUSPICIOUS" — one for unusually high entropy consistent with packed/compressed code , and one with an entropy of exactly 0 because its raw size was 0 — also anomalous for a legitimate section . diec brbbot.exe diec Detect It Easy, command-line version reported UPX as the most likely packer — confirming the hint from the section names. upx -d %AppData%\brbbot.exe This is always worth trying, but it commonly fails on malware, because authors deliberately corrupt the UPX header/footer to block the standard unpacker while leaving the actual UPX decompression stub intact: CantUnpackException: file is possibly modified/hacked/protected; take care Since the automated route was blocked, the next move is manual dumping : let the malware unpack itself in memory at runtime, then dump that unpacked memory image to disk. setdllcharacteristics -d %AppData%\brbbot.exe This flips the DYNAMIC BASE flag in the PE header from 1 to 0. Without this, the binary would load at a randomized base address every run, which makes it harder to find a stable breakpoint address across debugging sessions. With the sample running via a desktop shortcut set to "Run as administrator" , attach Scylla x64 to the process and click Dump . This grabs the in-memory, already-unpacked version of the code. But a raw memory dump alone usually isn't runnable — the Import Address Table IAT is broken, because imports get resolved dynamically and the dump doesn't capture that resolution cleanly. So: Scylla writes a new file with SCY appended to the name e.g., brbbot-dumped SCY.exe — this is the "fixed" version with a repaired import table. Loading the fixed dump back into PeStudio showed more imports than the packed original — a good sign. But running the fixed dump directly produced a different outcome than expected it exited immediately, without dropping the configuration file the real malware drops . This is a useful and realistic lesson : successfully fixing the IAT doesn't guarantee a perfectly runnable standalone binary. Sometimes further reconstruction is needed. Don't take "it loads more imports now" as proof the unpacking job is fully done — verify behavior too. Scylla's automatic dump-and-fix approach doesn't always work cleanly, so it's worth knowing the manual debugger-based path too. Load the packed binary in x64dbg . Scroll through the disassembly until the unpacking stub's instructions end and you hit a long run of zero bytes — that boundary is usually right where the final jump sits: jmp brbbot.140003F94 That 140003F94 target address is the OEP — the address where the real , unpacked program logic begins. Set a breakpoint on the JMP instruction, then run F9 . The process will execute all the unpacking logic and pause right at that breakpoint, immediately before transferring control to the unpacked code. Step over the jump F7 or F8 to land at the OEP — execution is now paused inside the unpacked code. Don't just trust the address — verify it. Right-click in the CPU view and run: Both showing up is good confirmation you're looking at genuinely unpacked code. From x64dbg's Plugins menu: OllyDumpEx → Dump process . Key details: UPX1 section row and enable the MEM WRITE characteristic flag before dumping without write permission flagged, some dumpers won't capture the section properly brbbot dump 64.exe Same logic as before — IAT Autosearch → Get Imports → Fix Dump , pointed at the OllyDumpEx output. Result: a SCY -suffixed file with a repaired import table. Sometimes you don't want to fully unpack a sample — you just want to watch a specific operation happen, like decryption of an embedded configuration. Run the packed binary in x64dbg with no breakpoints set F9 . It unpacks itself into memory and continues normally. In the Memory Map tab, look for memory regions that don't belong to a Windows DLL and have "E" execute in the Protection column. In this sample, two regions matched that profile — the unpacker code region and a second region holding the freshly unpacked code. Right-click the latter and choose Follow in Disassembler . Right-click → Search for → Current Region → Intermodular calls , then filter the results by typing a keyword e.g. Crypt in the search box. This surfaced a call to CryptDecrypt — a strong signal that the malware decrypts an embedded configuration at runtime. Select the instruction right after the CryptDecrypt call the result-checking instruction , and set a hardware breakpoint on execution . Then restart the process Ctrl+F2 and run again F9 . Why restart rather than just continuing? Because the process may have already executed past this point once — restarting guarantees you hit the breakpoint fresh, from the actual entry point, so register/stack state is consistent with a real first-run analysis. Once paused there, the decrypted configuration data is sitting in memory commonly reachable via the stack — ready for inspection, exactly like you would when analyzing the unpacked version of the same family of malware. This is where things get more interesting: a single executable that chains together several different technologies to avoid writing an obviously malicious file to disk. Start Process Monitor capturing, then run the sample. Watch the process tree in Process Hacker : the initial process spawns mshta.exe and powershell.exe , then after roughly a minute or two, spawns a couple of regsvr32.exe processes. Once those appear, terminate the process tree and pause Process Monitor capture — you don't need to let it run indefinitely, you just need enough activity captured to reconstruct the chain. Export the Process Monitor log as CSV, then load it into ProcDOT along with the initial malicious process. ProcDOT generates a visual graph of what touched what — registry keys created, files dropped, and a persistence entry added under the Run autostart key. It also revealed the malware created files with an unusual, randomly-generated extension and a batch file, plus matching registry entries describing how Windows should handle that custom file extension. In Regedit , navigate to: HKEY CURRENT USER\Software\Classes\.