You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/import_obfuscation.go

706 lines
20 KiB
Go

// Copyright (c) 2020, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"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 accidently match another import, such
// as a stdlib package
type privateImports struct {
privatePaths []string
privateNames []string
}
func appendPrivateNameMap(pkg *goobj2.Package, nameMap map[string]string) error {
for _, member := range pkg.ArchiveMembers {
if member.ArchiveHeader.Name != garbleMapHeaderName {
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
}
// 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, tempDir, importCfgPath string) (garbledObj string, garbledImports, privateNameMap map[string]string, _ error) {
importCfg, err := goobj2.ParseImportCfg(importCfgPath)
if err != nil {
return "", nil, nil, err
}
mainPkg, err := goobj2.Parse(objPath, "main", importCfg)
if err != nil {
return "", nil, nil, fmt.Errorf("error parsing main objfile: %v", err)
}
pkgs := []pkgInfo{{mainPkg, objPath, true}}
privateNameMap = make(map[string]string)
// build list of imported packages that are private
for pkgPath, info := range importCfg {
// if the '-tiny' flag is passed, we will strip filename
// and position info of every package, but not garble anything
if private := isPrivate(pkgPath); envGarbleTiny || private {
pkg, err := goobj2.Parse(info.Path, pkgPath, importCfg)
if err != nil {
return "", nil, nil, 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)
}
}
}
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)
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 {
if !isPrivate(imp) {
return imp
}
privPaths, privNames := explodeImportPath(imp)
privImports.privatePaths = append(privImports.privatePaths, privPaths...)
privImports.privateNames = append(privImports.privateNames, privNames...)
return hashImport(imp, garbledImports)
}
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 = dedupStrings(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] > privImports.privateNames[j]
})
// 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, garbledImports, &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(tempDir, "pkg.*.a")
if err != nil {
return "", nil, nil, 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)
}
replacedFiles[p.path] = tempObj
}
// garble importcfg so the linker knows where to find garbled imports
if err := garbleImportCfg(importCfgPath, importCfg, garbledImports, replacedFiles); err != nil {
return "", nil, nil, err
}
return replacedFiles[objPath], garbledImports, privateNameMap, 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 {
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]
// TODO: last element returned should get same buildID
// as full path?
// ie github.com/foo/bar.buildID == bar.buildID
func explodeImportPath(path string) ([]string, []string) {
paths := strings.Split(path, "/")
if len(paths) == 1 {
return []string{path}, nil
}
pkgPaths := make([]string, 0, len(paths)-1)
pkgNames := make([]string, 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)
pkgNames = append(pkgNames, paths[i])
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)
pkgNames = append(pkgNames, paths[i])
lastComboIdx++
}
pkgNames = append(pkgNames, paths[len(paths)-1])
return pkgPaths, pkgNames
}
func dedupStrings(paths []string) []string {
seen := make(map[string]struct{}, len(paths))
j := 0
for _, v := range paths {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
paths[j] = v
j++
}
return paths[:j]
}
// TODO: possible that package collisions can occur; for instance, if
// 'github.com/thingy/foo' and 'bar/baz/foo/zip' were both private imports
// of the same object, 'foo' would be added to as a private import
// twice, due to the logic of importPathCombos(). There needs to be some
// way to differentiate between 'foo' of 'github.com/thingy/foo' and
// 'bar/baz/foo/zip' so the same buildID is not used, which would create
// an identical hash.
func hashImport(pkg string, garbledImports map[string]string) string {
if garbledPkg, ok := garbledImports[pkg]; ok {
return garbledPkg
}
initial support for build caching (#142) As per the discussion in https://github.com/golang/go/issues/41145, it turns out that we don't need special support for build caching in -toolexec. We can simply modify the behavior of "[...]/compile -V=full" and "[...]/link -V=full" so that they include garble's own version and options in the printed build ID. The part of the build ID that matters is the last, since it's the "content ID" which is used to work out whether there is a need to redo the action (build) or not. Since cmd/go parses the last word in the output as "buildID=...", we simply add "+garble buildID=_/_/_/${hash}". The slashes let us imitate a full binary build ID, but we assume that the other components such as the action ID are not necessary, since the only reader here is cmd/go and it only consumes the content ID. The reported content ID includes the tool's original content ID, garble's own content ID from the built binary, and the garble options which modify how we obfuscate code. If any of the three changes, we should use a different build cache key. GOPRIVATE also affects caching, since a different GOPRIVATE value means that we might have to garble a different set of packages. Include tests, which mainly check that 'garble build -v' prints package lines when we expect to always need to rebuild packages, and that it prints nothing when we should be reusing the build cache even when the built binary is missing. After this change, 'go test' on Go 1.15.2 stabilizes at about 8s on my machine, whereas it used to be at around 25s before.
4 years ago
garbledPkg := hashWith(buildInfo.imports[pkg].actionID, pkg)
garbledImports[pkg] = garbledPkg
return garbledPkg
}
// 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) {
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
// add the symbol name to a blacklist, so we don't garble related symbols
// 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, garbledImports, dataTyp, buf)
if s.Size != 0 {
s.Size = uint32(len(s.Data))
}
}
s.Name = garbleSymbolName(s.Name, privImports, garbledImports, sb)
for i := range s.Reloc {
s.Reloc[i].Name = garbleSymbolName(s.Reloc[i].Name, privImports, garbledImports, sb)
}
if s.Type != nil {
s.Type.Name = garbleSymbolName(s.Type.Name, privImports, garbledImports, 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)
}
for _, inl := range s.Func.InlTree {
inl.Func.Name = garbleSymbolName(inl.Func.Name, privImports, garbledImports, sb)
if envGarbleTiny {
inl.Line = 1
}
}
if envGarbleTiny {
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, garbledImports, 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, garbledImports map[string]string, sb *strings.Builder) string {
prefix, name, skipSym := splitSymbolPrefix(symName)
if skipSym {
// log.Printf("\t\t? Skipped symbol: %s", symName)
return symName
}
var namedataSym bool
if prefix == "type..namedata." {
namedataSym = true
}
var off int
for {
o, l := 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], garbledImports))
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.
func privateImportIndex(symName string, privImports privateImports, nameDataSym bool) (int, int) {
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
}
firstOff, l := -1, 0
for _, privatePkg := range privImports.privatePaths {
off := matchPkg(privatePkg)
if off == -1 {
continue
} else if off < firstOff || firstOff == -1 {
firstOff = off
l = len(privatePkg)
}
}
if nameDataSym {
for _, privateName := 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(privateName + ".")
if off == -1 {
continue
} else if off < firstOff || firstOff == -1 {
firstOff = off
l = len(privateName)
}
}
}
return firstOff, l
}
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, garbledImports map[string]string, 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 := 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]), garbledImports))
}
buf.Write(symData[off : off+o])
buf.WriteString(hashImport(string(symData[off+o:off+o+l]), garbledImports))
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, garbledImports, 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, info := range importCfg {
if isPrivate(pkgPath) {
pkgPath = hashImport(pkgPath, garbledImports)
}
if info.IsSharedLib {
newCfgWr.WriteString("packageshlib")
} else {
newCfgWr.WriteString("packagefile")
}
newCfgWr.WriteRune(' ')
newCfgWr.WriteString(pkgPath)
newCfgWr.WriteRune('=')
if replaced := replacedFiles[info.Path]; replaced != "" {
newCfgWr.WriteString(replaced)
} else {
newCfgWr.WriteString(info.Path)
}
newCfgWr.WriteRune('\n')
}
if err := newCfgWr.Flush(); err != nil {
return fmt.Errorf("error writing importcfg: %v", err)
}
return nil
}