Skip to content

Commit

Permalink
obfuscate all names used in reflection
Browse files Browse the repository at this point in the history
Go code can retrieve and use field and method names via the `reflect` package.
For that reason, historically we did not obfuscate names of fields and methods
underneath types that we detected as used for reflection, via e.g. `reflect.TypeOf`.

However, that caused a number of issues. Since we obfuscate and build one package
at a time, we could only detect when types were used for reflection in their own package
or in upstream packages. Use of reflection in downstream packages would be detected
too late, causing one package to obfuscate the names and the other not to, leading to a build failure.

A different approach is implemented here. All names are obfuscated now, but we collect
those types used for reflection, and at the end of a build in `package main`,
we inject a function into the runtime's `internal/abi` package to reverse the obfuscation
for those names which can be used for reflection.

This does mean that the obfuscation for these names is very weak, as the binary
contains a one-to-one mapping to their original names, but they cannot be obfuscated
without breaking too many Go packages out in the wild. There is also some amount
of overhead in `internal/abi` due to this, but we aim to make the overhead insignificant.

Fixes #884, #799, #817, #881, #858, #843, #842

Closes #406
  • Loading branch information
lu4p committed Nov 27, 2024
1 parent 515358b commit 70d7267
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 97 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/
debug
2 changes: 1 addition & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Daniel Martí <mvdan@mvdan.cc>
Emmanuel Chee-zaram Okeke <ecokeke21@gmail.com>
NHAS <jordanatararimu@gmail.com>
Nicholas Jones <me@nicholasjon.es>
Paul Scheduikat <lu4p@pm.me>
Zachary Wasserman <zachwass2000@gmail.com>
lu4p <lu4p@pm.me>
pagran <pagran@protonmail.com>
shellhazard <shellhazard@tutanota.com>
xuannv <xuan11290@gmail.com>
16 changes: 0 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,23 +157,7 @@ to document the current shortcomings of this tool.
be required by interfaces. This area is a work in progress; see
[#3](https://github.com/burrowers/garble/issues/3).

* Garble automatically detects which Go types are used with reflection
to avoid obfuscating them, as that might break your program.
Note that Garble obfuscates [one package at a time](#speed),
so if your reflection code inspects a type from an imported package,
you may need to add a "hint" in the imported package to exclude obfuscating it:
```go
type Message struct {
Command string
Args string
}

// Never obfuscate the Message type.
var _ = reflect.TypeOf(Message{})
```

* Aside from `GOGARBLE` to select patterns of packages to obfuscate,
and the hint above with `reflect.TypeOf` to exclude obfuscating particular types,
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.

Expand Down
18 changes: 4 additions & 14 deletions hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,15 +223,7 @@ func entryOffKey() uint32 {
return runtimeHashWithCustomSalt([]byte("entryOffKey"))
}

func hashWithPackage(tf *transformer, pkg *listedPackage, name string) string {

// In some places it is not appropriate to access the transformer
if tf != nil {
// If the package is marked as "in-use" by reflection, the private structures are not obfuscated, so dont return them as a hash. Fixes #882
if _, ok := tf.curPkgCache.ReflectObjects[pkg.ImportPath+"."+name]; ok {
return name
}
}
func hashWithPackage(pkg *listedPackage, name string) string {
// If the user provided us with an obfuscation seed,
// we use that with the package import path directly..
// Otherwise, we use GarbleActionID as a fallback salt.
Expand Down Expand Up @@ -412,11 +404,9 @@ func hashWithCustomSalt(salt []byte, name string) string {
// Turn "afoo" into "Afoo".
b64Name[0] = toUpper(b64Name[0])
}
} else {
if isUpper(b64Name[0]) {
// Turn "Afoo" into "afoo".
b64Name[0] = toLower(b64Name[0])
}
} else if isUpper(b64Name[0]) {
// Turn "Afoo" into "afoo".
b64Name[0] = toLower(b64Name[0])
}
}
return string(b64Name)
Expand Down
84 changes: 49 additions & 35 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ func (tf *transformer) transformAsm(args []string) ([]string, error) {
newPaths := make([]string, 0, len(paths))
if !slices.Contains(args, "-gensymabis") {
for _, path := range paths {
name := hashWithPackage(tf, tf.curPkg, filepath.Base(path)) + ".s"
name := hashWithPackage(tf.curPkg, filepath.Base(path)) + ".s"
pkgDir := filepath.Join(sharedTempDir, tf.curPkg.obfuscatedImportPath())
newPath := filepath.Join(pkgDir, name)
newPaths = append(newPaths, newPath)
Expand Down Expand Up @@ -786,7 +786,7 @@ func (tf *transformer) transformAsm(args []string) ([]string, error) {
// directory, as assembly files do not support `/*line` directives.
// TODO(mvdan): per cmd/asm/internal/lex, they do support `#line`.
basename := filepath.Base(path)
newName := hashWithPackage(tf, tf.curPkg, basename) + ".s"
newName := hashWithPackage(tf.curPkg, basename) + ".s"
if path, err := tf.writeSourceFile(basename, newName, buf.Bytes()); err != nil {
return nil, err
} else {
Expand Down Expand Up @@ -902,7 +902,7 @@ func (tf *transformer) replaceAsmNames(buf *bytes.Buffer, remaining []byte) {
remaining = remaining[nameEnd:]

if lpkg.ToObfuscate && !compilerIntrinsics[lpkg.ImportPath][name] {
newName := hashWithPackage(tf, lpkg, name)
newName := hashWithPackage(lpkg, name)
if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed
log.Printf("asm name %q hashed with %x to %q", name, tf.curPkg.GarbleActionID, newName)
}
Expand Down Expand Up @@ -949,16 +949,39 @@ func (tf *transformer) writeSourceFile(basename, obfuscated string, content []by
// parseFiles parses a list of Go files.
// It supports relative file paths, such as those found in listedPackage.CompiledGoFiles,
// as long as dir is set to listedPackage.Dir.
func parseFiles(dir string, paths []string) ([]*ast.File, error) {
var files []*ast.File
func parseFiles(lpkg *listedPackage, dir string, paths []string) (files []*ast.File, err error) {
mainPackage := lpkg.Name == "main" && lpkg.ForTest == ""

for _, path := range paths {
if !filepath.IsAbs(path) {
path = filepath.Join(dir, path)
}
file, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution|parser.ParseComments)

var src any

if lpkg.ImportPath == "internal/abi" && filepath.Base(path) == "type.go" {
src, err = abiNamePatch(path)
if err != nil {
return nil, err
}
} else if mainPackage && reflectPatchFile == "" {
src, err = reflectMainPrePatch(path)
if err != nil {
return nil, err
}

reflectPatchFile = path
}

file, err := parser.ParseFile(fset, path, src, parser.SkipObjectResolution|parser.ParseComments)
if err != nil {
return nil, err
}

if mainPackage && src != nil {
astutil.AddNamedImport(fset, file, "_", "unsafe")
}

files = append(files, file)
}
return files, nil
Expand All @@ -972,7 +995,7 @@ func (tf *transformer) transformCompile(args []string) ([]string, error) {
flags = append(flags, "-dwarf=false")

// The Go file paths given to the compiler are always absolute paths.
files, err := parseFiles("", paths)
files, err := parseFiles(tf.curPkg, "", paths)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1085,6 +1108,10 @@ func (tf *transformer) transformCompile(args []string) ([]string, error) {
return nil, err
}

if tf.curPkg.Name == "main" && strings.HasSuffix(reflectPatchFile, basename) {
src = reflectMainPostPatch(src, tf.curPkg, tf.curPkgCache)
}

// We hide Go source filenames via "//line" directives,
// so there is no need to use obfuscated filenames here.
if path, err := tf.writeSourceFile(basename, basename, src); err != nil {
Expand Down Expand Up @@ -1140,7 +1167,7 @@ func (tf *transformer) transformDirectives(comments []*ast.CommentGroup) {
func (tf *transformer) transformLinkname(localName, newName string) (string, string) {
// obfuscate the local name, if the current package is obfuscated
if tf.curPkg.ToObfuscate && !compilerIntrinsics[tf.curPkg.ImportPath][localName] {
localName = hashWithPackage(tf, tf.curPkg, localName)
localName = hashWithPackage(tf.curPkg, localName)
}
if newName == "" {
return localName, ""
Expand Down Expand Up @@ -1208,29 +1235,26 @@ func (tf *transformer) transformLinkname(localName, newName string) (string, str

var newForeignName string
if receiver, name, ok := strings.Cut(foreignName, "."); ok {
if lpkg.ImportPath == "reflect" && (receiver == "(*rtype)" || receiver == "Value") {
// These receivers are not obfuscated.
// See the TODO below.
} else if strings.HasPrefix(receiver, "(*") {
if strings.HasPrefix(receiver, "(*") {
// pkg/path.(*Receiver).method
receiver = strings.TrimPrefix(receiver, "(*")
receiver = strings.TrimSuffix(receiver, ")")
receiver = "(*" + hashWithPackage(tf, lpkg, receiver) + ")"
receiver = "(*" + hashWithPackage(lpkg, receiver) + ")"
} else {
// pkg/path.Receiver.method
receiver = hashWithPackage(tf, lpkg, receiver)
receiver = hashWithPackage(lpkg, receiver)
}
// Exported methods are never obfuscated.
//
// TODO(mvdan): We're duplicating the logic behind these decisions.
// Reuse the logic with transformCompile.
if !token.IsExported(name) {
name = hashWithPackage(tf, lpkg, name)
name = hashWithPackage(lpkg, name)
}
newForeignName = receiver + "." + name
} else {
// pkg/path.function
newForeignName = hashWithPackage(tf, lpkg, foreignName)
newForeignName = hashWithPackage(lpkg, foreignName)
}

newPkgPath := lpkg.ImportPath
Expand Down Expand Up @@ -1308,7 +1332,7 @@ func (tf *transformer) processImportCfg(flags []string, requiredPkgs []string) (
// For beforePath="vendor/foo", afterPath and
// lpkg.ImportPath can be just "foo".
// Don't use obfuscatedImportPath here.
beforePath = hashWithPackage(tf, lpkg, beforePath)
beforePath = hashWithPackage(lpkg, beforePath)

afterPath = lpkg.obfuscatedImportPath()
}
Expand Down Expand Up @@ -1405,14 +1429,9 @@ type pkgCache struct {
// unless we were smart enough to detect which arguments get used as %#v or %T.
ReflectAPIs map[funcFullName]map[int]bool

// ReflectObjects is filled with the fully qualified names from each
// package that we cannot obfuscate due to reflection.
// The included objects are named types and their fields,
// since it is those names being obfuscated that could break the use of reflect.
//
// This record is necessary for knowing what names from imported packages
// weren't obfuscated, so we can obfuscate their local uses accordingly.
ReflectObjects map[objectString]struct{}
// ReflectObjectNames maps obfuscated names which are reflected to their "real"
// non-obfuscated names.
ReflectObjectNames map[objectString]string

// EmbeddedAliasFields records which embedded fields use a type alias.
// They are the only instance where a type alias matters for obfuscation,
Expand All @@ -1425,7 +1444,7 @@ type pkgCache struct {

func (c *pkgCache) CopyFrom(c2 pkgCache) {
maps.Copy(c.ReflectAPIs, c2.ReflectAPIs)
maps.Copy(c.ReflectObjects, c2.ReflectObjects)
maps.Copy(c.ReflectObjectNames, c2.ReflectObjectNames)
maps.Copy(c.EmbeddedAliasFields, c2.EmbeddedAliasFields)
}

Expand Down Expand Up @@ -1497,7 +1516,7 @@ func computePkgCache(fsCache *cache.Cache, lpkg *listedPackage, pkg *types.Packa
"reflect.TypeOf": {0: true},
"reflect.ValueOf": {0: true},
},
ReflectObjects: map[objectString]struct{}{},
ReflectObjectNames: map[objectString]string{},
EmbeddedAliasFields: map[objectString]typeName{},
}
for _, imp := range lpkg.Imports {
Expand Down Expand Up @@ -1530,7 +1549,7 @@ func computePkgCache(fsCache *cache.Cache, lpkg *listedPackage, pkg *types.Packa
// Missing or corrupted entry in the cache for a dependency.
// Could happen if GARBLE_CACHE was emptied but GOCACHE was not.
// Compute it, which can recurse if many entries are missing.
files, err := parseFiles(lpkg.Dir, lpkg.CompiledGoFiles)
files, err := parseFiles(lpkg, lpkg.Dir, lpkg.CompiledGoFiles)
if err != nil {
return err
}
Expand Down Expand Up @@ -1997,11 +2016,6 @@ func (tf *transformer) transformGoFile(file *ast.File) *ast.File {
}
}

// The package that declared this object did not obfuscate it.
if usedForReflect(tf.curPkgCache, obj) {
return true
}

lpkg, err := listPackage(tf.curPkg, path)
if err != nil {
panic(err) // shouldn't happen
Expand Down Expand Up @@ -2071,7 +2085,7 @@ func (tf *transformer) transformGoFile(file *ast.File) *ast.File {
return true // we only want to rename the above
}

node.Name = hashWithPackage(tf, lpkg, name)
node.Name = hashWithPackage(lpkg, name)
// TODO: probably move the debugf lines inside the hash funcs
if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed
log.Printf("%s %q hashed with %x… to %q", debugName, name, hashToUse[:4], node.Name)
Expand Down Expand Up @@ -2192,7 +2206,7 @@ func (tf *transformer) transformLink(args []string) ([]string, error) {
if path != "main" {
newPath = lpkg.obfuscatedImportPath()
}
newName := hashWithPackage(tf, lpkg, name)
newName := hashWithPackage(lpkg, name)
flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", newPath, newName, stringValue))
})

Expand Down
2 changes: 1 addition & 1 deletion position.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func printFile(lpkg *listedPackage, file *ast.File) ([]byte, error) {
newName := ""
if !flagTiny {
origPos := fmt.Sprintf("%s:%d", filename, origOffset)
newName = hashWithPackage(nil, lpkg, origPos) + ".go"
newName = hashWithPackage(lpkg, origPos) + ".go"
// log.Printf("%q hashed with %x to %q", origPos, curPkg.GarbleActionID, newName)
}

Expand Down
Loading

0 comments on commit 70d7267

Please sign in to comment.