mirror of
https://github.com/burrowers/garble.git
synced 2025-10-05 07:46:59 +08:00

Some users had been running into "cannot load cache entry" errors, which could happen if garble's cache files in GOCACHE were removed when Go's own cache files were not. Now that we've moved to our own separate cache directory, and that we've refactored the codebase to depend less on globals and no longer assume that we're loading info for the current package, we can now compute a pkgCache entry for a dependency if needed. We add a pkgCache.CopyFrom method to be able to append map entries from one pkgCache to another without needing an encoding/gob roundtrip. We also add a parseFiles helper, since we now have three bits of code which need to parse a list of Go files from disk. Fixes #708.
203 lines
5.5 KiB
Go
203 lines
5.5 KiB
Go
// Copyright (c) 2019, The Garble Authors.
|
|
// See LICENSE for licensing information.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/types"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// commandReverse implements "garble reverse".
|
|
func commandReverse(args []string) error {
|
|
flags, args := splitFlagsFromArgs(args)
|
|
if hasHelpFlag(flags) || len(args) == 0 {
|
|
fmt.Fprintf(os.Stderr, `
|
|
usage: garble [garble flags] reverse [build flags] package [files]
|
|
|
|
For example, after building an obfuscated program as follows:
|
|
|
|
garble -literals build -tags=mytag ./cmd/mycmd
|
|
|
|
One can reverse a captured panic stack trace as follows:
|
|
|
|
garble -literals reverse -tags=mytag ./cmd/mycmd panic-output.txt
|
|
`[1:])
|
|
return errJustExit(2)
|
|
}
|
|
|
|
pkg, args := args[0], args[1:]
|
|
listArgs := []string{
|
|
"-json",
|
|
"-deps",
|
|
"-export",
|
|
}
|
|
listArgs = append(listArgs, flags...)
|
|
listArgs = append(listArgs, pkg)
|
|
// TODO: We most likely no longer need this "list -toolexec" call, since
|
|
// we use the original build IDs.
|
|
_, err := toolexecCmd("list", listArgs)
|
|
defer os.RemoveAll(os.Getenv("GARBLE_SHARED"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We don't actually run a main Go command with all flags,
|
|
// so if the user gave a non-build flag,
|
|
// we need this check to not silently ignore it.
|
|
if _, firstUnknown := filterForwardBuildFlags(flags); firstUnknown != "" {
|
|
// A bit of a hack to get a normal flag.Parse error.
|
|
// Longer term, "reverse" might have its own FlagSet.
|
|
return flag.NewFlagSet("", flag.ContinueOnError).Parse([]string{firstUnknown})
|
|
}
|
|
|
|
// A package's names are generally hashed with the action ID of its
|
|
// obfuscated build. We recorded those action IDs above.
|
|
// Note that we parse Go files directly to obtain the names, since the
|
|
// export data only exposes exported names. Parsing Go files is cheap,
|
|
// so it's unnecessary to try to avoid this cost.
|
|
var replaces []string
|
|
|
|
for _, lpkg := range sharedCache.ListedPackages {
|
|
if !lpkg.ToObfuscate {
|
|
continue
|
|
}
|
|
addHashedWithPackage := func(str string) {
|
|
replaces = append(replaces, hashWithPackage(lpkg, str), str)
|
|
}
|
|
|
|
// Package paths are obfuscated, too.
|
|
addHashedWithPackage(lpkg.ImportPath)
|
|
|
|
files, err := parseFiles(lpkg.Dir, lpkg.CompiledGoFiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
origImporter := importerForPkg(lpkg)
|
|
_, info, err := typecheck(lpkg.ImportPath, files, origImporter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fieldToStruct := computeFieldToStruct(info)
|
|
for i, file := range files {
|
|
goFile := lpkg.CompiledGoFiles[i]
|
|
ast.Inspect(file, func(node ast.Node) bool {
|
|
switch node := node.(type) {
|
|
|
|
// Replace names.
|
|
// TODO: do var names ever show up in output?
|
|
case *ast.FuncDecl:
|
|
addHashedWithPackage(node.Name.Name)
|
|
case *ast.TypeSpec:
|
|
addHashedWithPackage(node.Name.Name)
|
|
case *ast.Field:
|
|
for _, name := range node.Names {
|
|
obj, _ := info.ObjectOf(name).(*types.Var)
|
|
if obj == nil || !obj.IsField() {
|
|
continue
|
|
}
|
|
strct := fieldToStruct[obj]
|
|
if strct == nil {
|
|
panic("could not find struct for field " + name.Name)
|
|
}
|
|
replaces = append(replaces, hashWithStruct(strct, name.Name), name.Name)
|
|
}
|
|
|
|
case *ast.CallExpr:
|
|
// Reverse position information of call sites.
|
|
pos := fset.Position(node.Pos())
|
|
origPos := fmt.Sprintf("%s:%d", goFile, pos.Offset)
|
|
newFilename := hashWithPackage(lpkg, origPos) + ".go"
|
|
|
|
// Do "obfuscated.go:1", corresponding to the call site's line.
|
|
// Most common in stack traces.
|
|
replaces = append(replaces,
|
|
newFilename+":1",
|
|
fmt.Sprintf("%s/%s:%d", lpkg.ImportPath, goFile, pos.Line),
|
|
)
|
|
|
|
// Do "obfuscated.go" as a fallback.
|
|
// Most useful in build errors in obfuscated code,
|
|
// since those might land on any line.
|
|
// Any ":N" line number will end up being useless,
|
|
// but at least the filename will be correct.
|
|
replaces = append(replaces,
|
|
newFilename,
|
|
fmt.Sprintf("%s/%s", lpkg.ImportPath, goFile),
|
|
)
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
repl := strings.NewReplacer(replaces...)
|
|
|
|
if len(args) == 0 {
|
|
modified, err := reverseContent(os.Stdout, os.Stdin, repl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !modified {
|
|
return errJustExit(1)
|
|
}
|
|
return nil
|
|
}
|
|
// TODO: cover this code in the tests too
|
|
anyModified := false
|
|
for _, path := range args {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
modified, err := reverseContent(os.Stdout, f, repl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
anyModified = anyModified || modified
|
|
f.Close() // since we're in a loop
|
|
}
|
|
if !anyModified {
|
|
return errJustExit(1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func reverseContent(w io.Writer, r io.Reader, repl *strings.Replacer) (bool, error) {
|
|
// Read line by line.
|
|
// Reading the entire content at once wouldn't be interactive,
|
|
// nor would it support large files well.
|
|
// Reading entire lines ensures we don't cut words in half.
|
|
// We use bufio.Reader instead of bufio.Scanner,
|
|
// to also obtain the newline characters themselves.
|
|
br := bufio.NewReader(r)
|
|
modified := false
|
|
for {
|
|
// Note that ReadString can return a line as well as an error if
|
|
// we hit EOF without a newline.
|
|
// In that case, we still want to process the string.
|
|
line, readErr := br.ReadString('\n')
|
|
|
|
newLine := repl.Replace(line)
|
|
if newLine != line {
|
|
modified = true
|
|
}
|
|
if _, err := io.WriteString(w, newLine); err != nil {
|
|
return modified, err
|
|
}
|
|
if readErr == io.EOF {
|
|
return modified, nil
|
|
}
|
|
if readErr != nil {
|
|
return modified, readErr
|
|
}
|
|
}
|
|
}
|