garble Documentation

Go binary & build repository by @burrowers. AI agent rules & system prompts with 5.8k stars.

5,784 stars Go Go 2 files ยท ~5,463 tokens #binary#build#code-obfuscator#golang#obfuscation
RAW Doc

Technical Documentation: burrowers/garble

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

garble

text
go install mvdan.cc/garble@latest # or @master

Obfuscate Go code by wrapping the Go toolchain. Requires Go 1.27 or later.

text
garble build [build flags] [packages]

The tool also supports garble test to run tests with obfuscated code,
garble run to obfuscate and execute simple programs,
garble reverse to de-obfuscate text such as stack traces,
and garble bug to file a pre-filled bug report.
Run garble -h to see all available commands and flags.

Purpose

Produce a binary that works as well as a regular build, but that has as little
information about the original source code as possible.

The tool is designed to be:

  • Coupled with cmd/go, to support modules and build caching
  • Deterministic and reproducible, given the same initial source code
  • Reversible given the original source, to de-obfuscate panic stack traces

Mechanism

The tool wraps calls to the Go compiler and linker to transform the Go build, in
order to:

The tool obfuscates all supported packages being built, including the standard runtime.
The unsupported runtime/cgo and crypto/internal/fips140 packages are excluded.
Runtime obfuscation follows Go's supported GOOS/GOARCH targets; Garble does not
maintain a narrower architecture allowlist.

Note that commands like garble build will use the go version found in your
$PATH. To use different versions of Go, you can use GOTOOLCHAIN.

Use cases

A common question is why a code obfuscator is needed for Go, a compiled language.
Go binaries include a surprising amount of information about the original source;
even with debug information and symbol tables stripped, many names and positions
remain in place for the sake of traces, reflection, and debugging.

Some use cases for Go require sharing a Go binary with the end user.
If the source code for the binary is private or requires a purchase,
its obfuscation can help discourage reverse engineering.

A similar use case is a Go library whose source is private or purchased.
Since Go libraries cannot be imported in binary form, and Go plugins
have their shortcomings,
sharing obfuscated source code becomes an option.
See #369.

Obfuscation can also help with aspects entirely unrelated to licensing.
For example, the -tiny flag can make binaries 15% smaller,
similar to the common practice in Android to reduce app sizes.
Obfuscation has also helped some open source developers work around
anti-virus scans incorrectly treating Go binaries as malware.

Literal obfuscation

Using the -literals flag causes literal expressions such as strings to be
replaced with more complex expressions, resolving to the same value at run-time.
String literals injected via -ldflags=-X are also replaced by this flag.
This feature is opt-in, as it can cause slow-downs depending on the input code.

Injected strings use the same size limits as source literals. Variables in the
runtime and its dependencies are not covered. With -literals, -ldflags must
be unpatterned; build commands separately if they need different injected values.

Literals used in constant expressions cannot be obfuscated, since they are
resolved at compile time. This includes any expressions part of a const
declaration, for example.

Note that this process can be reversed given enough effort;
see #984.

Embedded file obfuscation

The -literals flag also obfuscates files included with //go:embed.
It supports string, []byte, and embed.FS, including named string and
byte-slice types. Garble leaves the original files untouched.

Contents are decoded at run time:

  • Embedded string and byte-slice variables are decoded during package initialization.
  • Files in an embed.FS are decoded each time they are opened. Paths, directory
    entries, file sizes, seeking, and ReadAt behavior are unchanged.

Embedded variables use the same obfuscation algorithms as source literals.
Values larger than 2 KiB and embed.FS contents share a simpler decoder that
scales to large files. Decoding adds runtime work; opening files in an embed.FS
also allocates temporary buffers.

Embedded filenames remain visible. This is obfuscation, not encryption:
the executable contains the decoding key, so its contents can still be recovered.

Tiny mode

With the -tiny flag, even more information is stripped from the Go binary.
Position information is removed entirely, rather than being obfuscated.
Runtime code which prints panics, fatal errors, and trace/debug info is removed.
Many symbol names are also omitted from binary sections at link time.
All in all, this can make binaries about 15% smaller.

With this flag, no panics or fatal runtime errors will ever be printed, but they
can still be handled internally with recover as normal.

Note that this flag can make debugging crashes harder, as a panic will simply
exit the entire program without printing a stack trace, and source code
positions and many names are removed.
Similarly, garble reverse is generally not useful in this mode.

Control flow obfuscation

See: CONTROLFLOW.md

Speed

