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.
pull/274/head
Daniel Martí 3 years ago committed by GitHub
parent ffeea469e6
commit 748c6a0538
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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:

@ -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.

@ -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

@ -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)
}

Loading…
Cancel
Save