diff --git a/README.md b/README.md index 7e8cbce..f195379 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ order to: * Strip filenames and shuffle position information * Strip debugging information and symbol tables * Obfuscate literals, if the `-literals` flag is given -* Removes [extra information](#tiny-mode) if the `-tiny` flag is given +* Remove [extra information](#tiny-mode) if the `-tiny` flag is given ### Options @@ -59,22 +59,23 @@ to document the current shortcomings of this tool. Command string Args string } - + // never obfuscate the Message type var _ = reflect.TypeOf(Message{}) ``` ### Tiny Mode -When the `-tiny` flag is passed, extra information is stripped from the resulting -Go binary. This includes line numbers, filenames, and code in the runtime the -prints panics, fatal errors, and trace/debug info. All in all this can make binaries +When the `-tiny` flag is passed, extra information is stripped from the resulting +Go binary. This includes line numbers, filenames, and code in the runtime the +prints panics, fatal errors, and trace/debug info. All in all this can make binaries 6-10% smaller in our testing. Note: if `-tiny` is passed, no panics, fatal errors will ever be printed, but they can -still be handled internally with `recover` as normal. In addition, the `GODEBUG` +still be handled internally with `recover` as normal. In addition, the `GODEBUG` environmental variable will be ignored. ### Contributing -We actively seek new contributors, if you would like to contribute to garble use the + +We actively seek new contributors, if you would like to contribute to garble use the [CONTRIBUTING.md](CONTRIBUTING.md) as a starting point. diff --git a/bench_test.go b/bench_test.go index ea22217..e89a9dc 100644 --- a/bench_test.go +++ b/bench_test.go @@ -45,8 +45,8 @@ func BenchmarkBuild(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { cmd := exec.Command(garbleBin, "build", "./testdata/bench") - if err := cmd.Run(); err != nil { - b.Fatal(err) + if out, err := cmd.CombinedOutput(); err != nil { + b.Fatalf("%v: %s", err, out) } atomic.AddInt64(&n, 1) @@ -56,4 +56,9 @@ func BenchmarkBuild(b *testing.B) { }) b.ReportMetric(float64(userTime)/float64(n), "user-ns/op") b.ReportMetric(float64(systemTime)/float64(n), "sys-ns/op") + info, err := os.Stat(garbleBin) + if err != nil { + b.Fatal(err) + } + b.ReportMetric(float64(info.Size()), "bin-B") } diff --git a/hash.go b/hash.go index e6312c0..729328a 100644 --- a/hash.go +++ b/hash.go @@ -13,7 +13,6 @@ import ( "os" "os/exec" "strings" - "unicode" ) const buildIDSeparator = "/" @@ -143,6 +142,12 @@ func buildidOf(path string) (string, error) { } func hashWith(salt []byte, name string) string { + if len(salt) == 0 { + panic("hashWith: empty salt") + } + if name == "" { + panic("hashWith: empty name") + } const length = 4 d := sha256.New() @@ -156,37 +161,3 @@ func hashWith(salt []byte, name string) string { } return "z" + sum[:length] } - -func buildNameCharset() []rune { - var charset []rune - - for _, r := range unicode.Letter.R16 { - for c := r.Lo; c <= r.Hi; c += r.Stride { - charset = append(charset, rune(c)) - } - } - - for _, r := range unicode.Digit.R16 { - for c := r.Lo; c <= r.Hi; c += r.Stride { - charset = append(charset, rune(c)) - } - } - - return charset -} - -var privateNameCharset = buildNameCharset() - -func encodeIntToName(i int) string { - builder := strings.Builder{} - for i > 0 { - charIdx := i % len(privateNameCharset) - i -= charIdx + 1 - c := privateNameCharset[charIdx] - if builder.Len() == 0 && !unicode.IsLetter(c) { - builder.WriteByte('_') - } - builder.WriteRune(c) - } - return builder.String() -} diff --git a/import_obfuscation.go b/import_obfuscation.go index 189232c..b6da4e6 100644 --- a/import_obfuscation.go +++ b/import_obfuscation.go @@ -8,7 +8,6 @@ import ( "bufio" "bytes" "compress/gzip" - "encoding/json" "fmt" "io" "io/ioutil" @@ -59,22 +58,6 @@ type privateName struct { seed []byte } -func appendPrivateNameMap(pkg *goobj2.Package, nameMap map[string]string) error { - for _, member := range pkg.ArchiveMembers { - if member.ArchiveHeader.Name != headerPrivateNameMap { - continue - } - - serializedMap := member.ArchiveHeader.Data - serializedMap = serializedMap[:bytes.IndexByte(serializedMap, 0x00)] - if err := json.Unmarshal(serializedMap, &nameMap); err != nil { - return err - } - return nil - } - return nil -} - // extractDebugObfSrc extracts obfuscated sources from object files if -debugdir flag is enabled. func extractDebugObfSrc(pkgPath string, pkg *goobj2.Package) error { if opts.DebugDir == "" { @@ -153,21 +136,20 @@ func extractDebugObfSrc(pkgPath string, pkg *goobj2.Package) error { // It returns the path to the modified main object file, to be used for linking. // We also return a map of how the imports were garbled, as well as the private // name map recovered from the archive files, so that we can amend -X flags. -func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj string, garbledImports, privateNameMap map[string]string, _ error) { +func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj string, _ error) { mainPkg, err := goobj2.Parse(objPath, "main", importMap) if err != nil { - return "", nil, nil, fmt.Errorf("error parsing main objfile: %v", err) + return "", fmt.Errorf("error parsing main objfile: %v", err) } if err := extractDebugObfSrc("main", mainPkg); err != nil { - return "", nil, nil, err + return "", err } pkgs := []pkgInfo{{mainPkg, objPath, true}} - privateNameMap = make(map[string]string) // build list of imported packages that are private importCfg, err := goobj2.ParseImportCfg(curImportCfg) if err != nil { - return "", nil, nil, err + return "", err } for pkgPath, info := range importCfg.Packages { // if the '-tiny' flag is passed, we will strip filename @@ -175,18 +157,15 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st if private := isPrivate(pkgPath); opts.Tiny || private { pkg, err := goobj2.Parse(info.Path, pkgPath, importMap) if err != nil { - return "", nil, nil, fmt.Errorf("error parsing objfile %s at %s: %v", pkgPath, info.Path, err) + return "", fmt.Errorf("error parsing objfile %s at %s: %v", pkgPath, info.Path, err) } pkgs = append(pkgs, pkgInfo{pkg, info.Path, private}) - if err := appendPrivateNameMap(pkg, privateNameMap); err != nil { - return "", nil, nil, fmt.Errorf("error parsing name map %s at %s: %v", pkgPath, info.Path, err) - } // Avoiding double extraction from main object file if objPath != info.Path { if err := extractDebugObfSrc(pkgPath, pkg); err != nil { - return "", nil, nil, err + return "", err } } } @@ -195,7 +174,6 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st var sb strings.Builder var buf bytes.Buffer - garbledImports = make(map[string]string) replacedFiles := make(map[string]string) for _, p := range pkgs { // log.Printf("++ Obfuscating object file for %s ++", p.pkg.ImportPath) @@ -233,7 +211,7 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st privImports.privatePaths = append(privImports.privatePaths, privPaths...) privImports.privateNames = append(privImports.privateNames, privNames...) - return hashImport(imp, nil, garbledImports) + return hashImport(imp, nil) } for i := range am.Imports { @@ -268,7 +246,7 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st // log.Printf("\t== Private imports: %v ==\n", privImports) // garble all private import paths in all symbol names - garbleSymbols(&am, privImports, garbledImports, &buf, &sb) + garbleSymbols(&am, privImports, &buf, &sb) } // An archive under the temporary file. Note that @@ -276,22 +254,22 @@ func obfuscateImports(objPath string, importMap goobj2.ImportMap) (garbledObj st // simply use its name after closing the file. tempObjFile, err := ioutil.TempFile(sharedTempDir, "pkg.*.a") if err != nil { - return "", nil, nil, fmt.Errorf("creating temp file: %v", err) + return "", fmt.Errorf("creating temp file: %v", err) } tempObj := tempObjFile.Name() tempObjFile.Close() if err := p.pkg.Write(tempObj); err != nil { - return "", nil, nil, fmt.Errorf("error writing objfile %s at %s: %v", p.pkg.ImportPath, p.path, err) + return "", fmt.Errorf("error writing objfile %s at %s: %v", p.pkg.ImportPath, p.path, err) } replacedFiles[p.path] = tempObj } // garble importcfg so the linker knows where to find garbled imports - if err := garbleImportCfg(curImportCfg, importCfg, garbledImports, replacedFiles); err != nil { - return "", nil, nil, err + if err := garbleImportCfg(curImportCfg, importCfg, replacedFiles); err != nil { + return "", err } - return replacedFiles[objPath], garbledImports, privateNameMap, nil + return replacedFiles[objPath], nil } // stripPCLinesAndNames removes all filename and position info @@ -446,23 +424,23 @@ func dedupPrivateNames(names []privateName) []privateName { return names[:j] } -func hashImport(pkg string, seed []byte, garbledImports map[string]string) string { - if seed == nil { - if garbledPkg, ok := garbledImports[pkg]; ok { - return garbledPkg +func hashImport(pkg string, seed []byte) string { + if len(seed) == 0 { + pkgPath := pkg + if pkgPath == "main" { + // The main package is known under its import path in + // the import config map. + pkgPath = buildInfo.firstImport } - seed = buildInfo.imports[pkg].actionID + seed = buildInfo.imports[pkgPath].actionID } - garbledPkg := hashWith(seed, pkg) - garbledImports[pkg] = garbledPkg - - return garbledPkg + return hashWith(seed, pkg) } // garbleSymbols replaces all private import paths/package names in symbol names // and data of an archive member. -func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbledImports map[string]string, buf *bytes.Buffer, sb *strings.Builder) { +func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, buf *bytes.Buffer, sb *strings.Builder) { lists := [][]*goobj2.Sym{am.SymDefs, am.NonPkgSymDefs, am.NonPkgSymRefs} for _, list := range lists { for _, s := range list { @@ -493,26 +471,26 @@ func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbled } else if strings.HasPrefix(s.Name, "type..namedata.") { dataTyp = namedata } - s.Data = garbleSymData(s.Data, privImports, garbledImports, dataTyp, buf) + s.Data = garbleSymData(s.Data, privImports, dataTyp, buf) if s.Size != 0 { s.Size = uint32(len(s.Data)) } } - s.Name = garbleSymbolName(s.Name, privImports, garbledImports, sb) + s.Name = garbleSymbolName(s.Name, privImports, sb) for i := range s.Reloc { - s.Reloc[i].Name = garbleSymbolName(s.Reloc[i].Name, privImports, garbledImports, sb) + s.Reloc[i].Name = garbleSymbolName(s.Reloc[i].Name, privImports, sb) } if s.Type != nil { - s.Type.Name = garbleSymbolName(s.Type.Name, privImports, garbledImports, sb) + s.Type.Name = garbleSymbolName(s.Type.Name, privImports, sb) } if s.Func != nil { for i := range s.Func.FuncData { - s.Func.FuncData[i].Sym.Name = garbleSymbolName(s.Func.FuncData[i].Sym.Name, privImports, garbledImports, sb) + s.Func.FuncData[i].Sym.Name = garbleSymbolName(s.Func.FuncData[i].Sym.Name, privImports, sb) } for _, inl := range s.Func.InlTree { - inl.Func.Name = garbleSymbolName(inl.Func.Name, privImports, garbledImports, sb) + inl.Func.Name = garbleSymbolName(inl.Func.Name, privImports, sb) if opts.Tiny { inl.Line = 1 } @@ -533,7 +511,7 @@ func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbled } } for i := range am.SymRefs { - am.SymRefs[i].Name = garbleSymbolName(am.SymRefs[i].Name, privImports, garbledImports, sb) + am.SymRefs[i].Name = garbleSymbolName(am.SymRefs[i].Name, privImports, sb) } // remove dwarf file list, it isn't needed as we pass "-w, -s" to the linker @@ -542,7 +520,7 @@ func garbleSymbols(am *goobj2.ArchiveMember, privImports privateImports, garbled // garbleSymbolName finds all private imports in a symbol name, garbles them, // and returns the modified symbol name. -func garbleSymbolName(symName string, privImports privateImports, garbledImports map[string]string, sb *strings.Builder) string { +func garbleSymbolName(symName string, privImports privateImports, sb *strings.Builder) string { prefix, name, skipSym := splitSymbolPrefix(symName) if skipSym { // log.Printf("\t\t? Skipped symbol: %s", symName) @@ -572,7 +550,7 @@ func garbleSymbolName(symName string, privImports privateImports, garbledImports } sb.WriteString(name[off : off+o]) - sb.WriteString(hashImport(name[off+o:off+o+l], privName.seed, garbledImports)) + sb.WriteString(hashImport(name[off+o:off+o+l], privName.seed)) off += o + l } @@ -748,7 +726,7 @@ func isSymbol(c byte) bool { // garbleSymData finds all private imports in a symbol's data blob, // garbles them, and returns the modified symbol data. -func garbleSymData(data []byte, privImports privateImports, garbledImports map[string]string, dataTyp dataType, buf *bytes.Buffer) []byte { +func garbleSymData(data []byte, privImports privateImports, dataTyp dataType, buf *bytes.Buffer) []byte { var symData []byte switch dataTyp { case importPath: @@ -772,11 +750,11 @@ func garbleSymData(data []byte, privImports privateImports, garbledImports map[s // there is only one import path in the symbol's data, garble it and return if dataTyp == importPath { - return createImportPathData(hashImport(string(symData[o:o+l]), privName.seed, garbledImports)) + return createImportPathData(hashImport(string(symData[o:o+l]), privName.seed)) } buf.Write(symData[off : off+o]) - buf.WriteString(hashImport(string(symData[off+o:off+o+l]), privName.seed, garbledImports)) + buf.WriteString(hashImport(string(symData[off+o:off+o+l]), privName.seed)) off += o + l } @@ -817,7 +795,7 @@ func patchReflectData(newName []byte, data []byte) []byte { } // garbleImportCfg writes a new importcfg with private import paths garbled. -func garbleImportCfg(path string, importCfg goobj2.ImportCfg, garbledImports, replacedFiles map[string]string) error { +func garbleImportCfg(path string, importCfg goobj2.ImportCfg, replacedFiles map[string]string) error { newCfg, err := os.Create(path) if err != nil { return fmt.Errorf("error creating importcfg: %v", err) @@ -827,10 +805,10 @@ func garbleImportCfg(path string, importCfg goobj2.ImportCfg, garbledImports, re for pkgPath, otherPath := range importCfg.ImportMap { if isPrivate(pkgPath) { - pkgPath = hashImport(pkgPath, nil, garbledImports) + pkgPath = hashImport(pkgPath, nil) } if isPrivate(otherPath) { - otherPath = hashImport(otherPath, nil, garbledImports) + otherPath = hashImport(otherPath, nil) } newCfgWr.WriteString("importmap ") newCfgWr.WriteString(pkgPath) @@ -841,7 +819,7 @@ func garbleImportCfg(path string, importCfg goobj2.ImportCfg, garbledImports, re for pkgPath, info := range importCfg.Packages { if isPrivate(pkgPath) { - pkgPath = hashImport(pkgPath, nil, garbledImports) + pkgPath = hashImport(pkgPath, nil) } if info.IsSharedLib { newCfgWr.WriteString("packageshlib ") diff --git a/main.go b/main.go index 73b5aa9..a11e3cf 100644 --- a/main.go +++ b/main.go @@ -128,8 +128,7 @@ var ( const ( // Note that these are capped at 16 bytes. - headerPrivateNameMap = "garble/privMap" - headerDebugSource = "garble/debugSrc" + headerDebugSource = "garble/debugSrc" ) func garbledImport(path string) (*types.Package, error) { @@ -465,8 +464,6 @@ func transformCompile(args []string) ([]string, func() error, error) { return nil, nil, fmt.Errorf("typecheck error: %v", err) } - tf.privateNameMap = make(map[string]string) - tf.existingNames = collectExistingNames(files) tf.recordReflectArgs(files) if opts.GarbleLiterals { @@ -583,19 +580,9 @@ func transformCompile(args []string) ([]string, func() error, error) { return err } - nameMap, err := json.Marshal(tf.privateNameMap) - if err != nil { - return err - } - // Adding an extra archive header is safe, // and shouldn't break other tools like the linker since our header name is unique pkg.ArchiveMembers = append(pkg.ArchiveMembers, - goobj2.ArchiveMember{ArchiveHeader: goobj2.ArchiveHeader{ - Name: headerPrivateNameMap, - Size: int64(len(nameMap)), - Data: nameMap, - }}, goobj2.ArchiveMember{ArchiveHeader: goobj2.ArchiveHeader{ Name: headerDebugSource, Size: int64(obfSrcArchive.Len()), @@ -657,14 +644,9 @@ func (tf *transformer) handleDirectives(comments []string) { // The name exists and was obfuscated; replace the // comment with the obfuscated name. - if token.IsExported(name) { - obfName := hashWith(listedPkg.actionID, name) - fields[2] = pkg + "." + obfName - comments[i] = strings.Join(fields, " ") - } else if obfName, ok := tf.privateNameMap[fields[2]]; ok { - fields[2] = pkg + "." + obfName - comments[i] = strings.Join(fields, " ") - } + obfName := hashWith(listedPkg.actionID, name) + fields[2] = pkg + "." + obfName + comments[i] = strings.Join(fields, " ") } } @@ -873,22 +855,6 @@ func (tf *transformer) recordReflectArgs(files []*ast.File) { } } -// collectExistingNames collects all names, including the names of local -// variables, functions, global fields, etc. -func collectExistingNames(files []*ast.File) map[string]bool { - names := make(map[string]bool) - visit := func(node ast.Node) bool { - if ident, ok := node.(*ast.Ident); ok { - names[ident.Name] = true - } - return true - } - for _, file := range files { - ast.Inspect(file, visit) - } - return names -} - // transformer holds all the information and state necessary to obfuscate a // single Go package. type transformer struct { @@ -896,17 +862,6 @@ type transformer struct { pkg *types.Package info *types.Info - // existingNames contains all the existing names in the current package, - // to avoid name collisions. - existingNames map[string]bool - - // privateNameMap records how unexported names were obfuscated. For - // example, "some/pkg.foo" could be mapped to "C" if it was one of the - // first private names to be obfuscated. - // TODO: why include the "some/pkg." prefix if it's always going to be - // the current package? - privateNameMap map[string]string - // ignoreObjects records all the objects we cannot obfuscate. An object // is any named entity, such as a declared variable or type. // @@ -918,11 +873,6 @@ type transformer struct { // * Types or variables from external packages which were not // obfuscated, for caching reasons; see transformGo. ignoreObjects map[types.Object]bool - - // nameCounter keeps track of how many unique identifier names we've - // obfuscated, so that the obfuscated names get assigned incrementing - // short names like "a", "b", "c", etc. - nameCounter int } // transformGo garbles the provided Go syntax node. @@ -1149,33 +1099,8 @@ func (tf *transformer) transformGo(file *ast.File) *ast.File { origName := node.Name _ = origName // used for debug prints below - // The exported names cannot be shortened as counter synchronization - // between packages is not currently implemented - if token.IsExported(node.Name) { - node.Name = hashWith(actionID, node.Name) - // log.Printf("%q hashed with %x to %q", origName, actionID, node.Name) - return true - } - - fullName := tf.pkg.Path() + "." + node.Name - if name, ok := tf.privateNameMap[fullName]; ok { - node.Name = name - // log.Printf("%q retrieved private name %q for %q", origName, name, path) - return true - } - - var name string - for { - tf.nameCounter++ - name = encodeIntToName(tf.nameCounter) - if !tf.existingNames[name] { - break - } - } - - tf.privateNameMap[fullName] = name - node.Name = name - // log.Printf("%q assigned private name %q for %q", origName, name, path) + node.Name = hashWith(actionID, node.Name) + // log.Printf("%q hashed with %x to %q", origName, actionID, node.Name) return true } return astutil.Apply(file, pre, nil).(*ast.File) @@ -1251,7 +1176,7 @@ func transformLink(args []string) ([]string, func() error, error) { importMap := func(importPath string) (objectPath string) { return buildInfo.imports[importPath].packagefile } - garbledObj, garbledImports, privateNameMap, err := obfuscateImports(paths[0], importMap) + garbledObj, err := obfuscateImports(paths[0], importMap) if err != nil { return nil, nil, err } @@ -1279,17 +1204,10 @@ func transformLink(args []string) ([]string, func() error, error) { // the import config map. pkgPath = buildInfo.firstImport } - if id := buildInfo.imports[pkgPath].actionID; len(id) > 0 { - // We use privateNameMap because unexported names are obfuscated - // to short names like "A", "B", "C" etc, which is not reproducible - // here. If the name isn't in the map, a hash will do. - newName, ok := privateNameMap[pkg+"."+name] - if !ok { - newName = hashWith(id, name) - } - garbledPkg := garbledImports[pkg] - flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", garbledPkg, newName, str)) - } + id := buildInfo.imports[pkgPath].actionID + newName := hashWith(id, name) + garbledPkg := hashWith(id, pkg) + flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", garbledPkg, newName, str)) }) // Ensure we strip the -buildid flag, to not leak any build IDs for the diff --git a/testdata/scripts/basic.txt b/testdata/scripts/basic.txt index 915ddad..ee2dbc2 100644 --- a/testdata/scripts/basic.txt +++ b/testdata/scripts/basic.txt @@ -23,7 +23,7 @@ stdout 'unknown' ! stdout $gofullversion # The binary can't contain the version string either. -! binsubstr main$exe ${WORK@R} 'main.go' 'globalVar' 'globalFunc' $gofullversion +! binsubstr main$exe ${WORK@R} 'main.go' 'globalVar' 'globalFunc' 'garble' $gofullversion [short] stop # checking that the build is reproducible is slow