garble build should take about twice as long as go build, as it needs to
complete two builds. The original build, to be able to load and type-check the
input code, and then the obfuscated build.

Garble obfuscates one package at a time, mirroring how Go compiles one package
at a time. This allows Garble to fully support Go's build cache; incremental
garble build calls should only re-build and re-obfuscate modified code.

Note that the first call to garble build may be comparatively slow,
as it has to obfuscate each package for the first time. This is akin to clearing
GOCACHE with go clean -cache and running a go build from scratch.

Garble also makes use of its own cache to reuse work, akin to Go's GOCACHE.
It defaults to a directory under your user's cache directory,
such as ~/.cache/garble, and can be placed elsewhere by setting GARBLE_CACHE.

Determinism and seeds

Just like Go, garble builds are deterministic and reproducible in nature.
This has significant benefits, such as caching builds and being able to use
garble reverse to de-obfuscate stack traces.

By default, garble will obfuscate each package in a unique way,
which will change if its build input changes: the version of garble, the version
of Go, the package's source code, or any build parameter such as GOOS or -tags.
This is a reasonable default since guessing those inputs is very hard.

You can use the -seed flag to provide your own obfuscation randomness seed.
Reusing the same seed can help produce the same code obfuscation,
which can help when debugging or reproducing problems.
Regularly rotating the seed can also help against reverse-engineering in the long run,
as otherwise one can look at changes in how Go's standard library is obfuscated
to guess when the Go or garble versions were changed across a series of builds.

To always use a different seed for each build, use -seed=random.
Note that extra care should be taken when using custom seeds:
if a -seed value used in a build is lost, garble reverse will not work.

Caveats

Most of these can improve with time and effort. The purpose of this section is
to document the current shortcomings of this tool.

  • Exported methods are never obfuscated at the moment, since they could
    be required by interfaces. This area is a work in progress; see
    #3.

  • There is no supported way to exclude obfuscating a selection of files or packages.
    More often than not, a user would want to do this to work around a bug; please file the bug instead.

  • Go programs are initialized one package at a time,
    where imported packages are always initialized before their importers,
    and otherwise they are initialized in the lexical order of their import paths.
    Since garble obfuscates import paths, this lexical order may change arbitrarily.

  • Go plugins are not currently supported; see #87.

  • Garble requires git to patch the linker. That can be avoided once go-gitdiff
    supports non-strict patches.

  • APIs like runtime.GOROOT
    and runtime/debug.ReadBuildInfo
    will not work in obfuscated binaries. This can affect
    loading timezones, for example.

Contributing

We welcome new contributors. If you would like to contribute, see
CONTRIBUTING.md as a starting point.

Please file an issue before sending a PR, whether it is a bug report or a
feature request, so that we can agree on the problem and its solution first.


CONTROLFLOW (`docs/CONTROLFLOW.md`)

Control Flow Obfuscation

This feature is experimental. To enable it, set the environment variable GARBLE_EXPERIMENTAL_CONTROLFLOW=1

Mechanism

Control flow obfuscation works in several stages:

  1. Collect functions with //garble:controlflow comment
  2. Converts go/ast representation to go/ssa
  3. Applies block splitting
  4. Generates junk jumps
  5. Applies control flow flattening
  6. Applies (if enabled) control flow hardening
  7. Generates trash blocks
  8. Converts go/ssa back into go/ast

Example usage

go
// Obfuscate with defaults parameters
//garble:controlflow
func main() {
	println("Hello world!")
}


// Obfuscate with maximum parameters
//garble:controlflow block_splits=max junk_jumps=max flatten_passes=max
func main() {
    println("Hello world!")
}

Parameter explanation

Unlike other garble features (which just work), we recommend that you understand how parameters affect control flow obfuscation and which caveats exist.

Code snippets below without name obfuscation, for better readability.

Block splitting

Parameter: block_splits (default: 0)

Warning: this param affects resulting binary only when used in combination with flattening

Block splitting splits the largest SSA block into 2 parts of random size, this is done block_splits times at most. If there is no more suitable block to split (number of ssa instructions in block is less than 3), splitting stops.

This param is very useful if your code has few branches (if, switch etc.).

Input:

go
package main

// Note that the block_splits value is "max", so code blocks are split into the smallest possible blocks.
//garble:controlflow flatten_passes=0 junk_jumps=0 block_splits=max
func main() {
	println("1")
	println("2")
	println("3")
	println("4")
	println("5")
}

Result:

