diff --git a/line_obfuscator.go b/line_obfuscator.go index 9e12801..1fe0b3b 100644 --- a/line_obfuscator.go +++ b/line_obfuscator.go @@ -16,6 +16,49 @@ import ( // Source: https://go.googlesource.com/go/+/refs/heads/master/src/cmd/compile/internal/syntax/parser_test.go#229 const PosMin = 1 +const buildTagPrefix = "// +build" + +// Source: https://go.googlesource.com/go/+/refs/heads/master/src/cmd/compile/internal/gc/noder.go#1493 +var nameSpecialDirectives = []string{ + "//go:linkname", + + "//go:cgo_export_static", + "//go:cgo_export_dynamic", + "//go:cgo_import_static", + "//go:cgo_import_dynamic", +} + +var specialDirectives = append([]string{ + "//go:cgo_ldflag", + "//go:cgo_dynamic_linker", +}, nameSpecialDirectives...) + +func isDirective(text string, directives []string) bool { + for _, prefix := range directives { + if strings.HasPrefix(text, prefix) { + return true + } + } + return false +} + +func getLocalName(text string) (string, bool) { + if !isDirective(text, nameSpecialDirectives) { + return "", false + } + parts := strings.Fields(text) + if len(parts) < 2 { + return "", false + } + + name := strings.TrimSpace(parts[1]) + if len(name) == 0 { + return "", false + } + + return name, true +} + func prependComment(group *ast.CommentGroup, comment *ast.Comment) *ast.CommentGroup { if group == nil { return &ast.CommentGroup{List: []*ast.Comment{comment}} @@ -32,9 +75,8 @@ func clearCommentGroup(group *ast.CommentGroup) *ast.CommentGroup { } var comments []*ast.Comment - for _, comment := range group.List { - if strings.HasPrefix(comment.Text, "//go:") { + if strings.HasPrefix(comment.Text, "//go:") && !isDirective(comment.Text, specialDirectives) { comments = append(comments, &ast.Comment{Text: comment.Text}) } } @@ -68,21 +110,46 @@ func clearNodeComments(node ast.Node) { } } -func findBuildTags(commentGroups []*ast.CommentGroup) (buildTags []string) { - for _, group := range commentGroups { - for _, comment := range group.List { - if !strings.Contains(comment.Text, "+build") { +// processDetachedDire collects all not attached to declarations comments and build tags +// It returns detached comments and local name blacklist +func processDetachedDirectives(commentGroups []*ast.CommentGroup) (detachedComments, localNameBlacklist []string) { + var buildTags []string + var specialComments []string + for _, commentGroup := range commentGroups { + for _, comment := range commentGroup.List { + if strings.HasPrefix(comment.Text, buildTagPrefix) { + buildTags = append(buildTags, comment.Text) + continue + } + + if !isDirective(comment.Text, specialDirectives) { continue } - buildTags = append(buildTags, comment.Text) + + specialComments = append(specialComments, comment.Text) + if localName, ok := getLocalName(comment.Text); ok { + localNameBlacklist = append(localNameBlacklist, localName) + } } } - return buildTags + + detachedComments = append(detachedComments, buildTags...) + detachedComments = append(detachedComments, specialComments...) + detachedComments = append(detachedComments, "") + return detachedComments, localNameBlacklist } -func transformLineInfo(file *ast.File) ([]string, *ast.File) { +// transformLineInfo removes the comment except go directives and build tags. Converts comments to the node view. +// It returns comments not attached to declarations and names of declarations which cannot be renamed. +func transformLineInfo(file *ast.File, cgoFile bool) (detachedComments, localNameBlacklist []string, f *ast.File) { + prefix := "" + if cgoFile { + prefix = "_cgo_" + } + // Save build tags and add file name leak protection - extraComments := append(findBuildTags(file.Comments), "", "//line :1") + detachedComments, localNameBlacklist = processDetachedDirectives(file.Comments) + detachedComments = append(detachedComments, "", "//line "+prefix+":1") file.Comments = nil newLines := mathrand.Perm(len(file.Decls)) @@ -92,16 +159,20 @@ func transformLineInfo(file *ast.File) ([]string, *ast.File) { node := cursor.Node() clearNodeComments(node) + // If tiny mode is active information about line numbers is erased in object files + if envGarbleTiny { + return true + } funcDecl, ok := node.(*ast.FuncDecl) if !ok { return true } - comment := &ast.Comment{Text: fmt.Sprintf("//line %c.go:%d", nameCharset[mathrand.Intn(len(nameCharset))], PosMin+newLines[funcCounter])} + comment := &ast.Comment{Text: fmt.Sprintf("//line %s%c.go:%d", prefix, nameCharset[mathrand.Intn(len(nameCharset))], PosMin+newLines[funcCounter])} funcDecl.Doc = prependComment(funcDecl.Doc, comment) funcCounter++ return true } - return extraComments, astutil.Apply(file, pre, nil).(*ast.File) + return detachedComments, localNameBlacklist, astutil.Apply(file, pre, nil).(*ast.File) } diff --git a/main.go b/main.go index acdae95..6baaf6b 100644 --- a/main.go +++ b/main.go @@ -591,11 +591,25 @@ func transformCompile(args []string) ([]string, error) { privateNameMap := make(map[string]string) existingNames := collectNames(files) packageCounter := 0 + detachedComments := make([][]string, len(files)) + + for i, file := range files { + name := filepath.Base(filepath.Clean(paths[i])) + cgoFile := strings.HasPrefix(name, "_cgo_") + fileDetachedComments, localNameBlacklist, file := transformLineInfo(file, cgoFile) + for _, name := range localNameBlacklist { + obj := pkg.Scope().Lookup(name) + if obj != nil { + blacklist[obj] = struct{}{} + } + } + detachedComments[i] = fileDetachedComments + files[i] = file + } // TODO: randomize the order and names of the files newPaths := make([]string, 0, len(files)) for i, file := range files { - var extraComments []string origName := filepath.Base(filepath.Clean(paths[i])) name := origName switch { @@ -622,9 +636,6 @@ func transformCompile(args []string) ([]string, error) { // messy. name = "_cgo_" + name default: - if !envGarbleTiny { - extraComments, file = transformLineInfo(file) - } file = transformGo(file, info, blacklist, privateNameMap, pkgPath, existingNames, &packageCounter) // Uncomment for some quick debugging. Do not delete. @@ -651,8 +662,9 @@ func transformCompile(args []string) ([]string, error) { printWriter = io.MultiWriter(tempFile, debugFile) } - if len(extraComments) > 0 { - for _, comment := range extraComments { + fileDetachedComments := detachedComments[i] + if len(fileDetachedComments) > 0 { + for _, comment := range fileDetachedComments { if _, err = printWriter.Write([]byte(comment + "\n")); err != nil { return nil, err } diff --git a/testdata/scripts/syntax.txt b/testdata/scripts/syntax.txt index 3d7cb98..23a18e4 100644 --- a/testdata/scripts/syntax.txt +++ b/testdata/scripts/syntax.txt @@ -1,19 +1,20 @@ env GOPRIVATE='test/main,rsc.io/*' -garble build +garble build -tags directives exec ./main$exe cmp stderr main.stderr -! binsubstr main$exe 'localName' 'globalConst' 'globalVar' 'globalType' 'valuable information' 'rsc.io' +! binsubstr main$exe 'localName' 'globalConst' 'globalVar' 'globalType' 'valuable information' 'rsc.io' 'remoteIntReturn' 'intReturn' +binsubstr main$exe 'magicFunc' [short] stop # no need to verify this with -short -go build +go build -tags directives exec ./main$exe cmp stderr main.stderr binsubstr main$exe 'globalVar' # 'globalType' only matches on go < 1.15 -! binsubstr main$exe 'localName' 'globalConst' +! binsubstr main$exe 'localName' 'globalConst' 'remoteIntReturn' 'intReturn' -- go.mod -- @@ -73,6 +74,7 @@ func main() { scopesTest() println(quote.Go()) sub.Test() + sub.TestDirectives() } -- scopes.go -- @@ -122,6 +124,39 @@ func Test() { func noop(...interface{}) {} +-- sub/directives.go -- +// +build directives + +package sub + +import ( + _ "unsafe" + _ "test/main/sub/a" +) + +//go:linkname remoteIntReturn a.magicFunc + +func remoteIntReturn() int + +//go:noinline +func TestDirectives() { + if remoteIntReturn() != 42 { + panic("invalid result") + } +} +-- sub/a/directives.go -- +// +build directives + +package a + +import _ "unsafe" + +//go:linkname intReturn a.magicFunc + +//go:noinline +func intReturn() int { + return 42 +} -- main.stderr -- nil case {"Foo":3}