UPX Header Manipulation
Corrupting the UPX magic, version, or l_info fields so the standard `upx -d` refuses to unpack, while the runtime stub still decompresses normally.
A clean UPX binary can be restored with a single upx -d. To deny analysts that
shortcut, malware operators tamper with the metadata UPX writes into the file —
the UPX! magic, the version and format bytes, or the l_info/p_info
checksums — so the official tool aborts with errors like NotPackedException or
CantUnpackException, even though the embedded stub decompresses the payload
perfectly at runtime.
The trick works because the runtime stub does not consult those high-level
header fields the way the command-line unpacker does. The stub follows hard-coded
pointers to the compressed blocks, while upx -d validates the metadata first
and refuses to continue if anything looks wrong. Manipulating bytes the stub
never reads breaks the tool without breaking execution.
How it works
UPX appends a PackHeader containing the magic, version, method, and CRCs. The
unpacker locates UPX!, verifies the structure, then walks the compressed
blocks. Zeroing or altering the magic/version makes that validation fail:
UPX PackHeader (packed sample)
+0x00 magic "UPX!" (0x21585055) <- overwritten -> 0x00000000
+0x04 version 0x0D <- bumped to bogus value
+0x05 format 0x09 (win32/pe)
+0x06 method 0x0E (LZMA)
+0x07 level 0x09
+0x08 u_len original size
+0x0C c_len compressed size
+0x10 u_adler / c_adler (CRC32 checksums) <- corrupted to defeat verifyThe stub ignores these fields and decompresses by following its own offsets:
/* upx -d path: validates header, then refuses */
if (memcmp(ph.magic, "UPX!", 4) != 0) /* tampered -> fails here */
throw NotPackedException();
if (ph.version > MAX_SUPPORTED_VERSION) /* bogus version -> fails here */
throw CantUnpackException();
/* runtime stub path: never checks magic, uses fixed pointers */
nrv2b_decompress(blob_ptr, c_len, dest); /* still works at execution */Detection & analysis
Static analysis: Even with a wiped magic, the binary still looks packed:
sections named UPX0/UPX1 (or a single RWX ELF PT_LOAD), a tiny import
table, and ~7.9 bits/byte entropy. Compare the version/format bytes against the
valid UPX enumerations — out-of-range values, a zeroed magic, or mismatched
Adler/CRC fields next to otherwise intact UPX section names are the giveaway
that headers were hand-edited rather than produced by a real UPX build.
Dynamic analysis: Ignore the broken metadata entirely and unpack
generically — run the sample to its OEP by breakpointing the stub's tail
jmp/popad; jmp, then dump and rebuild the IAT with Scylla or pe-sieve (or
gcore on Linux). Alternatively, repair the header: restore UPX!, set a valid
version/format, and recompute the CRCs so upx -d accepts the file again.
Detection rule hint: Alert on files that carry UPX section names or b_info
structures but whose UPX! magic is absent, zeroed, or whose version byte is
outside the known range — a strong indicator of deliberate header manipulation
to break automated unpacking, far more common in malware than in legitimately
packed software.