go
func main() {
	{
		println("1")
		goto _s2a_l3
	}
_s2a_l1:
	{
		println("3")
		goto _s2a_l4
	}
_s2a_l2:
	{
		println("5")
		return
	}
_s2a_l3:
	{
		println("2")
		goto _s2a_l1
	}
_s2a_l4:
	{
		println("4")
		goto _s2a_l2
	}
}

Junk jumps

Parameter: junk_jumps (default: 0, maximum: 1024)

Warning: this param affects resulting binary only when used in combination with flattening

Junk jumps adds jumps to random blocks junk_jumps times, these inserted jumps can also form a chain. This param is useful for linearly increasing the functions complexity.

Input:

go
//garble:controlflow flatten_passes=0 junk_jumps=5 block_splits=0
func main() {
	if true {
		println("hello world")
	}
}

Result:

go
func main() {
	{
		if true {
			goto _s2a_l4
		} else {
			goto _s2a_l2
		}
	}
_s2a_l1:
	{
		println("hello world")
		goto _s2a_l7
	}
_s2a_l2:
	{
		return
	}
_s2a_l3:
	{
		goto _s2a_l2
	}
_s2a_l4:
	{
		goto _s2a_l6
	}
_s2a_l5:
	{
		goto _s2a_l3
	}
_s2a_l6:
	{
		goto _s2a_l1
	}
_s2a_l7:
	{
		goto _s2a_l5
	}
}

Control flow flattening

Parameter: flatten_passes (default: 1, maximum: 4)

This parameter completely flattens the control flow flatten_passes times, which makes analysing the logic of the function very difficult
This is the most important parameter without which the other parameters have no effect on the resulting binary.

Warning: Unlike junk jumps, this parameter increases control flow complexity non-linearly. In most cases we do not recommend specifying a value greater than 3. Check Benchmark

Input:

go
//garble:controlflow flatten_passes=1 junk_jumps=0 block_splits=0
func main() {
	if true {
		println("hello world")
	} else {
		println("not hello world")
	}
}

Result:

go
func main() {
	var _s2a_0 int
_s2a_l0:
	{
		goto _s2a_l2
	}
_s2a_l1:
	{
		println("not hello world")
		goto _s2a_l6
	}
_s2a_l2:
	{
		_s2a_1 := _s2a_0 == (int)(1)
		if _s2a_1 {
			goto _s2a_l9
		} else {
			goto _s2a_l8
		}
	}
_s2a_l3:
	{
		_s2a_2 := _s2a_0 == (int)(4)
		if _s2a_2 {
			goto _s2a_l4
		} else {
			goto _s2a_l5
		}
	}
_s2a_l4:
	{
		return
	}
_s2a_l5:
	{
		if true {
			goto _s2a_l7
		} else {
			goto _s2a_l11
		}
	}
_s2a_l6:
	{
		_s2a_0 = (int)(4)
		goto _s2a_l0
	}
_s2a_l7:
	{
		_s2a_0 = (int)(1)
		goto _s2a_l0
	}
_s2a_l8:
	{
		_s2a_3 := _s2a_0 == (int)(2)
		if _s2a_3 {
			goto _s2a_l1
		} else {
			goto _s2a_l12
		}
	}
_s2a_l9:
	{
		println("hello world")
		goto _s2a_l10
	}
_s2a_l10:
	{
		_s2a_0 = (int)(3)
		goto _s2a_l0
	}
_s2a_l11:
	{
		_s2a_0 = (int)(2)
		goto _s2a_l0
	}
_s2a_l12:
	{
		_s2a_4 := _s2a_0 == (int)(3)
		if _s2a_4 {
			goto _s2a_l4
		} else {
			goto _s2a_l3
		}
	}
}

Control flow hardening

Parameter: flatten_hardening (default: empty, supported: xor,delegate_table)

Dispatcher is the main and most vulnerable part of control flow flattening. By static analysis of the dispatcher, it is possible to reconstruct the original control flow (example). Hardening can be used to make this analysis more difficult by adding an extra layer of obfuscation and moving some of the computation to runtime

Input:

go
//garble:controlflow flatten_passes=1 junk_jumps=0 block_splits=0 flatten_hardening=xor,delegate_table
func main() {
    delegateTableExample := func() {
        if true {
            println("hello world")
        } else {
            println("not hello world")
        }
    }
    xorHardeningExample := func() {
        if true {
            println("hello world")
        } else {
            println("not hello world")
        }
    }
    xorHardeningExample()
    delegateTableExample()
}

