Files
garble/reverse.go
T
Daniel Martí 5c2470f075 clean up uses of encoding/gob and go/ssa
encoding/gob.Decoder already has "map copy" semantics;
any decoded map keys get inserted, and other existing map keys are left.
This is exactly the same behavior that the new code did with maps.Copy,
so the new code is unnecessary.

If there is any bug with this code, it needs to be shown via a test.
As far as I can tell, there is no bug, because no two packages
should clash in terms of map keys.

Add a helper to obtain a go/ssa.Function's origin if there is one,
falling back to itself if there is none.
This mimics how go/types.Func.Origin works,
and lets us simplify quite a bit of code.

Finally, fieldToStruct uses origin objects only,
so don't do a double lookup in reverse.go.

While here, fix up the func mentions in two type aliases.
2026-03-02 00:29:12 +01:00

203 lines
5.7 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.Fprint(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:]
// We don't actually run `go list -toolexec=garble`; we only use toolexecCmd
// to ensure that sharedCache.ListedPackages is filled.
_, err := toolexecCmd("list", append(flags, pkg))
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)
// Assembly filenames are obfuscated in a simple way.
// Mirroring [transformer.transformAsm]; note the lack of a test
// as so far this has only mattered for build errors with positions.
for _, name := range lpkg.SFiles {
newName := hashWithPackage(lpkg, name) + ".s"
replaces = append(replaces, newName, name)
}
files, err := parseFiles(lpkg, 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]
for node := range ast.Preorder(file) {
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
}
originObj := obj.Origin()
strct := fieldToStruct[originObj]
if strct == nil {
panic("could not find struct for field " + name.Name)
}
replaces = append(replaces, hashWithStruct(strct, originObj), 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),
)
}
}
}
}
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
}
}
}