ooooo ooo ooooo ooo ooooooooo. ooooooo ooooo
`888' `8' `888' `8' `888 `Y88. `8888 d8'
888 8 888 8 888 .d88' Y888..8P
888 8 888 8 888ooo88P' `8888'
888 8 888 8 888 .8PY888.
`88. .8' `88. .8' 888 d8' `888b
`YbodP' `YbodP' o888o o888o o88888o
A fork of UPX for security research, focused on unpacking UPX-packed binaries that have had their metadata deliberately clobbered to resist static unpacking.
UPX is a widely-used executable packer. Malware authors frequently use UPX to compress their payloads, then tamper with the UPX metadata (magic bytes, checksums, header fields) so that the standard upx -d refuses to unpack the binary. This forces analysts to resort to dynamic unpacking (running the binary in a sandbox and dumping at OEP), which has significant drawbacks.
uUPX adds a --force-unpack flag that bypasses all metadata validation and recovers the original binary by trusting only the compressed data blocks (b_info chain), which must remain intact for the binary to self-execute at runtime.
We consider a binary to be UPX-packed -- regardless of how much metadata has been corrupted -- as long as:
- The unpacking stub is unmodified. The embedded decompression code (assembly entry point + C decompressor) that runs at load time has not been altered.
- The binary can unpack itself at runtime. When executed, the stub successfully decompresses the original program and transfers control to it with full functionality preserved.
If these two conditions hold, then by definition all the information needed to statically recover the original binary is still present in the file. The runtime stub only reads the b_info block headers (compression method, block sizes) and the compressed data -- it never checks UPX! magic, checksums, l_info, p_info, or any of the other metadata that upx -d validates. Therefore, any metadata that the stub ignores can be freely clobbered without affecting our ability to unpack.
# Standard UPX refuses to unpack a tampered binary
$ upx -d malware.packed -o malware.unpacked
upx: malware.packed: NotPackedException: not packed by UPX
# uUPX force-unpacks it
$ ./upx -d --force-unpack malware.packed -o malware.unpacked
1032912 <- 407760 39.48% linux/amd64 malware.unpackedWhen analysts unpack malware dynamically by running it and dumping memory at the original entry point, they lose important information:
- Section headers are gone. The runtime loader discards them. uUPX recovers the full section header table (
.text,.data,.rodata,.bss,.symtab, etc.) because they are stored inside the compressed data. - Symbol tables are preserved. If the original binary was not stripped,
uUPXrecovers the symbol table intact. A memory dump has no symbols. - Relocations and dynamic linking info are intact. The GOT, PLT, and relocation tables are restored exactly. Memory dumps require manual IAT reconstruction.
- Byte-identical output. uUPX produces the exact original binary, byte-for-byte. No alignment issues, no page-granularity artifacts, no need to fix up PE/ELF headers manually.
- No execution required. The malware never runs. No sandbox, no risk of anti-analysis triggers, no network callbacks. Purely static analysis.
- BuildID preserved. The GNU BuildID (SHA-1 hash of the binary) is recovered, enabling correlation with debug symbols or known-binary databases.
uUPX handles all of the following being zeroed, randomized, or scrambled:
| Field | Purpose | Clobbered? |
|---|---|---|
UPX! magic (all occurrences) |
Format detection | Handled |
l_info (checksum, magic, lsize, version, format) |
Loader metadata | Handled |
p_info (filesize, blocksize) |
Pack metadata | Handled |
| PackHeader (version, format, method, checksums, sizes) | Decompression parameters | Handled |
overlay_offset (last 4 bytes of file) |
Locates compressed data | Handled |
EOF marker (b_info with sz_cpr == UPX_MAGIC) |
End-of-blocks sentinel | Handled |
| Adler32 checksums | Integrity verification | Handled |
Section names (UPX0, UPX1) |
PE section identification | Handled |
The only data that must remain intact is the b_info block chain and the compressed data itself. This is the minimum required for the runtime decompression stub to function -- if the binary can still self-execute, uUPX can unpack it.
The standard upx -d unpacker validates extensive metadata before decompressing:
- Searches for
UPX!magic to locate the PackHeader - Validates version, format, method, header checksum
- Reads
l_infoandp_infofor loader and size metadata - Verifies Adler32 checksums after decompression
- Checks decompressed ELF headers match the packed wrapper
With --force-unpack, uUPX bypasses all of these and instead:
- Reads the PackHeader from the end of the file (fixed position at
fsize - 36), falling back to scanning for a validb_infochain if the PackHeader is also scrambled - Derives
blocksizeandorig_file_sizefrom the actual block sizes in theb_infochain - Decompresses each block using the method specified in the per-block
b_infoheader - Applies unfiltering (reverses the x86 call/jump preprocessing) using the filter ID from
b_info - Writes the output using the decompressed ELF/PE headers for layout
- Linux ELF x86-64 (amd64) -- fully tested
- Linux ELF x86 (i386) -- fully tested
- Windows PE x86-64 -- metadata bypass implemented, not yet tested against real samples
- Windows PE x86 -- metadata bypass implemented, not yet tested against real samples
git submodule update --init
make build/release -j$(nproc)
# Binary at build/release/upxA comprehensive test suite is included that compiles test binaries, packs them with UPX, applies 9 different clobbering strategies, and verifies byte-identical recovery:
python3 test_force_unpack.pyTesting architecture: amd64 -- 9/9 passed
Testing architecture: i386 -- 9/9 passed
TOTAL: 18 passed, 0 failed
Based on UPX by Markus Oberhumer, Laszlo Molnar & John Reiser.