initial support for reversing panic output (#225)
For now, this only implements reversing of exported names which are hashed with action IDs. Many other kinds of obfuscation, like positions and private names, are not yet implemented. Note that we don't document this new command yet on purpose, since it's not finished. Some other minor cleanups were done for future changes, such as making transformLineInfo into a method that also receives the original filename, and making header names more self-describing. Updates #5.pull/227/head
parent
d33faabb94
commit
d8e8738216
@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2019, The Garble Authors.
|
||||
// See LICENSE for licensing information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// commandReverse implements "garble reverse".
|
||||
func commandReverse(args []string) error {
|
||||
flags, args := splitFlagsFromArgs(args)
|
||||
mainPkg := "."
|
||||
if len(args) > 0 {
|
||||
mainPkg = args[0]
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
listArgs := []string{
|
||||
"-json",
|
||||
"-deps",
|
||||
"-export",
|
||||
}
|
||||
listArgs = append(listArgs, flags...)
|
||||
listArgs = append(listArgs, mainPkg)
|
||||
cmd, err := toolexecCmd("list", listArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("go list error: %v", err)
|
||||
}
|
||||
mainPkgPath := ""
|
||||
dec := json.NewDecoder(stdout)
|
||||
var privatePkgPaths []string
|
||||
for dec.More() {
|
||||
var pkg listedPackage
|
||||
if err := dec.Decode(&pkg); err != nil {
|
||||
return err
|
||||
}
|
||||
if pkg.Export == "" {
|
||||
continue
|
||||
}
|
||||
if pkg.Name == "main" {
|
||||
if mainPkgPath != "" {
|
||||
return fmt.Errorf("found two main packages: %s %s", mainPkgPath, pkg.ImportPath)
|
||||
}
|
||||
mainPkgPath = pkg.ImportPath
|
||||
}
|
||||
if isPrivate(pkg.ImportPath) {
|
||||
privatePkgPaths = append(privatePkgPaths, pkg.ImportPath)
|
||||
}
|
||||
buildID, err := buildidOf(pkg.Export)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Adding it to buildInfo.imports allows us to reuse the
|
||||
// "if" branch below. Plus, if this edge case triggers
|
||||
// multiple times in a single package compile, we can
|
||||
// call "go list" once and cache its result.
|
||||
buildInfo.imports[pkg.ImportPath] = importedPkg{
|
||||
packagefile: pkg.Export,
|
||||
actionID: decodeHash(splitActionID(buildID)),
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return fmt.Errorf("go list error: %v: %s", err, stderr.Bytes())
|
||||
}
|
||||
|
||||
var replaces []string
|
||||
|
||||
for _, pkgPath := range privatePkgPaths {
|
||||
ipkg := buildInfo.imports[pkgPath]
|
||||
|
||||
// All original exported names names are hashed with the
|
||||
// obfuscated package's action ID.
|
||||
tpkg, err := origImporter.Import(pkgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkgScope := tpkg.Scope()
|
||||
for _, name := range pkgScope.Names() {
|
||||
obj := pkgScope.Lookup(name)
|
||||
if !obj.Exported() {
|
||||
continue
|
||||
}
|
||||
replaces = append(replaces, hashWith(ipkg.actionID, name), name)
|
||||
}
|
||||
}
|
||||
repl := strings.NewReplacer(replaces...)
|
||||
|
||||
// TODO: return a non-zero status code if we could not reverse any string.
|
||||
if len(args) == 0 {
|
||||
return reverseContent(os.Stdout, os.Stdin, repl)
|
||||
}
|
||||
for _, path := range args {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := reverseContent(os.Stdout, f, repl); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Close() // since we're in a loop
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reverseContent(w io.Writer, r io.Reader, repl *strings.Replacer) 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)
|
||||
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')
|
||||
if _, err := repl.WriteString(w, line); err != nil {
|
||||
return err
|
||||
}
|
||||
if readErr == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
env GOPRIVATE=test/main
|
||||
|
||||
# Unknown build flags should result in errors.
|
||||
! garble reverse -badflag
|
||||
stderr 'flag provided but not defined'
|
||||
|
||||
garble build
|
||||
exec ./main
|
||||
cp stderr main.stderr
|
||||
exec cat main.stderr
|
||||
|
||||
# Ensure that the garbled panic output looks correct.
|
||||
# This output is not reproducible between 'go test' runs,
|
||||
# so we can't use a static golden file.
|
||||
grep 'goroutine 1 \[running\]' main.stderr
|
||||
! grep 'SomeFunc|test/main|main.go|lib.go' main.stderr
|
||||
|
||||
stdin main.stderr
|
||||
garble reverse
|
||||
stdout -count=1 'SomeFunc'
|
||||
# TODO: this is what we want when "reverse" is finished
|
||||
# cmp stdout reverse.stdout
|
||||
|
||||
# Ensure that the reversed output matches the non-garbled output.
|
||||
go build -trimpath
|
||||
exec ./main
|
||||
cmp stderr reverse.stdout
|
||||
|
||||
-- go.mod --
|
||||
module test/main
|
||||
|
||||
go 1.15
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import "test/main/lib"
|
||||
|
||||
func main() {
|
||||
lib.SomeFunc()
|
||||
}
|
||||
-- lib/lib.go --
|
||||
package lib
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func SomeFunc() {
|
||||
// Panic outputs include "0xNN" pointers and offsets which change
|
||||
// between platforms.
|
||||
// Strip them out here, to have portable static stdout files.
|
||||
rxVariableSuffix := regexp.MustCompile(`0x[0-9a-f]+`)
|
||||
|
||||
stack := debug.Stack()
|
||||
stack = rxVariableSuffix.ReplaceAll(stack, []byte("0x??"))
|
||||
os.Stderr.Write(stack)
|
||||
}
|
||||
-- reverse.stdout --
|
||||
goroutine 1 [running]:
|
||||
runtime/debug.Stack(0x??, 0x??, 0x??)
|
||||
runtime/debug/stack.go:24 +0x??
|
||||
test/main/lib.SomeFunc()
|
||||
test/main/lib/lib.go:15 +0x??
|
||||
main.main()
|
||||
test/main/main.go:6 +0x??
|
Loading…
Reference in New Issue