reimplement import path obfuscation without goobj2 (#242)

We used to rely on a parallel implementation of an object file parser
and writer to be able to obfuscate import paths. After compiling each
package, we would parse the object file, replace the import paths, and
write the updated object file in-place.

That worked well, in most cases. Unfortunately, it had some flaws:

* Complexity. Even when most of the code is maintained in a separate
  module, the import_obfuscation.go file was still close to a thousand
  lines of code.

* Go compatibility. The object file format changes between Go releases,
  so we were supporting Go 1.15, but not 1.16. Fixing the object file
  package to work with 1.16 would probably break 1.15 support.

* Bugs. For example, we recently had to add a workaround for #224, since
  import paths containing dots after the domain would end up escaped.
  Another example is #190, which seems to be caused by the object file
  parser or writer corrupting the compiled code and causing segfaults in
  some rare edge cases.

Instead, let's drop that method entirely, and force the compiler and
linker to do the work for us. The steps necessary when compiling a
package to obfuscate are:

1) Replace its "package foo" lines with the obfuscated package path. No
   need to separate the package path and name, since the obfuscated path
   does not contain slashes.

2) Replace the "-p pkg/foo" flag with the obfuscated path.

3) Replace the "import" spec lines with the obfuscated package paths,
   for those dependencies which were obfuscated.

4) Replace the "-importcfg [...]" file with a version that uses the
   obfuscated paths instead.

The linker also needs that last step, since it also uses an importcfg
file to find object files.

There are three noteworthy drawbacks to this new method:

1) Since we no longer write object files, we can't use them to store
   data to be cached. As such, the -debugdir flag goes back to using the
   "-a" build flag to always rebuild all packages. On the plus side,
   that caching didn't work very well; see #176.

2) The package name "main" remains in all declarations under it, not
   just "func main", since we can only rename entire packages. This
   seems fine, as it gives little information to the end user.

3) The -tiny mode no longer sets all lines to 0, since it did that by
   modifying object files. As a temporary measure, we instead set all
   top-level declarations to be on line 1. A TODO is added to hopefully
   improve this again in the near future.

The upside is that we get rid of all the issues mentioned before. Plus,
garble now nearly works with Go 1.16, with the exception of two very
minor bugs that look fixable. A follow-up PR will take care of that and
start testing on 1.16.

