A hardened fork of UPX — the Ultimate Packer for eXecutables — with anti-debug and anti-tamper runtime protection built into every packed binary.
UPX Ultimate keeps everything you know from vanilla UPX: fast, lossless executable compression with typical size reductions of 50–70%, completely self-contained output, and zero runtime overhead. On top of that, it adds a layer of runtime protection that vanilla UPX deliberately does not.
Vanilla UPX explicitly refuses to add protection or encryption — the upstream
README states "We will NOT add any sort of protection and/or encryption…
all 'protectors' can be broken by definition." That is a sound principle, but
there are cases where you want to raise the bar for casual tampering and
analysis without changing the format or sacrificing compression.
UPX Ultimate is that variant. It injects a self-verifying runtime stub into every packed amd64-linux ELF binary. The stub runs before the original program starts, and kills the process if it detects an attached debugger or any modification to its own code.
| Capability | Vanilla UPX | UPX Ultimate |
|---|---|---|
| Lossless executable compression | Yes | Yes |
| Self-contained, zero-overhead output | Yes | Yes |
| Custom pack-header magic (anti-unpacking) | No | Yes |
| Anti-debug (tracer detection) | No | Yes |
| Anti-tamper (breakpoint / code-patch detection) | No | Yes |
| Same CLI and file format | — | Yes |
The protections live entirely inside the decompression stub, so they activate the instant a packed binary executes — no opt-in flag, transparent at pack time.
A rolling 32-bit checksum over the loader stub's own code region
[_start, stub_checksum) is computed at startup and compared against the value
the packer wrote at pack time.
- Seed
0x9E3779B9; per byteh = rol7(h + byte). - The checksum slot carries a
0x0BADF00Dmarker (UPX_STUB_CHK_MAGIC_LE32) in the stub source; the packer locates the marker in the final relocated loader and patches the real value in place.
Any software breakpoint (int3 / 0xCC) or code patch written into that region
by gdb, radare2, IDA Pro, Ghidra, or a similar tool changes the bytes and trips
the check.
A read-only /proc/self/status check reads the TracerPid: field. A non-zero
value means a tracer (gdb, r2 -d, IDA remote, Ghidra, strace — all attach via
ptrace) is present, and the binary is terminated.
TracerPid is used deliberately instead of ptrace(PTRACE_TRACEME), which
would mark the process traceable and can stall it under any supervising tracer
(sandboxes, job supervisors, CI runners). The TracerPid read is side-effect
free; if /proc is unavailable (chroot, minimal container) the open() fails
and the check is skipped safely.
Packed binaries are stamped with a custom magic UPXM (0x4D585055) instead of
the standard UPX! header, so a vanilla upx -d cannot detect or unpack them.
UPX Ultimate itself recognizes both magics, keeping full round-trip
compatibility for its own output.
On either the anti-tamper or anti-debug condition the stub executes ud2,
raising SIGILL. The instruction is followed by a spin loop so that even if the
signal is swallowed and execution is resumed, the binary re-traps instead of
continuing.
| Target | Status |
|---|---|
| amd64-linux ELF | implemented and verified |
| i386 / arm64 / other ELF | not yet — same technique ports directly, needs cross-compiler toolchains to rebuild stubs |
| Windows PE | not yet — needs a PEB / IsDebuggerPresent implementation |
A normal build is unchanged. The generated stub blob is committed, so building the packer does not require the stub cross-compiler toolchain.
mkdir -p build/release && cd build/release
cmake -DCMAKE_BUILD_TYPE=Release ../..
cmake --build . -j$(nproc)The binary is produced at build/release/upx.
Rebuilding src/stub/src/amd64-linux.elf-entry.S normally needs the
upx-stubtools cross-compilers. On an amd64 host the entry stub can be rebuilt
with host gcc/objcopy/objdump by overriding the toolchain variables:
cd src/stub
make -f Makefile amd64-linux.elf-entry.h \
"tc.amd64-linux.elf.gcc=gcc -m64 -nostdinc -MMD -MT \$@" \
"tc.default.m-objcopy=objcopy" \
"tc.default.m-objdump=objdump" \
"tc.default.m-nm=nm" \
"tc.default.m-readelf=readelf"Identical to vanilla UPX:
# Pack a program
./upx --best program
# Unpack (UPX Ultimate only — vanilla UPX cannot unpack the custom magic)
./upx -d program
# List info about a packed file
./upx -l programTested end-to-end against a built binary:
- pack + run — normal exit code
upx -dround-trip — original exit code- hello-world (stdio) — prints and exits correctly
- anti-debug — launched under gdb →
SIGILL; underr2 -d→SIGNAL 4 (SIGILL) - anti-tamper — one byte patched in the stub code region (
0x31→0xCC, simulating a software breakpoint) →exit 132(SIGILL)
See README.PROTECTION.md for the full technical
details of the stub implementation.
These protections raise the cost of casual tampering and debugging, but — as with any protector — they are not unbeatable. A determined analyst with full control of the binary can always defeat runtime checks. Treat UPX Ultimate as a deterrent, not a guarantee. Pack and unpack only files you trust.
UPX Ultimate is a fork of UPX, distributed under
the same terms: GPLv2+, with the option of GPLv2+ plus special exceptions
permitting free use for all binaries including commercial programs. See
COPYING and LICENSE.
- Copyright (C) 1996–2026 Markus Franz Xaver Johannes Oberhumer
- Copyright (C) 1996–2026 Laszlo Molnar
- Copyright (C) 2000–2026 John F. Reiser
The anti-debug, anti-tamper, and custom pack-header magic enhancements in this fork are © 2026 MINHAJUL ISLAM.
[UPX is a shorthand for the Ultimate Packer for eXecutables and holds no connection with potential owners of registered trademarks or other rights.]