Skip to content
Packing & Cryptersintermediate

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:

text
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 verify

The stub ignores these fields and decompresses by following its own offsets:

c
/* 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.

Votes

Comments(0)