ELF Packing
Compressing an ELF binary with UPX or a modified packer so its code only materialises after a self-decompressing stub runs at load time on Linux.
ELF packing compresses the loadable segments of a Linux executable and prepends a small decompression stub. At runtime the stub mmaps a region, inflates the original program into it, and transfers control to the recovered entry point, so a static look at the file on disk sees only one or two compressed segments and a near-empty symbol table.
UPX is by far the most common ELF packer in the wild, used heavily by Linux
botnet families such as Mirai and XorDDoS. Because the UPX source is public,
operators routinely patch the stub or rewrite the UPX! magic so that upx -d
refuses to unpack, while the runtime decompression logic still works.
How it works
A packed ELF collapses the original multi-segment layout into a single
PT_LOAD segment marked read+write+execute, holding the stub followed by the
compressed image. Note how readelf shows almost no sections — only program
headers remain meaningful:
ELF program headers (packed)
Type Offset VirtAddr FileSiz MemSiz Flg
LOAD 0x000000 0x00400000 0x0a3f0 0x0a3f0 R E <- stub + compressed data
LOAD 0x00b000 0x00c00000 0x00000 0x18000 RW <- bss for inflated image
Entry point -> stub (decompresses, then jmp to recovered OEP)
Section header table: stripped / b_info "UPX!" markers presentThe stub decompresses each block and jumps to the original entry point (OEP):
; UPX-style ELF stub (simplified)
lea rsi, [rip + packed_blob] ; source: compressed image
mov rdi, dest_addr ; mmap'd RWX region
call ucl_nrv2b_decompress ; inflate original segments
mov rax, oep ; recovered original entry point
jmp rax ; transfer control to real programDetection & analysis
Static analysis: Run readelf -l and readelf -S — a packed ELF typically
shows a single RWX PT_LOAD, a stripped or absent section header table, and the
ASCII markers UPX! / UPX0 near the headers. Measure entropy with ent or
binwalk -E; the compressed segment reads ~7.8–7.9 bits/byte, far above the
~5–6 of normal .text. Search for the four-byte b_info magic 0x21585055
(UPX!) even when the version string has been wiped.
Dynamic analysis: Run the sample under gdb/ltrace in an isolated VM, set
a catchpoint on mmap/mprotect, and let the stub finish inflating. Break on
the tail jump (the jmp rax to a far, lower address), then dump the now-plain
RWX region with gcore or /proc/<pid>/mem and carve out the reconstructed
ELF. For modified-header samples, restoring the canonical UPX! magic and
version often lets upx -d succeed again.
Detection rule hint: Flag ELF files whose only loadable segment is RWX and
whose overall entropy exceeds ~7.2, especially when the section header table is
stripped but UPX! byte sequences appear — a combination that is normal for
packed botnet droppers and rare for legitimate stripped binaries.