From 748c6a0538629abee5a95710d8bc8ef87db17e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 14 Mar 2021 13:15:44 +0000 Subject: [PATCH] obfuscate asm function names as well (#273) Historically, it was impossible to rename those funcs as the implementation was in assembly files, and we only transformed Go code. Now that transformAsm exists, it's only about fifty lines to do some very basic parsing and rewriting of assembly files. This fixes the obfuscated builds of multiple std packages, including a few dependencies of net/http, since they included assembly funcs which called pure Go functions. Those pure Go functions had their names obfuscated, breaking the call sites in assembly. Fixes #258. Fixes #261. --- README.md | 3 - main.go | 109 +++++++++++++++++++++++++-------- testdata/scripts/asm.txt | 5 +- testdata/scripts/goprivate.txt | 8 +-- 4 files changed, 88 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e74a3de..7b388eb 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,6 @@ to document the current shortcomings of this tool. * Exported methods are never obfuscated at the moment, since they could be required by interfaces and reflection. This area is a work in progress. -* Functions implemented outside Go, such as assembly, aren't obfuscated since we - currently only transform the input Go source. - * Go plugins are not currently supported; see [#87](https://github.com/burrowers/garble/issues/87). * There are cases where garble is a little too agressive with obfuscation, this may lead to identifiers getting obfuscated which are needed for reflection, e.g. to parse JSON into a struct; see [#162](https://github.com/burrowers/garble/issues/162). To work around this you can pass a hint to garble, that an type is used for reflection via passing it to `reflect.TypeOf` or `reflect.ValueOf` in the same file: diff --git a/main.go b/main.go index c1b556a..c5a2c33 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,8 @@ import ( "strconv" "strings" "time" + "unicode" + "unicode/utf8" "golang.org/x/mod/module" "golang.org/x/mod/semver" @@ -113,6 +115,10 @@ var ( envGoPrivate = os.Getenv("GOPRIVATE") // complemented by 'go env' later ) +// TODO(mvdan): now that we also obfuscate assembly funcs, we could likely get +// rid of obfuscatedTypesPackage and have a function that tells us if a +// *types.Func (from the original types.Package) should be obfuscated. + func obfuscatedTypesPackage(path string) *types.Package { entry, ok := importCfgEntries[path] if !ok { @@ -411,6 +417,11 @@ var transformFuncs = map[string]func([]string) (args []string, _ error){ } func transformAsm(args []string) ([]string, error) { + // If the current package isn't private, we have nothing to do. + if !curPkg.Private { + return args, nil + } + flags, paths := splitFlagsFromFiles(args, ".s") // When assembling, the import path can make its way into the output @@ -419,7 +430,76 @@ func transformAsm(args []string) ([]string, error) { flags = flagSetValue(flags, "-p", curPkg.obfuscatedImportPath()) } - return append(flags, paths...), nil + // We need to replace all function references with their obfuscated name + // counterparts. + // Luckily, all func names in Go assembly files are immediately followed + // by the unicode "middle dot", like: + // + // TEXT ·privateAdd(SB),$0-24 + const middleDot = '·' + middleDotLen := utf8.RuneLen(middleDot) + + newPaths := make([]string, 0, len(paths)) + for _, path := range paths { + + // Read the entire file into memory. + // If we find issues with large files, we can use bufio. + content, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + // Find all middle-dot names, and replace them. + remaining := content + var buf bytes.Buffer + for { + i := bytes.IndexRune(remaining, middleDot) + if i < 0 { + buf.Write(remaining) + remaining = nil + break + } + i += middleDotLen + + buf.Write(remaining[:i]) + remaining = remaining[i:] + + // The name ends at the first rune which cannot be part + // of a Go identifier, such as a comma or space. + nameEnd := 0 + for nameEnd < len(remaining) { + c, size := utf8.DecodeRune(remaining[nameEnd:]) + if !unicode.IsLetter(c) && c != '_' && !unicode.IsDigit(c) { + break + } + nameEnd += size + } + name := string(remaining[:nameEnd]) + remaining = remaining[nameEnd:] + + newName := hashWith(curPkg.GarbleActionID, name) + // log.Printf("%q hashed with %x to %q", name, curPkg.GarbleActionID, newName) + buf.WriteString(newName) + } + + // TODO: do the original asm filenames ever matter? + tempFile, err := ioutil.TempFile(sharedTempDir, "*.s") + if err != nil { + return nil, err + } + defer tempFile.Close() + + if _, err := tempFile.Write(buf.Bytes()); err != nil { + return nil, err + } + if err := tempFile.Close(); err != nil { + return nil, err + } + + newPaths = append(newPaths, tempFile.Name()) + } + + return append(flags, newPaths...), nil } func transformCompile(args []string) ([]string, error) { @@ -782,16 +862,6 @@ var runtimeRelated = map[string]bool{ "unsafe": true, "vendor/golang.org/x/net/dns/dnsmessage": true, "vendor/golang.org/x/net/route": true, - - // These packages call pure Go functions from assembly functions. - // We obfuscate the pure Go function name, breaking the assembly. - // We do not deal with that edge case just yet, so for now, - // never obfuscate these packages. - // TODO: remove once we fix issue 261. - "math/big": true, - "math/rand": true, - "crypto/sha512": true, - "crypto": true, } // isPrivate checks if a package import path should be considered private, @@ -1097,9 +1167,6 @@ func (tf *transformer) transformGo(file *ast.File) *ast.File { if obj.Exported() && sign.Recv() != nil { return true // might implement an interface } - if implementedOutsideGo(x) { - return true // give up in this case - } switch node.Name { case "main", "init", "TestMain": return true // don't break them @@ -1112,7 +1179,7 @@ func (tf *transformer) transformGo(file *ast.File) *ast.File { } obfPkg := obfuscatedTypesPackage(path) - // Check if the imported name wasn't garbled, e.g. if it's assembly. + // Check if the imported name wasn't garbled. // If the object returned from the garbled package's scope has a // different type as the object we're searching for, they are // most likely two separate objects with the same name, so ok to @@ -1149,18 +1216,6 @@ func recordStruct(named *types.Named, m map[types.Object]bool) { } } -// implementedOutsideGo returns whether a *types.Func does not have a body, for -// example when it's implemented in assembly, or when one uses go:linkname. -// -// Note that this function can only return true if the obj parameter was -// type-checked from source - that is, if it's the top-level package we're -// building. Dependency packages, whose type information comes from export data, -// do not differentiate these "external funcs" in any way. -func implementedOutsideGo(obj *types.Func) bool { - return obj.Type().(*types.Signature).Recv() == nil && - (obj.Scope() != nil && obj.Scope().End() == token.NoPos) -} - // named tries to obtain the *types.Named behind a type, if there is one. // This is useful to obtain "testing.T" from "*testing.T", or to obtain the type // declaration object from an embedded field. diff --git a/testdata/scripts/asm.txt b/testdata/scripts/asm.txt index 522d861..9f77c9e 100644 --- a/testdata/scripts/asm.txt +++ b/testdata/scripts/asm.txt @@ -3,18 +3,19 @@ env GOPRIVATE=test/main garble build exec ./main cmp stderr main.stderr -binsubstr main$exe 'privateAdd' 'PublicAdd' +! binsubstr main$exe 'privateAdd' 'PublicAdd' [short] stop # no need to verify this with -short garble -tiny build exec ./main cmp stderr main.stderr -binsubstr main$exe 'privateAdd' 'PublicAdd' +! binsubstr main$exe 'privateAdd' 'PublicAdd' go build exec ./main cmp stderr main.stderr +binsubstr main$exe 'privateAdd' 'PublicAdd' -- go.mod -- module test/main diff --git a/testdata/scripts/goprivate.txt b/testdata/scripts/goprivate.txt index 39c42bf..5259c19 100644 --- a/testdata/scripts/goprivate.txt +++ b/testdata/scripts/goprivate.txt @@ -19,7 +19,7 @@ env GOPRIVATE='*' # Note that we won't obfuscate a few std packages just yet, mainly those around runtime. garble build std -# Link a binary importing crypto/ecdsa, which will catch whether or not we +# Link a binary importing net/http, which will catch whether or not we # support ImportMap when linking. garble build -o=out ./stdimporter @@ -44,10 +44,8 @@ var Name = "value" -- stdimporter/main.go -- package main -import ( - "crypto/ecdsa" -) +import "net/http" func main() { - ecdsa.Verify(nil, nil, nil, nil) + http.ListenAndServe("", nil) }