Skip to content

Commit

Permalink
This adds support for gomobile.
Browse files Browse the repository at this point in the history
Internally gomobile calls "go build". By modifying the PATH env var before gomobile is called, we can get gomobile to call garble instead of go.

When the user runs "garble mobile", the garble binary is copied to a temp directory and named "go". The temp directory is prepended to the PATH env var.
When gomobile calls "go build" it will actually be calling garble.

When garble is run, it will always check if the string "garble" is in the path. If the string "garble" is found, the temp dir will
be removed from the PATH in order to avoid an infinite loop.

This is how the call flow looks like:
"garble mobile" -> "gomobile" -> "go" (garble renamed in a temp dir) -> "go" (garble calls the real go binary)

This implementation only supports the gomobile commands "bind" and "clean". The reason being that "gomobile build" will result in gomobile immediately trying to read the symbols
as soon as the binary has been built, but it can't since the symbols have already been stripped during compilation.

Code blocking "gomobile build":
https://github.com/golang/mobile/blob/35478a0c49da882188b186a3893d45be6ff74327/cmd/gomobile/build_androidapp.go#L87
  • Loading branch information
Paul Hendricks committed Nov 20, 2023
1 parent 4271bc4 commit 9717b90
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/garble
/test
/bincmp_output/
.idea/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# garble

Install garble

go install mvdan.cc/garble@latest

Obfuscate Go code by wrapping the Go toolchain. Requires Go 1.20 or later.
Expand Down
55 changes: 55 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ Similarly, to combine garble flags and Go build flags:
The following commands are supported:
build replace "go build"
mobile replace "gomobile"
test replace "go test"
run replace "go run"
reverse de-obfuscate output such as stack traces
Expand Down Expand Up @@ -251,13 +252,23 @@ func main1() int {
if flagSeed.random {
fmt.Fprintf(os.Stderr, "-seed chosen at random: %s\n", base64.RawStdEncoding.EncodeToString(flagSeed.bytes))
}

executableName := filepath.Base(os.Args[0])
if executableName == "go" && isPassThroughCommand(args[0]) {
// This binary is being called by gomobile as "go".
// Redirect it to the real go binary since the command is not one
// that should be handled by garble.
return redirectToOgGo(os.Args[1:])
}

if err := mainErr(args); err != nil {
if code, ok := err.(errJustExit); ok {
return int(code)
}
fmt.Fprintln(os.Stderr, err)
return 1
}

return 0
}

