Introduction

Binary hardening is the process of securing an application by reducing or removing possible attack surfaces. One of many attack surfaces is static analysis, a process in which an executable binary is taken and analyzed as a file without running the executable. One of the ways to make this more difficult is to use executable packing. Executable packing uses compression and sometimes encryption to make the binary file harder to analyze without changing the functionality of the application. In this article, we will detail a way to pack your executable that is native to Golang.

Go embeds

Using the //go:embed directive, we can have the go compiler include files from our filesystem in the resulting binary. We can include a single file as a byte slice by doing the following:

golang:
import _ "embed"

//go:embed file.txt
var b []byte

Go plugins

Go plugins are Golang applications built using the build flag -buildmode=plugin other go applications can then load this plugin and execute exported functions from the said plugin.

Packed application structure

To protect our application we can use a variety of ways which we will discuss later in this article. To make these protections work, we will use a wrapper application referred to as a package. This package will contain our original application built with -buildmode=plugin and will unpack it when the package is run.

Original application

To make our application suitable for packing, we have to make sure the functions we want to use are exported and can be used by our package application. We can make sure this is the case by using the //export FunctionName directive.

Obfuscation

If we use an obfuscator like garble, for example, we have to make sure the exported function mentioned in the previous section does not take any arguments that are not generic types like byte or string. Because all structs are obfuscated and thus will not have the same name in both the package and plugin. If it’s necessary to use structs, we can exclude them from the obfuscation process by using var _ = reflect.TypeOf(NamedStruct{}).

Protecting the original binary

To make static analysis on the packed binary more difficult, we can use encryption or compression before we embed it in our package application. You can also of course include data like an expiration date in this embedded data. Sadly because of the way the plugin package works it forces us to load the plugin from disk, so we have to briefly write it to disk. To make it harder to intercept, we can use some random file and directory names to confuse any potential attackers. We could potentially modify the Golang runtime, but this would be a lot of work. Just googling around, there doesn’t seem to be an easy way to do this except manually mapping your plugin library in memory and then resolving the functions contained within. This gain could potentially work with Golang, but that could be an entire article in and of itself, so for now we will use what has been given to us.

Loading the application

We recommend having a single entry point function to communicate between your package and plugin applications. This is because you can then obfuscate all other methods and have that one entry method do all the work inside the plugin application. When the package application is loaded, we can decrypt or decompress it and briefly write it to the disk. Then we can use plugin.Open and once it’s loaded, remove it from the disk. Of course, a potential attacker can also dump your application from memory, but there are plenty of ways to make that more difficult which are not the topic of this article. Then once it’s loaded we can use plugin.Lookup to find our method symbol and execute any functions.

The building process

To make it easier on yourself to test the application, you can make an automated script in go or for example python. This script can call the go compiler twice and handle all the potential encrypting and compression. Compression is recommended since this can dramatically decrease the size of your final application. Of course, this does come at the cost of some startup performance.

Additional protection

Because Golang has the option to include C code within, it is very easy to include some OS-specific anti-debugging techniques. Here you can find some awesome anti-debugging tricks. Do note that not all anti-debugging tricks may work because they require fatal handling and Golang switches to a different stack when issuing syscalls.

Resources