# Go Binary Obfuscation Toolkit A multi-layer Go binary obfuscation pipeline that combines source-code transformation, compiler-level obfuscation, and runtime protection to make reverse engineering prohibitively expensive. ## Overview The toolkit operates as a staged pipeline: ``` Source Code │ ▼ ┌─────────────────────────────────┐ │ Stage 1: Source Transformer │ Text-based source rewriting │ (cmd/transformer) │ 9 obfuscation passes └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ Stage 2: Garble Compilation │ garble -literals -tiny │ (mvdan.cc/garble) │ -trimpath -ldflags="-s -w" └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ Stage 3: Integrity Hash │ SHA-256 of .text / __text │ (cmd/inthash) │ section for tamper detection └─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────┐ │ Stage 4: Analysis │ Size, symbols, strings, │ (Makefile) │ secret leakage scan └─────────────────────────────────┘ ``` ## Quick Start ```bash # Prerequisites go install mvdan.cc/garble@latest # Build and compare normal vs obfuscated make compare # Run the obfuscated binary make run # Run just the transformer make transformer ``` ### `make compare` Output ``` Normal Obfuscated Size: 2707026 3063458 Symbols: 2864 58 Strings: 15335 4302 Secret leaks: 1 (LIC-*) 0 Function names: visible 0 (good) ``` ## Architecture ``` obfuscator/ ├── cmd/ │ ├── transformer/ CLI for source transformation │ ├── demoapp/ Target application with embedded secrets │ └── inthash/ Integrity hash computation tool ├── internal/ │ └── transform/ Core transformation engine (9 passes) ├── pkg/ │ ├── strenc/ AES-256-GCM encrypted string pool │ ├── stringcrypt/ XOR string encryption + random identifiers │ ├── vm/ Stack-based bytecode virtual machine │ ├── syscallobf/ Obfuscated syscall dispatch │ ├── integrity/ Binary .text section integrity verification │ ├── antidebug/ Anti-debugging / anti-hooking checks │ └── pageman/ Encrypted memory buffer management └── Makefile Full build pipeline ``` ## Obfuscation Layers ### 1. String Encryption (`pkg/strenc`) All string literals are replaced with `_decStr(index)` calls backed by an AES-256-GCM encrypted pool. **How it works:** - The transformer scans Go AST for string literals (skipping imports and directives) - Each plaintext is length-padded to 64-byte blocks with a 2-byte little-endian length prefix - A random 32-byte master seed is generated per build - Per-string IVs are generated; subkeys are derived via HMAC-SHA256(masterKey, IV) - Encryption uses AES-256-GCM with the derived subkey - The generated code includes `_masterSeed`, `_encEntries`, `_encPool`, and a `//go:noinline` `_decStr()` function - At runtime, `GetString(index)` decrypts on demand; the plaintext is never present as a static string **Key derivation (`pkg/strenc/keyderive.go`):** ``` seed ──► HMAC-SHA256(seed, "strenc.masterkey.v1") ──► first 16 bytes ──► HMAC-SHA256(seed, "strenc.masterkey.v2") ──► last 16 bytes ──► 32-byte master key ``` ### 2. Opaque Predicates Injects always-true conditional branches into function bodies at ~40% probability (after 3+ statements). **6 always-true expression variants:** - `(a*7) % 7 == 0` - `(a ^ a) == 0` - `^(a | a) == ^a` - `(a & a) == a` - `len([N]byte{}) == 0` - `(b + a) > (b + a - 1)` These are syntactically valid Go expressions that always evaluate to `true` but require symbolic execution or constant folding to prove. ### 3. Dead Code Injection Injects always-false conditional branches containing unreachable code at ~30% probability. **6 always-false expression variants:** - `(a - a) < 0` - `func() bool { return 1 < 0 }()` - `func() bool { var _x = a; return _x > _x }()` - `func() bool { var _x int = a; _x -= a; return _x > 0 }()` - `len([N]byte{}) < 0` - `func() bool { return (a^a) > 0 && (a^a) < 0 }()` ### 4. Bogus Control Flow Injects computationally expensive but semantically dead branches at ~25% probability. **4 bogus branch variants:** - Modulo arithmetic loop: `v := int64(1); for i := 0; i < 10; i++ { v = v*7%49999999 }; return v == 1` - Squared even check: `v := a*a; return v%2 == 0 && v > 0` - Byte array length: `v := [4]byte{0x72, 0x6F, 0x6F, 0x74}; return len(v) == 4` - Summation loop: `v := func() int { n := 0; for i := 1; i <= 100; i++ { n += i }; return n }()` ### 5. Indirect Dispatch Renames unexported functions and creates thin wrappers to obscure call targets. ```go // Before: func generateToken(userID string) string { ... } // After: func generateToken_obf4721(userID string) string { ... } // renamed func generateToken(userID string) string { // wrapper return generateToken_obf4721(userID) } ``` Skips `main`, `init`, exported functions, and `_`-prefixed functions. ### 6. Control-Flow Flattening Converts function bodies into a state-machine dispatcher pattern. ```go // Before: // After: func foo() { func foo() { a := 1 _stX := 0 b := a + 2 for { c := b * 3 switch _stX { return c case 0: a = 1; _stX = 1 } case 1: b = a + 2; _stX = 2 case 2: c = b * 3; _stX = 3 case 3: return c default: return } } } ``` **Safety checks:** - Only applies to functions with 5+ body lines - Excludes `main` and `init` functions - Pre-scan validates all `:=` assignments have inferrable types before hoisting - Validates variable names are valid Go identifiers (avoids misinterpreting injected opaque predicates) - Multi-var declarations (`a, b := ...`) are handled separately ### 7. Code Splicing Similar to flattening but targets functions with `return` statements, splitting them into 2-3 fragments joined by a state machine. ### 8. Build ID Polymorphism Injects a unique build identifier derived from the build seed: ```go var _buildID = "BUILD_1780185895842347000_A3F2B1C9" // polymorphic build marker var _ = _buildID ``` Every build with a different seed produces a structurally different binary, defeating signature-based detection. ### 9. Function Virtualization (`pkg/vm`) Replaces simple functions with a custom stack-based bytecode VM. **Supported patterns:** - `len(x) > N`, `len(x) == N`, `len(x) < N`, `len(x) >= N` - Two-param arithmetic: `a+b`, `a-b`, `a*b`, `a/b`, `a%b`, `a^b`, `a&b`, `a|b` - Single-param arithmetic: `a+N`, `a-N`, `a*N`, etc. **VM design (`pkg/vm/`):** - 28 opcodes (0x00-0x1B) + 4 junk opcodes (0x1C-0x1F) - Stack values XOR-masked with `0x5A5A5A5A5A5A5A5A` to resist memory analysis - Opcode dispatch key: `0xA5` — all opcodes XOR'd before encoding - Variable-length instruction encoding (1-9 bytes per instruction) - External callout mechanism: `PUSH args..., PUSH arity, CALL funcID` - Unknown opcodes silently consumed (breaks linear disassembly) **Opcodes:** | Opcode | Hex | Arg | Description | |--------|------|-------|-------------| | NOP | 0x00 | none | Anti-analysis padding | | PUSH | 0x01 | i64 | Push immediate | | POP | 0x02 | none | Discard top | | DUP | 0x03 | none | Duplicate top | | ADD | 0x04 | none | a + b | | SUB | 0x05 | none | a - b | | MUL | 0x06 | none | a * b | | DIV | 0x07 | none | a / b | | MOD | 0x08 | none | a % b | | XOR | 0x09 | none | a ^ b | | AND | 0x0A | none | a & b | | OR | 0x0B | none | a \| b | | NOT | 0x0C | none | ~a | | SHL | 0x0D | i8 | a << n | | SHR | 0x0E | i8 | a >> n | | CMP_EQ | 0x0F | none | a == b ? 1 : 0 | | CMP_LT | 0x10 | none | a < b ? 1 : 0 | | CMP_GT | 0x11 | none | a > b ? 1 : 0 | | JMP | 0x12 | i32 | Unconditional jump | | JZ | 0x13 | i32 | Jump if zero | | JNZ | 0x14 | i32 | Jump if not zero | | LOAD | 0x15 | i16 | Load variable | | STORE | 0x16 | i16 | Store variable | | CALL | 0x17 | i16 | Call external function | | RET | 0x18 | none | Return | | HALT | 0x19 | none | Stop execution | | SWAP | 0x1A | i8 | Swap stack positions | | ROT | 0x1B | none | Rotate top 3 | ## Runtime Protection Packages ### Syscall Obfuscation (`pkg/syscallobf`) Hides syscall numbers from static analysis using XOR-encoded dispatch. - **Encoding:** Each syscall number is XOR'd with a random `uint64` dispatch key generated once via `sync.Once` - **Linux (`syscall_linux.go`):** Wraps `ptrace` (101), `getpid` (39), `kill` (62), `open` (2), `close` (3), `read` (0), `lseek` (8), `mmap` (9), `mprotect` (10) via `syscall.RawSyscall` - **Darwin (`syscall_darwin.go`):** Returns `ENOSYS` stubs (platform-specific implementation pending) The anti-debugging package uses these for stealthy `/proc/self/mem` and `/proc/self/maps` access. ### Anti-Debugging (`pkg/antidebug`) **`Check()` runs all detection methods:** | Check | Platform | Description | |-------|----------|-------------| | `checkPtrace()` | Linux | Reads `/proc/self/status` for non-zero `TracerPid` | | `disableCoreDump()` | Linux | Writes "0" to `/proc/self/coredump_filter` | | `checkTiming()` | All | 1M loop iterations must complete in <200ms (detects single-stepping) | | `checkEnv()` | All | Checks for `GODEBUG`, `DELVE_LISTENER`, `DEBUGINFOD_URL`, `RR_LOG_FILE`, `_JAVA_OPTIONS`, `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES` | | `checkParentProcess()` | Linux | Reads `/proc//comm`, matches against 12 debugger names | | `checkFunctionPrologues()` | Linux | Reads `/proc/self/mem` at function addresses, checks for JMP hooks (0xE9, 0xEB, 0xFF 0x25) and INT3 breakpoints (0xCC) | | `scanForBreakpoints()` | Linux | Parses `/proc/self/maps`, reads executable regions, flags ≥8 consecutive 0xCC bytes | **Background goroutines:** - `AntiAttach()` — polls `checkPtrace()` every 500ms, exits with code 66 on detection - `AntiDump()` — re-disables core dumps every 2s All `/proc` reads use `syscallobf` raw syscalls to avoid `os.Open` hooks. ### Binary Integrity (`pkg/integrity`) Verifies the binary hasn't been tampered with at runtime. - **Linux:** SHA-256 of the `.text` ELF section (via `debug/elf`) - **macOS:** SHA-256 of the `__text` Mach-O section (via `debug/macho`) - **Fallback:** SHA-256 of the entire file **Usage:** ```go // After building, compute hash: // go run ./cmd/inthash ./mybinary integrity.SetExpectedHashHex("03b1b8341708322379195b1d4427fb15...") integrity.StartMonitor(30*time.Second, func(err error) { // tamper detected }) ``` `StartMonitor` spawns a goroutine that verifies the hash at the given interval (with random initial jitter). Exits with code 66 on mismatch. ### Encrypted Memory (`pkg/pageman`) `SecureBuffer` provides XOR-encrypted memory with automatic locking. **Features:** - Random 32-byte key per buffer - XOR encryption with per-byte key rotation (`rotateKey(b, pos) = b<>(8-shift)` where `shift = (pos*3+7)%8`) - `Access(fn)` — auto-unlock, execute callback with plaintext, auto-relock - `Close()` — zeros all data and key material - Thread-safe via `sync.Mutex` **Guard goroutine (`guard.go`):** - Watches multiple `SecureBuffer`s - `enforceAutoLock()` — re-locks buffers that exceeded their `autoLock` duration - `checkMapsTampering()` — compares `/proc/self/maps` snapshots; force-locks all buffers if the memory map changed (detects debugger attachment / memory mapping modifications) ## Transformer CLI ``` Usage: transformer -src -dst [flags] Flags: -src string Source directory to transform -dst string Destination directory for transformed source -strings Encrypt string literals (default true) -opaque Inject opaque predicates (default true) -deadcode Inject dead code (default true) -randomize Randomize declaration order (default true) -indirect Indirect function dispatch (default true) -bogus-cfg Bogus control-flow injection (default true) -flatten Control-flow flattening (default true) -vm Virtualize simple functions into VM bytecode (default true) -buildid Inject unique build ID constant (default true) -seed int Build seed for polymorphic output (0 = use current timestamp) ``` ## Build Pipeline The `Makefile` orchestrates the full pipeline: ```makefile # Stage 1: Source transformation transformer -src cmd/demoapp -dst build/obfuscated/demoapp -seed=$(SEED) # Stage 2: Garble compilation garble -literals -tiny build -buildvcs=false -trimpath \ -ldflags="-s -w -buildid=" -o build/demoapp-garbled ./build/obfuscated/demoapp # Stage 3: Integrity hash go run ./cmd/inthash build/demoapp-garbled # Stage 4: Analysis # - Binary size # - Symbol count (nm) # - String count (strings) # - Secret leakage scan # - Function name visibility ``` ## Demo Application `cmd/demoapp/main.go` contains intentionally leaked secrets for testing: ```go const ( apiKey = "sk-proj-FAKE-KEY-1234567890abcdef" secretVal = "super-secret-license-key-DO-NOT-SHARE" dbConnStr = "postgres://admin:P@ssw0rd!@db.internal:5432/production" jwtSecret = "my-jwt-signing-secret-2024" ) var licenseKey = "LIC-XXXX-YYYY-ZZZZ-PRODUCTION" ``` The `make compare` target verifies all of these are hidden in the obfuscated binary. ## Testing ```bash # Run all package tests go test ./pkg/... # VM tests (22 tests: arithmetic, bitwise, comparisons, jumps, callouts, determinism) go test ./pkg/vm/ -v # Syscall obfuscation tests go test ./pkg/syscallobf/ -v # Vet all source go vet ./... ``` ## Requirements - Go 1.26.3+ (darwin/arm64 or linux/amd64) - `garble` (`go install mvdan.cc/garble@latest`) - No external Go dependencies (stdlib only) ## Limitations - **Text-based transformation:** The transformer operates on source text, not Go AST. This limits what can be reliably transformed (e.g., type inference for variable hoisting is heuristic). - **Darwin syscall stubs:** `pkg/syscallobf` returns `ENOSYS` on macOS. Linux is the primary target for runtime protections. - **Garble required:** Stage 2 depends on `mvdan.cc/garble` for name mangling, literal encryption, and binary stripping. - **Simple function virtualization only:** The VM currently handles len-comparisons and arithmetic. Complex control flow is not virtualized.