Expand Down Expand Up @@ -318,6 +329,8 @@ garble was built with %q and is being used with %q; rebuild it with a command li
func mainErr(args []string) error {
command, args := args[0], args[1:]

resetPath()

// Catch users reaching for `go build -toolexec=garble`.
if command != "toolexec" && len(args) == 1 && args[0] == "-V=full" {
return fmt.Errorf(`did you run "go [command] -toolexec=garble" instead of "garble [command]"?`)
Expand Down Expand Up @@ -401,6 +414,7 @@ func mainErr(args []string) error {
case "reverse":
return commandReverse(args)
case "build", "test", "run":

cmd, err := toolexecCmd(command, args)
defer func() {
if err := os.RemoveAll(os.Getenv("GARBLE_SHARED")); err != nil {
Expand All @@ -425,6 +439,40 @@ func mainErr(args []string) error {
log.Printf("calling via toolexec: %s", cmd)
return cmd.Run()

case "mobile":

// ensure gomobile is found in PATH
_, err := exec.LookPath("gomobile")
if err != nil {
return errors.New("gomobile not found in PATH")
}

binDir, err := copyGarbleToTempDirAsGo()
if err != nil {
return fmt.Errorf("failed to copy garble to temp dir as go: %w", err)
}
defer os.RemoveAll(binDir)

goBinaryPath, err := exec.LookPath("go")
if err != nil {
return errors.New("go not found in PATH")
}

// This env var will be read when redirecting to the real go binary
os.Setenv(garbleOgGo, goBinaryPath)

// Add the tmp dir to the PATH env var so that when gomobile
// calls "go build", our binary will be called instead.
err = prependToPath(binDir)
if err != nil {
return fmt.Errorf("unable to modify PATH env var: %w", err)
}

cmd := exec.Command("gomobile", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()

case "toolexec":
_, tool := filepath.Split(args[0])
if runtime.GOOS == "windows" {
Expand Down Expand Up @@ -1977,6 +2025,13 @@ func (tf *transformer) transformGoFile(file *ast.File) *ast.File {
if !lpkg.ToObfuscate {
return true // we're not obfuscating this package
}

usingGoMobile := os.Getenv(garbleOgGo) != ""
// gobind (used by gomobile) requires that exported symbols of the main packages not be obfuscated.
if usingGoMobile && lpkg.Module.Main && node.IsExported() {
return true
}

hashToUse := lpkg.GarbleActionID
debugName := "variable"

Expand Down
129 changes: 129 additions & 0 deletions mobile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"errors"
"fmt"
"golang.org/x/exp/slices"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
)

// The path to the real go binary is stored under this key in environment
const garbleOgGo = "GARBLE_OG_GO"

// If go mobile calls this binary with a command other than "build" we need to call the real go binary.
func redirectToOgGo(args []string) int {

command := os.Getenv(garbleOgGo)

cmd := exec.Command(command, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
// exit with same code returned from cmd
return status.ExitStatus()
}
}
return 1
}

return 0
}

// Copy this binary to a temp dir, name it "go", and return the path
func copyGarbleToTempDirAsGo() (dir string, err error) {
pathToGarble, err := os.Executable()
if err != nil {
return
}
absPathToGarble, err := filepath.Abs(pathToGarble)
if err != nil {
return
}

dir, err = copyFileToTmp(absPathToGarble, "go")
if err != nil {
return
}

// Add execute permissions
err = os.Chmod(filepath.Join(dir, "go"), 0700)

return
}

// The copied file will have newName as its name.
func copyFileToTmp(src, newName string) (tmpDir string, err error) {

sourceFile, err := os.Open(src)
if err != nil {
return "", fmt.Errorf("unable to open source file: %w", err)
}
defer sourceFile.Close()

// It is important that "garble" is part of the name as the "resetPath" function
// looks for the string "garble"
tmpDir, err = os.MkdirTemp("", "garble")
if err != nil {
return "", fmt.Errorf("unable to create temp directory: %w", err)
}

destPath := filepath.Join(tmpDir, newName)
destFile, err := os.Create(destPath)
if err != nil {
return "", fmt.Errorf("unable to create destination file: %w", err)
}
defer destFile.Close()

if _, err := io.Copy(destFile, sourceFile); err != nil {
return "", fmt.Errorf("failed to copy file contents: %w", err)
}

return tmpDir, nil
}

// If "garble mobile" was called previously, it modified our path. We want to reset it so that we will call
// the real go binary instead of our redirection binary.
func resetPath() {
path := os.Getenv("PATH")
if !strings.Contains(path, "garble") {
return
}
path = trimUntilChar(path, os.PathListSeparator)
os.Setenv("PATH", path)
}

func trimUntilChar(s string, c rune) string {
index := strings.IndexRune(s, c)
if index == -1 {
return s // Character not found, return the original string
}
return s[index+1:] // Slice the string from the character onwards
}

func prependToPath(dir string) error {
// Normalize based on OS
dir = filepath.FromSlash(dir)

path := os.Getenv("PATH")

// Append the directory to the PATH
path = dir + string(os.PathListSeparator) + path

return os.Setenv("PATH", path)
}

// When this binary is called as "go" by gomobile, if the command is not in the slice below,
// we want to exec the real go binary.
func isPassThroughCommand(cmd string) bool {
return !slices.Contains([]string{"build", "toolexec"}, cmd)
}
3 changes: 3 additions & 0 deletions shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ type listedPackage struct {
CompiledGoFiles []string
IgnoredGoFiles []string
Imports []string
Module struct {
Main bool // true if is a package from the project (not an external dep)
}

Error *packageError // to report package loading errors to the user

Expand Down

0 comments on commit 9717b90

Please sign in to comment.