Packing Executable Files to Reduce Distribution Size with UPX

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
entropy analysis with binwalk before running upx to pack the executable.

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.