Removing metadata from Go binaries

· 817 words · 4 minute read

The go compiler includes a lot of information about the system the binary was compiled on in the resulting executable. In some cases, this is not desirable; therefore, I will introduce several types of information embedded by the compiler and how to remove or replace them.

To visualize the following chapters, I created a simple go project via the go mod init metadata_example command with a singular module named logger:

1$ exa --tree
2.
3├── go.mod
4├── logger
5│  └── logger.go
6└── main.go

The main.go file contains the following call to the exported Print() function of the logger module:

1package main
2
3import "metadata_example/logger"
4
5func main() {
6	logger.Print("Hello World")
7}

The logger module contains as previously mentioned the Print() function:

1package logger
2
3import "fmt"
4
5func Print(s string) {
6	fmt.Printf("Printing: %s", s)
7}

Compiling the given source files via go build creates a binary called metadata_example which can now be examined with the gnu coreutils: strings, file and the go tool objdump.

Simply running strings on the metadata_example binary and filtering the output via ripgrep1 allows us to view paths to included modules and the name of the compiling user:

1$ strings metadata_example | rg "teo"
2/home/teo/programming/test/main.go
3/home/teo/programming/test/logger/logger.go

And the function we defined in the logger module:

1$ strings metadata_example | rg "logger"
2metadata_example/logger.Print
3/home/teo/programming/test/logger/logger.go
4metadata_example/logger..inittask

We can see the unique buildid the go compiler generated2 for this build using file:

1$ file metadata_example
2metadata_example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
3statically linked, Go
4BuildID=EeK2FDOWyG6IGg5mazlg/ENHyunFNhP8fxN7Y3QPV/12qIzndxNxBtjs7mhfY7/RRZcvOGD57KThfaaOCUV,
5with debug_info, not stripped

The binary contains debug information and can therefore be objdumped fairly easy:

1$ go tool objdump metadata_example | tail -n 26
2TEXT main.main(SB) /home/teo/programming/test/main.go
3  main.go:5             0x482820                493b6610                CMPQ 0x10(R14), SP
4  ....

Removing Metadata #

The following three types of information are fairly easy to remove: Each one will have an explanation and a removal example.

Removing Debug information ##

Technically the go tool chain includes the following information via the linker not the compiler. 3

omitting DWARF symbol table ###

The DWARF symbol table 4 is used for debugging. It can be stripped by passing the -w flag to the go linker.

1go build -ldflags="-w"

omitting symbol table and debug information ###

The go symbol table and the debug information embedded in the binary enable and enhance the usage of the gosym-package 5 and the objdump go tool 6. Omitting both the symbol table and debug information can be achieved by invoking the go linker with the -s flag.

1go build -ldflags="-s"

Running the go objdump tool on the resulting binary returns an error:

1$ go tool objdump metadata_example
2objdump: disassemble metadata_example: no symbol section

Trimming module paths ##

As showcased before, the compiled binary contains all the files included in the resulting binary. The go compiler exposes the -trimpath to strip common prefixes from these files, which almost always contain compromising information such as the name of the compiling user.

The compiler does the above when invoked with the flag, as follows:

1go build -trimpath

Using the flag when compiling results in the binary not including the absolute paths but project root relative paths:

1$ go build
2$ go tool objdump metadata_example | rg "main.main"
3TEXT main.main(SB) /home/teo/programming/test/main.go
4  main.go:5             0x482898                eb86                    JMP main.main(SB)
5$ go build -trimpath
6$ go tool objdump metadata_example | rg "main.main"
7TEXT main.main(SB) metadata_example/main.go
8  main.go:5             0x482898                eb86                    JMP main.main(SB)

Comparing the strings output from the start of this post, the binary now no longer contains the given example:

1$ strings metadata_example | rg "teo"
2$ strings metadata_example | rg "logger"
3metadata_example/logger.Print
4metadata_example/logger/logger.go

Replacing the buildid ##

The buildid can not be removed but it can be replaced with the -buildid linker flag:

1go build -ldflags="-buildid="

Which results in the following file output:

1$ go build -ldflags="-buildid="
2$ file metadata_example
3metadata_example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
4statically linked, with debug_info, not stripped

Compile command example ##

Putting all of the above together, we arrive at the following build command:

1go build -ldflags="-w -s -buildid=" -trimpath

This build does not include the DWARF symbol table, the go symbol table, debug information, the unique buildid or compromising file paths.

Difference in file size7:

1  33 go.mod
2   - logger
3  93 main.go
41.2M metadata_example
51.9M metadata_example_full

The metadata_example_full was build with go build and the metadata_example was build with all flags included in this post.

Further options ##

Stripping binaries ###

Its always possible to strip binaries using the gnu coreutils strip tool, this is however strongly advised against when stripping go binaries due to random panics.8

Obfuscation ###

There are several go modules out there for replacing strings with computations that evaluate to strings at runtime or replacing paths to modules with hashes. 9 10

These can be useful for creating more obscure binaries, which are hard to reverse engineer.