Result:

text
/* Detailed source-code truncated for AI context efficiency. */

Trash blocks

Parameter: trash_blocks (default: 0, maximum: 1024)

Warning: this param affects resulting binary only when used in combination with flattening

Trash blocks generator generates blocks that will never be called. Trash blocks contain random function calls and random variable assignments. The purpose of this is to create a large number of references to different methods and local variables and in combination with other controlflow obfuscation parameters it helps to effectively hide the real code.

The generator does not add new dependencies to the project, it uses only existing direct or indirect dependencies. In the following example, the fmt package implicitly imports the io and os packages
Input:

go
package main

import "fmt"

//garble:controlflow block_splits=0 junk_jumps=0 flatten_passes=0 trash_blocks=1
func main() {
	if true {
		fmt.Println("hello world")
	}
}

Result:

go
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	var (
		_s2a_4 int
		_s2a_5 bool
	)
	{
		if true {
			goto _s2a_l1
		} else {
			goto _s2a_l2
		}
	}
_s2a_l1:
	{
		_s2a_0 := new([1]interface {
		})
		_s2a_1 := &_s2a_0[(int)(0)]
		_s2a_2 := (interface {
		})("hello world")
		*_s2a_1 = _s2a_2
		_s2a_3 := _s2a_0[:]
		_, _ = fmt.Println(_s2a_3...)
		_s2a_4 = (int)(1375793722)
		goto _s2a_l3
	}
_s2a_l2:
	{
		return
	}
_s2a_l3:
	{
		_s2a_5 = _s2a_4 == (int)(1414729372)
		if _s2a_5 {
			goto _s2a_l4
		} else {
			goto _s2a_l2
		}
	}
_s2a_l4:
	{
		_s2a_5, _s2a_4 = true, _s2a_4
		_garble1v7i062eba5j0, _ := fmt.Printf((string)(_s2a_4), _s2a_4, _s2a_5)
		_garble27oahvink0hig, _ := fmt.Println(_garble1v7i062eba5j0)
		_ = fmt.Errorf((string)(_garble27oahvink0hig), os.Stdout, 714272279, _garble27oahvink0hig, io.EOF)
		_garble27oahvink0hig, _s2a_4, _garble1v7i062eba5j0, _s2a_5 = _s2a_4, (int)(77), _s2a_4, _s2a_5
		_, _garble6oknljnlggutn := fmt.Printf((string)(_garble1v7i062eba5j0))
		_, _garble8ne429queuq8n := fmt.Scanf((string)(751648516), os.Stdin, 0.17251639929485216, "UAN4E===")
		_garble1qt8quedh1fo9, _ := fmt.Scanf((string)(_s2a_4), _garble8ne429queuq8n, _s2a_5, _garble6oknljnlggutn)
		_, _ = fmt.Scanf((string)(_garble1qt8quedh1fo9))
		goto _s2a_l4
	}
}

Caveats

Complexity benchmark

We approximate complexity by counting the number of blocks.

Analysed function:

go
func sort(arr []int) []int {
	for i := 0; i < len(arr)-1; i++ {
		for j := 0; j < len(arr)-i-1; j++ {
			if arr[j] > arr[j+1] {
				arr[j], arr[j+1] = arr[j+1], arr[j]
			}
		}
	}
	return arr
}

Before obfuscation this function has 8 blocks.

flatten_passes block_splits junk_jumps block_count
1 0 0 32
1 10 0 62
1 100 0 95
1 1024 0 95
1 0 10 62
1 10 10 92
1 100 10 125
1 1024 10 125
1 0 100 332
1 10 100 362
1 100 100 395
1 1024 100 395
2 0 0 123
2 10 0 233
2 100 0 354
2 1024 0 354
2 0 10 233
2 10 10 343
2 100 10 464
2 1024 10 464
2 0 100 1223
2 10 100 1333
2 100 100 1454
2 1024 100 1454
3 0 0 486
3 10 0 916
3 100 0 1389
3 1024 0 1389
3 0 10 916
3 10 10 1346
3 100 10 1819
3 1024 10 1819
3 0 100 4786
3 10 100 5216
3 100 100 5689
3 1024 100 5689
4 0 0 1937
4 10 0 3647
4 100 0 5528
4 1024 0 5528
4 0 10 3647
4 10 10 5357
4 100 10 7238
4 1024 10 7238
4 0 100 19037
4 10 100 20747
4 100 100 22628
4 1024 100 22628

--- METRICS ---