Fixes #176.
Fixes #190.
pull/238/head
Daniel Martí 3 years ago committed by GitHub
parent e2a32634a6
commit 05d0dd1801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -3,7 +3,6 @@ module mvdan.cc/garble
go 1.15
require (
github.com/Binject/debug v0.0.0-20210101210738-1b03ff50b8a5
github.com/google/go-cmp v0.5.4
github.com/rogpeppe/go-internal v1.7.1-0.20210131190821-dc4b49510d96
golang.org/x/mod v0.4.1

@ -1,5 +1,3 @@
github.com/Binject/debug v0.0.0-20210101210738-1b03ff50b8a5 h1:uks5QpWybw0NHhiHQHJUWkz6BBY0mbhG6+FRicVDP9A=
github.com/Binject/debug v0.0.0-20210101210738-1b03ff50b8a5/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=

@ -1,868 +0,0 @@
// Copyright (c) 2020, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"github.com/Binject/debug/goobj2"
)
// pkgInfo stores a parsed go archive/object file,
// and the original path to which it was read from.
type pkgInfo struct {
pkg *goobj2.Package
path string
private bool
}
// dataType signifies whether the Data portion of a
// goobj2.Sym is reflection data for an import path,
// reflection data for a method of struct field, or
// something else.
type dataType uint8
const (
other dataType = iota
importPath
namedata
)
// privateImports stores package paths and names that
// match GOPRIVATE. privateNames are elements of the
// paths in privatePaths, separated so that the shorter
// names don't accidentally match another import, such
// as a stdlib package
type privateImports struct {
privatePaths []string
privateNames []privateName
}
// privateName is a package name with a unique seed
// that insures that two package names with the same
// name but from different package paths will be hashed
// differently.
type privateName struct {
name string
seed []byte
}
// extractDebugObfSrc extracts obfuscated sources from object files if -debugdir flag is enabled.
func extractDebugObfSrc(pkgPath string, pkg *goobj2.Package) error {
if opts.DebugDir == "" {
return nil
}
var archiveMember *goobj2.ArchiveMember
for _, member := range pkg.ArchiveMembers {
if member.ArchiveHeader.Name == headerDebugSource {
archiveMember = &member
break
}
}
if archiveMember == nil {
return nil
}
osPkgPath := filepath.FromSlash(pkgPath)
pkgDebugDir := filepath.Join(opts.DebugDir, osPkgPath)
if err := os.MkdirAll(pkgDebugDir, 0o755); err != nil {
return err
}
archive := bytes.NewBuffer(archiveMember.ArchiveHeader.Data)
gzipReader, err := gzip.NewReader(archive)
if err != nil {
return err
}
defer gzipReader.Close()
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
debugFilePath := filepath.Join(pkgDebugDir, header.Name)
debugFile, err := os.Create(debugFilePath)
if err != nil {
return err
}
if _, err := io.Copy(debugFile, tarReader); err != nil {
return err
}
if err := debugFile.Close(); err != nil {
return err
}
obfuscationTime := header.ModTime.Local()
// Restore the actual source obfuscation time so as not to mislead the user.
if err := os.Chtimes(debugFilePath, obfuscationTime, obfuscationTime); err != nil {
return err
}
}
}
// obfuscateImports does all the necessary work to replace the import paths of
// obfuscated packages with hashes. It takes the single object file and import
// config passed to the linker, as well as a temporary directory to store
// modified object files.
//
// For each garbled package, we write a modified version of its object file,
// replacing import paths as necessary. We can't modify the object files
// in-place, as those are the cached compiler output. Modifying the output of
// the compiler cache would trigger recompilations.
//
// Note that we can modify the importcfg file in-place, because it's not part of
// the build cache.
//
// 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, _ error) {
mainPkg, err := goobj2.Parse(objPath, "main", importMap)
if err != nil {
return "", fmt.Errorf("error parsing main objfile: %v", err)
}
if err := extractDebugObfSrc("main", mainPkg); err != nil {
return "", err
}
pkgs := []pkgInfo{{mainPkg, objPath, true}}
// build list of imported packages that are private
importCfg, err := goobj2.ParseImportCfg(curImportCfg)
if err != nil {
return "", err
}
for pkgPath, info := range importCfg.Packages {
// if the '-tiny' flag is passed, we will strip filename
// and position info of every package, but not garble anything
if private := isPrivate(pkgPath); opts.Tiny || private {
pkg, err := goobj2.Parse(info.Path, pkgPath, importMap)
if err != nil {
return "", fmt.Errorf("error parsing objfile %s at %s: %v", pkgPath, info.Path, err)
}
pkgs = append(pkgs, pkgInfo{pkg, info.Path, private})
// Avoiding double extraction from main object file
if objPath != info.Path {
if err := extractDebugObfSrc(pkgPath, pkg); err != nil {
return "", err
}
}
}
}
var sb strings.Builder
var buf bytes.Buffer
replacedFiles := make(map[string]string)
for _, p := range pkgs {
// log.Printf("++ Obfuscating object file for %s ++", p.pkg.ImportPath)
for _, am := range p.pkg.ArchiveMembers {
// log.Printf("\t## Obfuscating archive member %s ##", am.ArchiveHeader.Name)
// skip objects that are not used by the linker, or that do not contain
// any Go symbol info
if am.IsCompilerObj() || am.IsDataObj {
continue
}
// not part of a private package, so just strip filename
// and position info and move on
if !p.private {
stripPCLinesAndNames(&am)
continue
}
// add all private import paths to a list to garble
var privImports privateImports
privImports.privatePaths, privImports.privateNames = explodeImportPath(p.pkg.ImportPath)
// the main package might not have the import path "main" due to modules,
// so add "main" to private import paths
if p.pkg.ImportPath == buildInfo.firstImport {
privImports.privatePaths = append(privImports.privatePaths, "main")
}
initImport := func(imp string) string {
// Due to an apparent bug in goobj2, in some rare
// edge cases like gopkg.in/yaml.v2 we end up with
// URL-escaped paths, like "gopkg.in/yaml%2ev2".
// Unescape them to use isPrivate and hashImport.
// TODO: Investigate and fix this in goobj2.
unesc, err := url.PathUnescape(imp)
if err != nil {
panic(err)
}
if !isPrivate(unesc) {
// If the path isn't private, leave the
// original path as-is, even if escaped.
return imp
}
privPaths, privNames := explodeImportPath(unesc)
privImports.privatePaths = append(privImports.privatePaths, privPaths...)
privImports.privateNames = append(privImports.privateNames, privNames...)
return hashImport(unesc, nil)
}
for i := range am.Imports {
am.Imports[i].Pkg = initImport(am.Imports[i].Pkg)
}
for i := range am.Packages {
am.Packages[i] = initImport(am.Packages[i])
}
privImports.privatePaths = dedupStrings(privImports.privatePaths)
privImports.privateNames = dedupPrivateNames(privImports.privateNames)
// move imports that contain another import as a substring to the front,
// so that the shorter import will not match first and leak part of an
// import path
sort.Slice(privImports.privatePaths, func(i, j int) bool {
iSlashes := strings.Count(privImports.privatePaths[i], "/")
jSlashes := strings.Count(privImports.privatePaths[j], "/")
// sort by number of slashes unless equal, then sort reverse alphabetically
if iSlashes == jSlashes {
return privImports.privatePaths[i] > privImports.privatePaths[j]
}
return iSlashes > jSlashes
})
sort.Slice(privImports.privateNames, func(i, j int) bool {
return privImports.privateNames[i].name > privImports.privateNames[j].name
})
// no private import paths, nothing to garble
if len(privImports.privatePaths) == 0 {
continue
}
// log.Printf("\t== Private imports: %v ==\n", privImports)
// garble all private import paths in all symbol names
garbleSymbols(&am, privImports, &buf, &sb)
}
// An archive under the temporary file. Note that
// ioutil.TempFile creates a file to ensure no collisions, so we
// simply use its name after closing the file.
tempObjFile, err := ioutil.TempFile(sharedTempDir, "pkg.*.a")
if err != nil {
return "", fmt.Errorf("creating temp file: %v", err)
}
tempObj := tempObjFile.Name()
tempObjFile.Close()
if err := p.pkg.Write(tempObj); err != nil {
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, replacedFiles); err != nil {
return "", err
}
return replacedFiles[objPath], nil
}
// stripPCLinesAndNames removes all filename and position info
// from an archive member.
func stripPCLinesAndNames(am *goobj2.ArchiveMember) {
lists := [][]*goobj2.Sym{am.SymDefs, am.NonPkgSymDefs, am.NonPkgSymRefs}
for _, list := range lists {
for _, s := range list {
// remove filename symbols when -tiny is passed as they
// are only used for printing panics, and -tiny removes
// panic printing; we need to set the symbol names to
// 'gofile..', otherwise the linker will expect to see
// filename symbols and panic
if strings.HasPrefix(s.Name, "gofile..") {
s.Name = "gofile.."
}
if s.Func == nil {
continue
}
for _, inl := range s.Func.InlTree {
inl.Line = 1
}
s.Func.PCFile = nil
s.Func.PCLine = nil
s.Func.PCInline = nil
// remove unneeded debug aux symbols
s.Func.DwarfInfo = nil
s.Func.DwarfLoc = nil
s.Func.DwarfRanges = nil
s.Func.DwarfDebugLines = nil
}
}
// remove dwarf file list, it isn't needed as we pass "-w, -s" to the linker
am.DWARFFileList = nil
}
// explodeImportPath returns lists of import paths
// and package names that could all potentially be
// in symbol names of the package that imported 'path'.
// ex. path=github.com/foo/bar/baz, GOPRIVATE=github.com/*
// pkgPaths=[github.com/foo/bar, github.com/foo]
// pkgNames=[foo, bar, baz]
// Because package names could refer to multiple import
// paths, a seed is bundled with each package name to
// ensure that 2 identical package names from different
// import paths will get hashed differently.
func explodeImportPath(path string) ([]string, []privateName) {
paths := strings.Split(path, "/")
if len(paths) == 1 {
return []string{path}, nil
}
pkgPaths := make([]string, 0, len(paths)-1)
pkgNames := make([]privateName, 0, len(paths)-1)
var restPrivate bool
if isPrivate(paths[0]) {
pkgPaths = append(pkgPaths, paths[0])
restPrivate = true
}
// find first private match
privateIdx := 1
if !restPrivate {
newPath := paths[0]
for i := 1; i < len(paths); i++ {
newPath += "/" + paths[i]
if isPrivate(newPath) {
pkgPaths = append(pkgPaths, newPath)
if newName, ok := newPrivateName(paths[i], newPath); ok {
pkgNames = append(pkgNames, newName)
}
privateIdx = i + 1
restPrivate = true
break
}
}
if !restPrivate {
return nil, nil
}
}
lastComboIdx := 1
for i := privateIdx; i < len(paths); i++ {
newPath := pkgPaths[lastComboIdx-1] + "/" + paths[i]
pkgPaths = append(pkgPaths, newPath)
if newName, ok := newPrivateName(paths[i], newPath); ok {
pkgNames = append(pkgNames, newName)
}
lastComboIdx++
}
lastPath := paths[len(paths)-1]
if newName, ok := newPrivateName(lastPath, path); ok {
pkgNames = append(pkgNames, newName)
}
return pkgPaths, pkgNames
}
// newPrivateName creates a privateName from a package name
// and import path. The seed is set to path's actionID if
// we know the path. If not, the package doesn't contain any
// code, and false is returned so that the caller knows not
// to use this name as a private name.
func newPrivateName(name, path string) (privateName, bool) {
actionID := buildInfo.imports[path].actionID
if actionID == nil {
// log.Printf("*** Skipped %s of %s ***", name, path)
return privateName{}, false
}
pName := privateName{name: name, seed: actionID}
return pName, true
}
func dedupStrings(paths []string) []string {
seen := make(map[string]bool, len(paths))
j := 0
for _, v := range paths {
if seen[v] {
continue
}
seen[v] = true
paths[j] = v
j++
}
return paths[:j]
}
func dedupPrivateNames(names []privateName) []privateName {
seen := make(map[string]bool, len(names))
j := 0
for _, v := range names {
combined := v.name + string(v.seed)
if seen[combined] {
continue
}
seen[combined] = true
names[j] = v
j++
}
return names[:j]
}
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[pkgPath].actionID
// If we can't find a seed, provide a more helpful panic than hashWith's.
if len(seed) == 0 {
var imports []string
for path := range buildInfo.imports {
imports = append(imports, path)
}
sort.Strings(imports)
panic(fmt.Sprintf("could not find package path %s; imports:\n%s", pkgPath, strings.Join(imports, "\n")))
}
}
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, buf *bytes.Buffer, sb *strings.Builder) {
lists := [][]*goobj2.Sym{am.SymDefs, am.NonPkgSymDefs, am.NonPkgSymRefs}
for _, list := range lists {
for _, s := range list {
// skip debug symbols, and remove the debug symbol's data to save space
if s.Kind >= goobj2.SDWARFINFO && s.Kind <= goobj2.SDWARFLINES {
s.Size = 0
s.Data = nil
continue
}
// Skip local asm symbols. For some reason garbling these breaks things.
// TODO: don't add duplicates
if s.Kind == goobj2.SABIALIAS {
if parts := strings.SplitN(s.Name, ".", 2); parts[0] == "main" {
skipPrefixes = append(skipPrefixes, s.Name)
skipPrefixes = append(skipPrefixes, `"".`+parts[1])
continue
}
}
// garble read-only static data, but not strings. If import paths are in string
// symbols, that means garbling string symbols might effect the behavior of the
// compiled binary
if s.Kind == goobj2.SRODATA && s.Data != nil && !strings.HasPrefix(s.Name, "go.string.") {
var dataTyp dataType
if strings.HasPrefix(s.Name, "type..importpath.") {
dataTyp = importPath
} else if strings.HasPrefix(s.Name, "type..namedata.") {
dataTyp = namedata
}
s.Data = garbleSymData(s.Data, privImports, dataTyp, buf)
if s.Size != 0 {
s.Size = uint32(len(s.Data))
}
}
s.Name = garbleSymbolName(s.Name, privImports, sb)
for i := range s.Reloc {
s.Reloc[i].Name = garbleSymbolName(s.Reloc[i].Name, privImports, sb)
}
if s.Type != nil {
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, sb)
}
for _, inl := range s.Func.InlTree {
inl.Func.Name = garbleSymbolName(inl.Func.Name, privImports, sb)
if opts.Tiny {
inl.Line = 1
}
}
if opts.Tiny {
s.Func.PCFile = nil
s.Func.PCLine = nil
s.Func.PCInline = nil
}
// remove unneeded debug aux symbols
s.Func.DwarfInfo = nil
s.Func.DwarfLoc = nil
s.Func.DwarfRanges = nil
s.Func.DwarfDebugLines = nil
}
}
}
for i := range am.SymRefs {
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
am.DWARFFileList = nil
}
// garbleSymbolName finds all private imports in a symbol name, garbles them,
// and returns the modified symbol name.
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)
return symName
}
// remove filename symbols when -tiny is passed
// as they are only used for printing panics,
// and -tiny removes panic printing
if opts.Tiny && prefix == "gofile.." {
return prefix
}
var namedataSym bool
if prefix == "type..namedata." {
namedataSym = true
}
var off int
for {
o, l, privName := privateImportIndex(name[off:], privImports, namedataSym)
if o == -1 {
if sb.Len() != 0 {
sb.WriteString(name[off:])
}
break
}
sb.WriteString(name[off : off+o])
sb.WriteString(hashImport(name[off+o:off+o+l], privName.seed))
off += o + l
}
if sb.Len() == 0 {
// log.Printf("\t\t? Skipped symbol: %s", symName)
return symName
}
defer sb.Reset()
return prefix + sb.String()
}
// if symbols have one of these prefixes, skip
// garbling
// TODO: skip compiler generated/builtin symbols
var skipPrefixes = []string{
// these symbols never contain import paths
"gclocals.",
"gclocals·",
// string names be what they be
"go.string.",
// skip debug symbols
"go.info.",
// skip entrypoint symbols
"main.init.",
"main..stmp",
}
// symbols that are related to the entrypoint
// that cannot be garbled
var entrypointSyms = [...]string{
"main.main",
"main..inittask",
}
// if any of these strings are found in a
// symbol name, it should not be garbled
var skipSubstrs = [...]string{
// skip test symbols
"_test.",
}
// prefixes of symbols that we will garble,
// but we split the symbol name by one of
// these prefixes so that we do not
// accidentally garble an essential prefix
var symPrefixes = [...]string{
"go.builtin.",
"go.itab.",
"go.itablink.",
"go.interface.",
"go.map.",
"gofile..",
"type..eq.",
"type..eqfunc.",
"type..hash.",
"type..importpath.",
"type..namedata.",
"type.",
}
// splitSymbolPrefix returns the symbol name prefix Go uses
// to help designate the type of the symbol, and the rest of
// the symbol name. Additionally, a bool is returned that
// signifies whether garbling the symbol name should be skipped.
func splitSymbolPrefix(symName string) (string, string, bool) {
if symName == "" {
return "", "", true
}
for _, prefix := range skipPrefixes {
if strings.HasPrefix(symName, prefix) {
return "", "", true
}
}
for _, entrySym := range entrypointSyms {
if symName == entrySym {
return "", "", true
}
}
for _, substr := range skipSubstrs {
if strings.Contains(symName, substr) {
return "", "", true
}
}
for _, prefix := range symPrefixes {
if strings.HasPrefix(symName, prefix) {
return symName[:len(prefix)], symName[len(prefix):], false
}
}
return "", symName, false
}
// privateImportIndex returns the offset and length of a private import
// in symName. If no private imports from privImports are present in
// symName, -1, 0 is returned.
// TODO: it is possible that there could be multiple private names with
// the same name but different seeds, and currently the first name will
// always be returned. Not sure how to know which private name is correct
// in that case
func privateImportIndex(symName string, privImports privateImports, nameDataSym bool) (int, int, privateName) {
matchPkg := func(pkg string) int {
off := strings.Index(symName, pkg)
if off == -1 {
return -1
// check that we didn't match inside an import path. If the
// byte before the start of the match is not a small set of
// symbols that can make up a symbol name, we must have matched
// inside of an ident name as a substring. Or, if the byte
// before the start of the match is a forward slash, we are
// definitely inside of an input path.
} else if off != 0 && (!isSymbol(symName[off-1]) || symName[off-1] == '/') {
return -1
}
return off
}
var privName privateName
firstOff, l := -1, 0
for _, privatePkg := range privImports.privatePaths {
off := matchPkg(privatePkg)
if off == -1 {
continue
} else if off < firstOff || firstOff == -1 {
// preform the same check that matchPkg does above, but
// on the byte after the end of the match so we are
// completely sure we didn't match inside an import path
l = len(privatePkg)
if bAfter := off + l; bAfter < len(symName)-1 && symName[bAfter] != '.' && (!isSymbol(symName[bAfter]) || symName[bAfter] == '/') {
continue
}
firstOff = off
}
}
if nameDataSym {
for _, pName := range privImports.privateNames {
// search for the package name plus a period, to
// minimize the likelihood that the package isn't
// matched as a substring of another ident name.
// ex: pkgName = main, symName = "domainname"
off := matchPkg(pName.name + ".")
if off == -1 {
continue
} else if off < firstOff || firstOff == -1 {
firstOff = off
l = len(pName.name)
privName = pName
}
}
}
return firstOff, l, privName
}
func isSymbol(c byte) bool {
switch c {
case ' ', '(', ')', '*', ',', '[', ']', '_', '{', '}':
return true
default:
return false
}
}
// 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, dataTyp dataType, buf *bytes.Buffer) []byte {
var symData []byte
switch dataTyp {
case importPath:
symData = data[3:]
case namedata:
oldNameLen := int(uint16(data[1])<<8 | uint16(data[2]))
symData = data[3 : 3+oldNameLen]
default:
symData = data
}
var off int
for {
o, l, privName := privateImportIndex(string(symData[off:]), privImports, dataTyp == namedata)
if o == -1 {
if buf.Len() != 0 {
buf.Write(symData[off:])
}
break
}
// 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))
}
buf.Write(symData[off : off+o])
buf.WriteString(hashImport(string(symData[off+o:off+o+l]), privName.seed))
off += o + l
}
if buf.Len() == 0 {
return data
}
defer buf.Reset()
if dataTyp == namedata {
return patchReflectData(buf.Bytes(), data)
}
return buf.Bytes()
}
// createImportPathData creates reflection data for an
// import path
func createImportPathData(importPath string) []byte {
l := 3 + len(importPath)
b := make([]byte, l)
b[0] = 0
b[1] = uint8(len(importPath) >> 8)
b[2] = uint8(len(importPath))
copy(b[3:], importPath)
return b
}
// patchReflectData replaces the name of a struct field or
// method in reflection namedata
func patchReflectData(newName []byte, data []byte) []byte {
oldNameLen := int(uint16(data[1])<<8 | uint16(data[2]))
data[1] = uint8(len(newName) >> 8)
data[2] = uint8(len(newName))
return append(data[:3], append(newName, data[3+oldNameLen:]...)...)
}
// garbleImportCfg writes a new importcfg with private import paths garbled.
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)
}
defer newCfg.Close()
newCfgWr := bufio.NewWriter(newCfg)
for pkgPath, otherPath := range importCfg.ImportMap {
if isPrivate(pkgPath) {
pkgPath = hashImport(pkgPath, nil)
}
if isPrivate(otherPath) {
otherPath = hashImport(otherPath, nil)
}
newCfgWr.WriteString("importmap ")
newCfgWr.WriteString(pkgPath)
newCfgWr.WriteByte('=')
newCfgWr.WriteString(otherPath)
newCfgWr.WriteByte('\n')
}
for pkgPath, info := range importCfg.Packages {
if isPrivate(pkgPath) {
pkgPath = hashImport(pkgPath, nil)
}
if info.IsSharedLib {
newCfgWr.WriteString("packageshlib ")
} else {
newCfgWr.WriteString("packagefile ")
}
newCfgWr.WriteString(pkgPath)
newCfgWr.WriteByte('=')
if replaced := replacedFiles[info.Path]; replaced != "" {
newCfgWr.WriteString(replaced)
} else {
newCfgWr.WriteString(info.Path)
}
newCfgWr.WriteByte('\n')
}
if err := newCfgWr.Flush(); err != nil {
return fmt.Errorf("error writing importcfg: %v", err)
}
return nil
}

