Recently I’ve been playing around with Ultimate Packer for Executables (UPX) to reduce a distributable CLI application’s size.
The application is built and stored as an asset for multiple target platforms as a GitHub Release.
I started using UPX as a build step to pack the executable release binaries and it made a big difference in final output size. Important, as the GitHub Release assets cost money to store.
UPX has some great advantages. It supports many different executable formats, multiple types of compression (and a strong compression ratio), it’s performant when compressing and decompressing, and it supports runtime decompression. You can even plugin your own compression algorithm if you like. (Probably a reason that malware authors tend to leverage UPX for packing too).
In my case I had a Node.js application that was being bundled into an executable binary file using nexe. It is possible to compress / pack the Node.js executable before nexe combines it with your Node.js code using UPX. I saw a 30% improvement in size after using UPX.
UPX Packing Example
Let’s demonstrate UPX in action with a simple example.
Create a simple C application called hello.c that will print the string “Hello there.”:
#include "stdio.h" int main() { printf("Hello there.\n"); return 0; }
Compile the application using static linking with gcc:
gcc -static -o hello hello.c
Note the static linked binary size of your new hello executable (around 876 KB):
sean@DESKTOP-BAO9C6F:~/hello$ gcc -static -o hello hello.c sean@DESKTOP-BAO9C6F:~/hello$ ls -la total 908 drwxr-xr-x 2 sean sean 4096 Oct 24 21:27 . drwxr-xr-x 26 sean sean 4096 Oct 24 21:27 .. -rwxr-xr-x 1 sean sean 896336 Oct 24 21:27 hello -rw-r--r-- 1 sean sean 23487 Oct 21 21:33 hello.c sean@DESKTOP-BAO9C6F:~/hello$
This may be a paltry example, but we’ll take a look at the compression ratio achieved. This can of course, generally be extrapolated for larger file sizes.
Analysing our Executable Before Packing
Before we pack this 876 KB executable, let’s analyse it’s entropy using binwalk. The entropy will be higher in parts where the bytes of the file are more random.
Generate an entropy graph of hello with binwalk:
binwalk --entropy --save hello
The lower points of entropy should compress fairly well when upx packs the binary file.
UPX Packing
Finally, let’s pack the hello executable with UPX. We’ll choose standard lzma compression – it should be a ‘friendlier’ compression option for anti-virus packages to more widely support.
upx --best --lzma -o hello-upx hello
Look at that, a 31.49% compression ratio! Not bad considering the code itself is really small and most of the original hello executable size is a result of static linking.
sean@DESKTOP-BAO9C6F:~/hello$ upx --best --lzma -o hello-upx hello Ultimate Packer for eXecutables Copyright (C) 1996 - 2020 UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020 File size Ratio Format Name -------------------- ------ ----------- ----------- 871760 -> 274516 31.49% linux/amd64 hello-upx Packed 1 file. sean@DESKTOP-BAO9C6F:~/hello$
Running the packed binary still works perfectly fine. UPX cleverly re-arranges the binary file to place the compressed contents in a specific location, adds a new entrypoint and a bit of logic to decompress the data when the file is executed.
sean@DESKTOP-BAO9C6F:~/hello$ ./hello-upx Hello there.
UPX is a great option to pack / compress your files for distribution. It’s performant and supports many different executable formats, including Windows and 64-bit executables.
A great use case, as demonstrated in this post is to reduce executable size for binary distributions, especially when (for example) cloud storage costs, or download sizes are a concern.