forked from burrowers/garble
-
Notifications
You must be signed in to change notification settings - Fork 3
/
reverse.go
202 lines (181 loc) · 5.46 KB
/
reverse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// 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, obj), 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
}
}
}