@ -113,10 +113,6 @@ func (tf *transformer) transformLineInfo(file *ast.File, name string) (detachedC
clearNodeComments(node)
return true
})
// If tiny mode is active information about line numbers is erased in object files
if opts.Tiny {
return detachedComments, file
}
newLines := mathrand.Perm(len(file.Decls))
@ -128,11 +124,14 @@ func (tf *transformer) transformLineInfo(file *ast.File, name string) (detachedC
case *ast.GenDecl:
doc = &decl.Doc
}
newPos := fmt.Sprintf("%s%c.go:%d",
prefix,
nameCharset[mathrand.Intn(len(nameCharset))],
PosMin+newLines[i],
)
newPos := prefix + ":1"
if !opts.Tiny {
newPos = fmt.Sprintf("%s%c.go:%d",
prefix,
nameCharset[mathrand.Intn(len(nameCharset))],
PosMin+newLines[i],
)
}
comment := &ast.Comment{Text: "//line " + newPos}
*doc = prependComment(*doc, comment)

@ -29,10 +29,10 @@ import (
"reflect"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/Binject/debug/goobj2"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
"golang.org/x/tools/go/ast/astutil"
@ -298,11 +298,10 @@ func mainErr(args []string) error {
transform := transformFuncs[tool]
transformed := args[1:]
var postFunc func() error
// log.Println(tool, transformed)
if transform != nil {
var err error
if transformed, postFunc, err = transform(transformed); err != nil {
if transformed, err = transform(transformed); err != nil {
return err
}
}
@ -312,11 +311,6 @@ func mainErr(args []string) error {
if err := cmd.Run(); err != nil {
return err
}
if postFunc != nil {
if err := postFunc(); err != nil {
return err
}
}
return nil
}
@ -376,6 +370,12 @@ func toolexecCmd(command string, args []string) (*exec.Cmd, error) {
"-trimpath",
"-toolexec=" + cache.ExecPath,
}
if flagDebugDir != "" {
// In case the user deletes the debug directory,
// and a previous build is cached,
// rebuild all packages to re-fill the debug dir.
goArgs = append(goArgs, "-a")
}
if command == "test" {
// vet is generally not useful on garbled code; keep it
// disabled by default.
@ -387,12 +387,12 @@ func toolexecCmd(command string, args []string) (*exec.Cmd, error) {
return exec.Command("go", goArgs...), nil
}
var transformFuncs = map[string]func([]string) (args []string, post func() error, _ error){
var transformFuncs = map[string]func([]string) (args []string, _ error){
"compile": transformCompile,
"link": transformLink,
}
func transformCompile(args []string) ([]string, func() error, error) {
func transformCompile(args []string) ([]string, error) {
var err error
flags, paths := splitFlagsFromFiles(args, ".go")
@ -408,8 +408,9 @@ func transformCompile(args []string) ([]string, func() error, error) {
opts.GarbleLiterals = false
opts.DebugDir = ""
} else if !isPrivate(curPkgPath) {
return append(flags, paths...), nil, nil
return append(flags, paths...), nil
}
for i, path := range paths {
if filepath.Base(path) == "_gomod_.go" {
// never include module info
@ -418,26 +419,26 @@ func transformCompile(args []string) ([]string, func() error, error) {
}
}
if len(paths) == 1 && filepath.Base(paths[0]) == "_testmain.go" {
return append(flags, paths...), nil, nil
return append(flags, paths...), nil
}
// If the value of -trimpath doesn't contain the separator ';', the 'go
// build' command is most likely not using '-trimpath'.
trimpath := flagValue(flags, "-trimpath")
if !strings.Contains(trimpath, ";") {
return nil, nil, fmt.Errorf("-toolexec=garble should be used alongside -trimpath")
return nil, fmt.Errorf("-toolexec=garble should be used alongside -trimpath")
}
if err := fillBuildInfo(flags); err != nil {
return nil, nil, err
newImportCfg, err := fillBuildInfo(flags)
if err != nil {
return nil, err
}
var files []*ast.File
for _, path := range paths {
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, nil, err
return nil, err
}
files = append(files, file)
}
@ -483,7 +484,7 @@ func transformCompile(args []string) ([]string, func() error, error) {
origTypesConfig := types.Config{Importer: origImporter}
tf.pkg, err = origTypesConfig.Check(curPkgPath, fset, files, tf.info)
if err != nil {
return nil, nil, fmt.Errorf("typecheck error: %v", err)
return nil, fmt.Errorf("typecheck error: %v", err)
}
tf.recordReflectArgs(files)
@ -517,6 +518,13 @@ func transformCompile(args []string) ([]string, func() error, error) {
obfSrcTarWriter := tar.NewWriter(obfSrcGzipWriter)
defer obfSrcTarWriter.Close()
newPkgPath := curPkgPath
if curPkgPath != "main" && isPrivate(curPkgPath) {
newPkgPath = hashWith(curActionID, curPkgPath)
flags = flagSetValue(flags, "-p", newPkgPath)
// println("flag:", curPkgPath, newPkgPath)
}
// TODO: randomize the order and names of the files
newPaths := make([]string, 0, len(files))
for i, file := range files {
@ -551,12 +559,39 @@ func transformCompile(args []string) ([]string, func() error, error) {
// Uncomment for some quick debugging. Do not delete.
// fmt.Fprintf(os.Stderr, "\n-- %s/%s --\n", curPkgPath, origName)
// if err := printConfig.Fprint(os.Stderr, fset, file); err != nil {
// return nil, nil, err
// return nil, err
// }
ast.Inspect(file, func(node ast.Node) bool {
imp, ok := node.(*ast.ImportSpec)
if !ok {
return true
}
path, err := strconv.Unquote(imp.Path.Value)
if err != nil {
panic(err) // should never happen
}
if isPrivate(path) {
actionID := buildInfo.imports[path].actionID
newPath := hashWith(actionID, path)
imp.Path.Value = strconv.Quote(newPath)
// TODO(mvdan): what if the package was
// named something else?
if imp.Name == nil {
imp.Name = &ast.Ident{Name: filepath.Base(path)}
}
// println("import:", path, newPath)
}
return true
})
}
if curPkgPath != "main" && isPrivate(curPkgPath) {
// println(file.Name.Name, newPkgPath)
file.Name.Name = newPkgPath
}
tempFile, err := ioutil.TempFile(sharedTempDir, name+".*.go")
if err != nil {
return nil, nil, err
return nil, err
}
defer tempFile.Close()
@ -565,14 +600,34 @@ func transformCompile(args []string) ([]string, func() error, error) {
for _, comment := range detachedComments[i] {
if _, err := printWriter.Write([]byte(comment + "\n")); err != nil {
return nil, nil, err
return nil, err
}
}
if err := printConfig.Fprint(printWriter, fset, file); err != nil {
return nil, nil, err
return nil, err
}
if opts.DebugDir != "" {
osPkgPath := filepath.FromSlash(curPkgPath)
pkgDebugDir := filepath.Join(opts.DebugDir, osPkgPath)
if err := os.MkdirAll(pkgDebugDir, 0o755); err != nil {
return nil, err
}
debugFilePath := filepath.Join(pkgDebugDir, origName)
debugFile, err := os.Create(debugFilePath)
if err != nil {
return nil, err
}
if err := printConfig.Fprint(debugFile, fset, file); err != nil {
return nil, err
}
if err := debugFile.Close(); err != nil {
return nil, err
}
}
if err := tempFile.Close(); err != nil {
return nil, nil, err
return nil, err
}
if err := obfSrcTarWriter.WriteHeader(&tar.Header{
@ -581,41 +636,19 @@ func transformCompile(args []string) ([]string, func() error, error) {
ModTime: time.Now(), // Need for restoring obfuscation time
Size: int64(obfSrc.Len()),
}); err != nil {
return nil, nil, err
return nil, err
}
if _, err := obfSrcTarWriter.Write(obfSrc.Bytes()); err != nil {
return nil, nil, err
return nil, err
}
newPaths = append(newPaths, tempFile.Name())
}
flags = flagSetValue(flags, "-importcfg", newImportCfg)
// fmt.Println(flags)
// After the compilation succeeds, add our headers to the object file.
objPath := flagValue(flags, "-o")
postCompile := func() error {
importMap := func(importPath string) (objectPath string) {
return buildInfo.imports[importPath].packagefile
}
pkg, err := goobj2.Parse(objPath, curPkgPath, importMap)
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: headerDebugSource,
Size: int64(obfSrcArchive.Len()),
Data: obfSrcArchive.Bytes(),
}},
)
return pkg.Write(objPath)
}
return append(flags, newPaths...), postCompile, nil
// println("transform compile done")
return append(flags, newPaths...), nil
}
// handleDirectives looks at all the comments in a file containing build
@ -644,11 +677,11 @@ func (tf *transformer) handleDirectives(comments []string) {
// 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 := strings.Split(fields[2], ".")
if len(newName) != 2 {
target := strings.Split(fields[2], ".")
if len(target) != 2 {
continue
}
pkg, name := newName[0], newName[1]
pkg, name := target[0], target[1]
if pkg == "runtime" && strings.HasPrefix(name, "cgo") {
continue // ignore cgo-generated linknames
}
@ -666,8 +699,12 @@ func (tf *transformer) handleDirectives(comments []string) {
// The name exists and was obfuscated; replace the
// comment with the obfuscated name.
obfName := hashWith(listedPkg.actionID, name)
fields[2] = pkg + "." + obfName
newName := hashWith(listedPkg.actionID, name)
newPkg := pkg
if pkg != "main" {
newPkg = hashWith(listedPkg.actionID, pkg)
}
fields[2] = newPkg + "." + newName
comments[i] = strings.Join(fields, " ")
}
}
@ -764,25 +801,27 @@ func isPrivate(path string) bool {
return module.MatchPrefixPatterns(envGoPrivate, path)
}
// fillBuildInfo initializes the global buildInfo struct via the supplied flags.
func fillBuildInfo(flags []string) error {
// fillBuildInfo initializes the global buildInfo struct via the supplied flags,
// and constructs a new importcfg with the obfuscated import paths changed as
// necessary.
func fillBuildInfo(flags []string) (newImportCfg string, _ error) {
buildID := flagValue(flags, "-buildid")
switch buildID {
case "", "true":
return fmt.Errorf("could not find -buildid argument")
return "", fmt.Errorf("could not find -buildid argument")
}
curActionID = decodeHash(splitActionID(buildID))
curImportCfg = flagValue(flags, "-importcfg")
if curImportCfg == "" {
return fmt.Errorf("could not find -importcfg argument")
return "", fmt.Errorf("could not find -importcfg argument")
}
data, err := ioutil.ReadFile(curImportCfg)
if err != nil {
return err
return "", err
}
importMap := make(map[string]string)
for _, line := range strings.Split(string(data), "\n") {
for _, line := range strings.SplitAfter(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
@ -810,16 +849,17 @@ func fillBuildInfo(flags []string) error {
importPath, objectPath := args[:j], args[j+1:]
buildID, err := buildidOf(objectPath)
if err != nil {
return err
return "", err
}
// log.Println("buildid:", buildID)
if len(buildInfo.imports) == 0 {
buildInfo.firstImport = importPath
}
actionID := decodeHash(splitActionID(buildID))
impPkg := importedPkg{
packagefile: objectPath,
actionID: decodeHash(splitActionID(buildID)),
actionID: actionID,
}
buildInfo.imports[importPath] = impPkg
@ -829,7 +869,30 @@ func fillBuildInfo(flags []string) error {
}
}
// log.Printf("%#v", buildInfo)
return nil
// Produce the modified importcfg file.
newCfg, err := ioutil.TempFile(sharedTempDir, "importcfg")
if err != nil {
return "", err
}
for beforePath, afterPath := range importMap {
if isPrivate(afterPath) {
actionID := buildInfo.imports[afterPath].actionID
afterPath = hashWith(actionID, afterPath)
}
fmt.Fprintf(newCfg, "importmap %s=%s\n", beforePath, afterPath)
}
for impPath, pkg := range buildInfo.imports {
if isPrivate(impPath) {
actionID := buildInfo.imports[impPath].actionID
impPath = hashWith(actionID, impPath)
}
fmt.Fprintf(newCfg, "packagefile %s=%s\n", impPath, pkg.packagefile)
}
if err := newCfg.Close(); err != nil {
return "", err
}
return newCfg.Name(), nil
}
// recordReflectArgs collects all the objects in a package which are known to be
@ -1175,26 +1238,14 @@ func isTestSignature(sign *types.Signature) bool {
return obj != nil && obj.Pkg().Path() == "testing" && obj.Name() == "T"
}
func transformLink(args []string) ([]string, func() error, error) {
func transformLink(args []string) ([]string, error) {
// We can't split by the ".a" extension, because cached object files
// lack any extension.
flags, paths := splitFlagsFromArgs(args)
if err := fillBuildInfo(flags); err != nil {
return nil, nil, err
}
flags, args := splitFlagsFromArgs(args)
// there should only ever be one archive/object file passed to the linker,
// the file for the main package or entrypoint
if len(paths) != 1 {
return nil, nil, fmt.Errorf("expected exactly one link argument")
}
importMap := func(importPath string) (objectPath string) {
return buildInfo.imports[importPath].packagefile
}
garbledObj, err := obfuscateImports(paths[0], importMap)
newImportCfg, err := fillBuildInfo(flags)
if err != nil {
return nil, nil, err
return nil, err
}
// Make sure -X works with garbled identifiers. To cover both garbled
@ -1222,9 +1273,13 @@ func transformLink(args []string) ([]string, func() error, error) {
}
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))
newPkg := pkg
if pkg != "main" && isPrivate(pkg) {
newPkg = hashWith(id, pkg)
}
flags = append(flags, fmt.Sprintf("-X=%s.%s=%s", newPkg, newName, str))
})
// fmt.Println(flags)
// Ensure we strip the -buildid flag, to not leak any build IDs for the
// link operation or the main package's compilation.
@ -1232,7 +1287,9 @@ func transformLink(args []string) ([]string, func() error, error) {
// Strip debug information and symbol tables.
flags = append(flags, "-w", "-s")
return append(flags, garbledObj), nil, nil
flags = flagSetValue(flags, "-importcfg", newImportCfg)
return append(flags, args...), nil
}
func splitFlagsFromArgs(all []string) (flags, args []string) {

@ -12,7 +12,7 @@ exists 'test1/test/main/imported/imported.go' 'test1/main/main.go'
cp $WORK/test1/main/main.go $WORK/test1/some_file_from_prev_build.go
garble -debugdir ./test1 build -v
! stderr 'test/main'
stderr 'test/main' # we force rebuilds with -debugdir
! exists $WORK/test1/some_file_from_prev_build.go
-- go.mod --

@ -20,7 +20,7 @@ garble build -tags buildtag
exec ./main
cmp stdout main.stdout
! binsubstr main$exe 'ImportedVar' 'ImportedConst' 'ImportedFunc' 'ImportedType' 'main.go' 'test/main' 'imported.' 'NormalStruct' 'NormalExportedField' 'normalUnexportedField'
! binsubstr main$exe 'ImportedVar' 'ImportedConst' 'ImportedFunc' 'ImportedType' 'main.go' 'test/main' 'importedpkg.' 'NormalStruct' 'NormalExportedField' 'normalUnexportedField'
binsubstr main$exe 'ReflectInDefined' 'ExportedField2' 'unexportedField2'
[short] stop # checking that the build is reproducible is slow
@ -68,26 +68,26 @@ import (
"reflect"
"strings"
"test/main/imported"
"test/main/importedpkg"
"rsc.io/quote"
garbletest "gopkg.in/garbletest.v2"
)
func main() {
fmt.Println(imported.ImportedVar)
fmt.Println(imported.ImportedConst)
fmt.Println(imported.ImportedFunc('x'))
fmt.Println(imported.ImportedType(3))
fmt.Println(imported.ReflectInDefinedVar.ExportedField2)
fmt.Println(imported.ReflectInDefined{ExportedField2: 5})
normal := imported.NormalStruct{SharedName: 3}
fmt.Println(importedpkg.ImportedVar)
fmt.Println(importedpkg.ImportedConst)
fmt.Println(importedpkg.ImportedFunc('x'))
fmt.Println(importedpkg.ImportedType(3))
fmt.Println(importedpkg.ReflectInDefinedVar.ExportedField2)
fmt.Println(importedpkg.ReflectInDefined{ExportedField2: 5})
normal := importedpkg.NormalStruct{SharedName: 3}
normal.IndirectStruct.Field = 23
fmt.Println(normal)
printfWithoutPackage("%T\n", imported.ReflectTypeOf(2))
printfWithoutPackage("%T\n", imported.ReflectTypeOfIndirect(4))
printfWithoutPackage("%T\n", importedpkg.ReflectTypeOf(2))
printfWithoutPackage("%T\n", importedpkg.ReflectTypeOfIndirect(4))
v := imported.ReflectValueOfVar
v := importedpkg.ReflectValueOfVar
printfWithoutPackage("%#v\n", v)
method := reflect.ValueOf(&v).MethodByName("ExportedMethodName")
if method.IsValid() {
@ -117,12 +117,12 @@ package main
import "fmt"
func init() { fmt.Println("buildtag init func") }
-- imported/imported.go --
package imported
-- importedpkg/imported.go --
package importedpkg
import (
"reflect"
"test/main/imported/indirect"
"test/main/importedpkg/indirect"
)
var ImportedVar = "imported var value"
@ -174,7 +174,7 @@ type NormalStruct struct {
// ImportedType comes after the calls to reflect, to ensure no false positives.
type ImportedType int
-- imported/indirect/indirect.go --
-- importedpkg/indirect/indirect.go --
package indirect
type Indirect struct {

@ -1,3 +1,5 @@
skip # see https://github.com/burrowers/garble/issues/241
# Note that we need bar_test too.
env GOPRIVATE=test/bar,test/bar_test

@ -6,7 +6,11 @@ garble -tiny build
env GODEBUG='allocfreetrace=1,gcpacertrace=1,gctrace=1,scavenge=1,scavtrace=1,scheddetail=1,schedtrace=10'
! exec ./main$exe
stderr '^\(0x[\d\w]{6,8},0x[\d\w]{6,8}\)' # interfaces/pointers print correctly
stderr '^caller: \? 0$' # position info is removed
# TODO: Make -tiny remove all line information again.
# Right now, we reset each declaration's start line to 1.
# Better than nothing, but we could still make *all* line numbers 1.
# stderr '^caller: \? 0$' # position info is removed
stderr '^caller: \?\? ' # position info is removed
stderr '^recovered: ya like jazz?'
! stderr 'panic: oh noes' # panics are hidden

Loading…
Cancel
Save