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

600 lines
16 KiB
Go

// Copyright (c) 2020, The Garble Authors.
// See LICENSE for licensing information.
package main
import (
"bufio"
"bytes"
"fmt"
"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
}
// 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 obfuscateImports(objPath, importCfgPath string) (map[string]string, error) {
importCfg, err := goobj2.ParseImportCfg(importCfgPath)
if err != nil {
return nil, err
}
mainPkg, err := goobj2.Parse(objPath, "main", importCfg)
if err != nil {
return nil, fmt.Errorf("error parsing main objfile: %v", err)
}
privatePkgs := []pkgInfo{{mainPkg, objPath}}
// build list of imported packages that are private
for pkgPath, info := range importCfg {
if isPrivate(pkgPath) {
pkg, err := goobj2.Parse(info.Path, pkgPath, importCfg)
if err != nil {
return nil, fmt.Errorf("error parsing objfile %s at %s: %v", pkgPath, info.Path, err)
}
privatePkgs = append(privatePkgs, pkgInfo{pkg, info.Path})
}
}
var sb strings.Builder
var buf bytes.Buffer
garbledImports := make(map[string]string)
for _, p := range privatePkgs {
// 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
}
// 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)
}
if err := p.pkg.Write(p.path); err != nil {
return nil, fmt.Errorf("error writing objfile %s at %s: %v", p.pkg.ImportPath, p.path, err)
}
}
// garble importcfg so the linker knows where to find garbled imports
if err := garbleImportCfg(importCfgPath, importCfg, garbledImports); err != nil {
return nil, err
}
return garbledImports, 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
}
garbledPkg := hashWith(buildInfo.imports[pkg].buildID, 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.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)
}
// 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 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('=')
newCfgWr.WriteString(info.Path)
newCfgWr.WriteRune('\n')
}
if err := newCfgWr.Flush(); err != nil {
return fmt.Errorf("error writing importcfg: %v", err)
}
return nil
}