From 7c2866356f1635a527b529986321d6cfe861d023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 9 Oct 2022 19:13:18 +0100 Subject: [PATCH] support obfuscating the syscall package One more package that further unblocks obfuscating the runtime. The issue was the TODO we already had about go:linkname directives with just one argument, which are used in the syscall package. While here, factor out the obfuscation of linkname directives into transformLinkname, as it was starting to get a bit complex. We now support debug logging as well, while still being able to use "early returns" for some cases where we bail out. We also need listPackage to treat all runtime sub-packages like it does runtime itself, as `runtime/internal/syscall` linknames into `syscall` without it being a dependency as well. Finally, add a regression test that, without the fix, properly spots that the syscall package was not obfuscated: FAIL: testdata/script/gogarble.txtar:41: unexpected match for ["syscall.RawSyscall6"] in out Updates #193. --- main.go | 112 +++++++++++++++++++-------------- shared.go | 6 +- testdata/script/gogarble.txtar | 10 +++ 3 files changed, 76 insertions(+), 52 deletions(-) diff --git a/main.go b/main.go index 7e3860b..119f120 100644 --- a/main.go +++ b/main.go @@ -888,9 +888,7 @@ func transformCompile(args []string) ([]string, error) { } // Uncomment for some quick debugging. Do not delete. - // if curPkg.ToObfuscate { - // fmt.Fprintf(os.Stderr, "\n-- %s/%s --\n%s", curPkg.ImportPath, filename, src) - // } + // fmt.Fprintf(os.Stderr, "\n-- %s/%s --\n%s", curPkg.ImportPath, filename, src) if path, err := writeTemp(filename, src); err != nil { return nil, err @@ -933,62 +931,80 @@ func (tf *transformer) handleDirectives(comments []*ast.CommentGroup) { if !strings.HasPrefix(comment.Text, "//go:linkname ") { continue } + + // We can have either just one argument: + // + // //go:linkname localName + // + // Or two arguments, where the second may refer to a name in a + // different package: + // + // //go:linkname localName newName + // //go:linkname localName pkg.newName fields := strings.Fields(comment.Text) - if len(fields) != 3 { - // TODO: the 2nd argument is optional, handle when it's not present - continue + localName := fields[1] + newName := "" + if len(fields) == 3 { + newName = fields[2] } - // This directive has two arguments: "go:linkname localName newName" - // obfuscate the local name, if the current package is obfuscated - if curPkg.ToObfuscate { - fields[1] = hashWithPackage(curPkg, fields[1]) + localName, newName = tf.transformLinkname(localName, newName) + fields[1] = localName + if len(fields) == 3 { + fields[2] = newName } - // If the new name is of the form "pkgpath.Name", and - // we've obfuscated "Name" in that package, rewrite the - // directive to use the obfuscated name. - newName := fields[2] - dotCnt := strings.Count(newName, ".") - if dotCnt < 1 { - // cgo-generated code uses linknames to made up symbol names, - // which do not have a package path at all. - // Replace the comment in case the local name was obfuscated. - comment.Text = strings.Join(fields, " ") - continue - } - switch newName { - case "main.main", "main..inittask", "runtime..inittask": - // The runtime uses some special symbols with "..". - // We aren't touching those at the moment. - continue + if flagDebug { // TODO(mvdan): remove once https://go.dev/issue/53465 if fixed + log.Printf("linkname %q changed to %q", comment.Text, strings.Join(fields, " ")) } + comment.Text = strings.Join(fields, " ") + } + } +} - // If the package path has multiple dots, split on the - // last one. - lastDotIdx := strings.LastIndex(newName, ".") - pkgPath, name := newName[:lastDotIdx], newName[lastDotIdx+1:] +func (tf *transformer) transformLinkname(localName, newName string) (string, string) { + // obfuscate the local name, if the current package is obfuscated + if curPkg.ToObfuscate { + localName = hashWithPackage(curPkg, localName) + } + if newName == "" { + return localName, "" + } + // If the new name is of the form "pkgpath.Name", and we've obfuscated + // "Name" in that package, rewrite the directive to use the obfuscated name. + dotCnt := strings.Count(newName, ".") + if dotCnt < 1 { + // cgo-generated code uses linknames to made up symbol names, + // which do not have a package path at all. + // Replace the comment in case the local name was obfuscated. + return localName, newName + } + switch newName { + case "main.main", "main..inittask", "runtime..inittask": + // The runtime uses some special symbols with "..". + // We aren't touching those at the moment. + return localName, newName + } - lpkg, err := listPackage(pkgPath) - if err != nil { - // Probably a made up name like above, but with a dot. - comment.Text = strings.Join(fields, " ") - continue - } - if lpkg.ToObfuscate { - // The name exists and was obfuscated; obfuscate - // the new name. - newName := hashWithPackage(lpkg, name) - newPkgPath := pkgPath - if pkgPath != "main" { - newPkgPath = lpkg.obfuscatedImportPath() - } - fields[2] = newPkgPath + "." + newName - } + // If the package path has multiple dots, split on the last one. + lastDotIdx := strings.LastIndex(newName, ".") + pkgPath, foreignName := newName[:lastDotIdx], newName[lastDotIdx+1:] - comment.Text = strings.Join(fields, " ") + lpkg, err := listPackage(pkgPath) + if err != nil { + // Probably a made up name like above, but with a dot. + return localName, newName + } + if lpkg.ToObfuscate { + // The name exists and was obfuscated; obfuscate the new name. + newForeignName := hashWithPackage(lpkg, foreignName) + newPkgPath := pkgPath + if pkgPath != "main" { + newPkgPath = lpkg.obfuscatedImportPath() } + newName = newPkgPath + "." + newForeignName } + return localName, newName } // processImportCfg parses the importcfg file passed to a compile or link step. diff --git a/shared.go b/shared.go index 9a4b78d..1635260 100644 --- a/shared.go +++ b/shared.go @@ -289,9 +289,6 @@ func appendListedPackages(packages []string, withDeps bool) error { // // TODO: investigate and resolve each one of these var cannotObfuscate = map[string]bool{ - // "//go:linkname must refer to declared function or variable" - "syscall": true, - // "unknown pc" crashes on windows in the cgo test otherwise "runtime/cgo": true, @@ -333,7 +330,8 @@ func listPackage(path string) (*listedPackage, error) { // This is due to how it linkname-implements std packages, // such as sync/atomic or reflect, without importing them in any way. // If ListedPackages lacks such a package we fill it with "std". - if curPkg.ImportPath == "runtime" { + // Note that this is also allowed for runtime sub-packages. + if curPkg.ImportPath == "runtime" || strings.HasPrefix(curPkg.ImportPath, "runtime/") { if ok { return pkg, nil } diff --git a/testdata/script/gogarble.txtar b/testdata/script/gogarble.txtar index 640c18f..5c6cd4c 100644 --- a/testdata/script/gogarble.txtar +++ b/testdata/script/gogarble.txtar @@ -36,7 +36,13 @@ garble build std # Link a binary importing net/http, which will catch whether or not we # support ImportMap when linking. +# Also ensure we are obfuscating low-level std packages. garble build -o=out ./stdimporter +! binsubstr out 'http.ListenAndServe' 'debug.WriteHeapDump' 'time.Now' 'syscall.Listen' + +# The same low-level std packages appear in plain sight in regular builds. +go build -o=out_regular ./stdimporter +binsubstr out_regular 'http.ListenAndServe' 'debug.WriteHeapDump' 'time.Now' 'syscall.Listen' # Also check that a full rebuild is reproducible, via a new GOCACHE. # This is slow, but necessary to uncover bugs hidden by the build cache. @@ -73,6 +79,8 @@ package main import ( "net/http" "runtime/debug" + "time" + "syscall" ) func main() { @@ -80,4 +88,6 @@ func main() { // debug.WriteHeapDump is particularly interesting, // as it is implemented by runtime via a linkname. debug.WriteHeapDump(1) + time.Now() + syscall.Listen(0, 1) }