rework the position obfuscator (#282)
First, rename line_obfuscator.go to position.go. We obfuscate filenames, not just line numbers, and "obfuscator" is a bit redundant. Second, use "/*line :x*/" comments rather than the "//line :x" form, as the former allows us to insert them in any position without adding unnecessary newlines. This will be important for changing the position of call sites, which will be important for "garble reverse". Third, do not rely on go/ast to remove and add comments. Since they are free-floating, we can very easily end up with misplaced comments, especially as the literal obfuscator heavily modifies the AST. The new method prints and re-parses the file, to ensure all node positions are consistent with a buffer, buf1. Then, we copy the contents into a new buffer, buf2, while inserting the comments that we need. The new method also modifies line numbers at the very end of obfuscating a Go file, instead of at the very beginning. That's going to be more robust long-term, as we will also obfuscate line numbers for any additions or modifications to the AST. Fourth, detachedDirectives is unnecessary, as we can accomplish the same with two simple prefix matches. Finally, this means we can stop using detachedComments entirely, as printFile already inserts the comments we need. For #5.pull/283/head
parent
ea19e39aa4
commit
961daf20c4
@ -1,137 +0,0 @@
|
|||||||
// Copyright (c) 2020, The Garble Authors.
|
|
||||||
// See LICENSE for licensing information.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PosMin is the smallest correct value for the line number.
|
|
||||||
// Source: https://go.googlesource.com/go/+/refs/heads/master/src/cmd/compile/internal/syntax/parser_test.go#229
|
|
||||||
const PosMin = 1
|
|
||||||
|
|
||||||
// detachedDirectives is a list of Go compiler directives which don't need to go
|
|
||||||
// right next to a Go declaration. Unlike all other detached comments, these
|
|
||||||
// need to be kept around as they alter compiler behavior.
|
|
||||||
var detachedDirectives = []string{
|
|
||||||
"// +build",
|
|
||||||
"//go:linkname",
|
|
||||||
"//go:cgo_ldflag",
|
|
||||||
"//go:cgo_dynamic_linker",
|
|
||||||
"//go:cgo_export_static",
|
|
||||||
"//go:cgo_export_dynamic",
|
|
||||||
"//go:cgo_import_static",
|
|
||||||
"//go:cgo_import_dynamic",
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDirective(text string, directives []string) bool {
|
|
||||||
for _, prefix := range directives {
|
|
||||||
if strings.HasPrefix(text, prefix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func prependComment(group *ast.CommentGroup, comment *ast.Comment) *ast.CommentGroup {
|
|
||||||
if group == nil {
|
|
||||||
return &ast.CommentGroup{List: []*ast.Comment{comment}}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.List = append([]*ast.Comment{comment}, group.List...)
|
|
||||||
return group
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all comments from CommentGroup except //go: directives.
|
|
||||||
// go:linkname directives are removed, since they're collected and rewritten
|
|
||||||
// separately.
|
|
||||||
func clearCommentGroup(group *ast.CommentGroup) *ast.CommentGroup {
|
|
||||||
if group == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var comments []*ast.Comment
|
|
||||||
for _, comment := range group.List {
|
|
||||||
if strings.HasPrefix(comment.Text, "//go:") && !strings.HasPrefix(comment.Text, "//go:linkname") {
|
|
||||||
comments = append(comments, &ast.Comment{Text: comment.Text})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(comments) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &ast.CommentGroup{List: comments}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all comments from Doc (if any) except //go: directives.
|
|
||||||
func clearNodeComments(node ast.Node) {
|
|
||||||
switch n := node.(type) {
|
|
||||||
case *ast.Field:
|
|
||||||
n.Doc = clearCommentGroup(n.Doc)
|
|
||||||
n.Comment = nil
|
|
||||||
case *ast.ImportSpec:
|
|
||||||
n.Doc = clearCommentGroup(n.Doc)
|
|
||||||
n.Comment = nil
|
|
||||||
case *ast.ValueSpec:
|
|
||||||
n.Doc = clearCommentGroup(n.Doc)
|
|
||||||
n.Comment = nil
|
|
||||||
case *ast.TypeSpec:
|
|
||||||
n.Doc = clearCommentGroup(n.Doc)
|
|
||||||
n.Comment = nil
|
|
||||||
case *ast.GenDecl:
|
|
||||||
n.Doc = clearCommentGroup(n.Doc)
|
|
||||||
case *ast.FuncDecl:
|
|
||||||
n.Doc = clearCommentGroup(n.Doc)
|
|
||||||
case *ast.File:
|
|
||||||
n.Doc = clearCommentGroup(n.Doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// transformLineInfo removes the comment except go directives and build tags. Converts comments to the node view.
|
|
||||||
// It returns comments not attached to declarations and names of declarations which cannot be renamed.
|
|
||||||
func (tf *transformer) transformLineInfo(file *ast.File, filename string) (detachedComments []string, f *ast.File) {
|
|
||||||
prefix := ""
|
|
||||||
if strings.HasPrefix(filename, "_cgo_") {
|
|
||||||
prefix = "_cgo_"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save build tags and add file name leak protection
|
|
||||||
for _, group := range file.Comments {
|
|
||||||
for _, comment := range group.List {
|
|
||||||
if isDirective(comment.Text, detachedDirectives) {
|
|
||||||
detachedComments = append(detachedComments, comment.Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
detachedComments = append(detachedComments, "", "//line "+prefix+":1")
|
|
||||||
file.Comments = nil
|
|
||||||
|
|
||||||
ast.Inspect(file, func(node ast.Node) bool {
|
|
||||||
clearNodeComments(node)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, decl := range file.Decls {
|
|
||||||
var doc **ast.CommentGroup
|
|
||||||
switch decl := decl.(type) {
|
|
||||||
case *ast.FuncDecl:
|
|
||||||
doc = &decl.Doc
|
|
||||||
case *ast.GenDecl:
|
|
||||||
doc = &decl.Doc
|
|
||||||
}
|
|
||||||
newName := ""
|
|
||||||
if !opts.Tiny {
|
|
||||||
origPos := fmt.Sprintf("%s:%d", filename, fset.Position(decl.Pos()).Offset)
|
|
||||||
newName = hashWith(curPkg.GarbleActionID, origPos) + ".go"
|
|
||||||
// log.Printf("%q hashed with %x to %q", origPos, curPkg.GarbleActionID, newName)
|
|
||||||
}
|
|
||||||
newPos := fmt.Sprintf("%s%s:1", prefix, newName)
|
|
||||||
|
|
||||||
comment := &ast.Comment{Text: "//line " + newPos}
|
|
||||||
*doc = prependComment(*doc, comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
return detachedComments, file
|
|
||||||
}
|
|
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (c) 2020, The Garble Authors.
|
||||||
|
// See LICENSE for licensing information.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/printer"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isDirective(text string) bool {
|
||||||
|
return strings.HasPrefix(text, "//go:") || strings.HasPrefix(text, "// +build")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printFile prints a Go file to a buffer, while also removing non-directive
|
||||||
|
// comments and adding extra compiler directives to obfuscate position
|
||||||
|
// information.
|
||||||
|
func printFile(file *ast.File) ([]byte, error) {
|
||||||
|
printConfig := printer.Config{Mode: printer.RawFormat}
|
||||||
|
|
||||||
|
var buf1 bytes.Buffer
|
||||||
|
if err := printConfig.Fprint(&buf1, fset, file); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
src := buf1.Bytes()
|
||||||
|
|
||||||
|
if !curPkg.Private {
|
||||||
|
// TODO(mvdan): make transformCompile handle non-private
|
||||||
|
// packages like runtime earlier on, to remove these checks.
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := fset.Position(file.Pos()).Filename
|
||||||
|
if strings.HasPrefix(filepath.Base(filename), "_cgo_") {
|
||||||
|
// cgo-generated files don't need changed line numbers.
|
||||||
|
// Plus, the compiler can complain rather easily.
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Many parts of garble, notably the literal obfuscator, modify the AST.
|
||||||
|
// Unfortunately, comments are free-floating in File.Comments,
|
||||||
|
// and those are the only source of truth that go/printer uses.
|
||||||
|
// So the positions of the comments in the given file are wrong.
|
||||||
|
// The only way we can get the final ones is to parse again.
|
||||||
|
file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the compiler directives, and change position info.
|
||||||
|
type commentToAdd struct {
|
||||||
|
offset int
|
||||||
|
text string
|
||||||
|
}
|
||||||
|
var toAdd []commentToAdd
|
||||||
|
addComment := func(offset int, text string) {
|
||||||
|
toAdd = append(toAdd, commentToAdd{offset, text})
|
||||||
|
}
|
||||||
|
addComment(0, "/*line :1*/")
|
||||||
|
for _, group := range file.Comments {
|
||||||
|
for _, comment := range group.List {
|
||||||
|
if isDirective(comment.Text) {
|
||||||
|
// TODO(mvdan): merge with the zeroing below
|
||||||
|
pos := fset.Position(comment.Pos())
|
||||||
|
addComment(pos.Offset, comment.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove all existing comments by making them whitespace.
|
||||||
|
for _, group := range file.Comments {
|
||||||
|
for _, comment := range group.List {
|
||||||
|
start := fset.Position(comment.Pos()).Offset
|
||||||
|
end := fset.Position(comment.End()).Offset
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
src[i] = ' '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, decl := range file.Decls {
|
||||||
|
newName := ""
|
||||||
|
if !opts.Tiny {
|
||||||
|
origPos := fmt.Sprintf("%s:%d", filename, fset.Position(decl.Pos()).Offset)
|
||||||
|
newName = hashWith(curPkg.GarbleActionID, origPos) + ".go"
|
||||||
|
// log.Printf("%q hashed with %x to %q", origPos, curPkg.GarbleActionID, newName)
|
||||||
|
}
|
||||||
|
newPos := fmt.Sprintf("%s:1", newName)
|
||||||
|
pos := fset.Position(decl.Pos())
|
||||||
|
|
||||||
|
// We use the /*text*/ form, since we can use multiple of them
|
||||||
|
// on a single line, and they don't require extra newlines.
|
||||||
|
addComment(pos.Offset, "/*line "+newPos+"*/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We add comments in order.
|
||||||
|
sort.Slice(toAdd, func(i, j int) bool {
|
||||||
|
return toAdd[i].offset < toAdd[j].offset
|
||||||
|
})
|
||||||
|
|
||||||
|
copied := 0
|
||||||
|
var buf2 bytes.Buffer
|
||||||
|
for _, comment := range toAdd {
|
||||||
|
buf2.Write(src[copied:comment.offset])
|
||||||
|
buf2.WriteString(comment.text)
|
||||||
|
if strings.HasPrefix(comment.text, "//") {
|
||||||
|
buf2.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
copied = comment.offset
|
||||||
|
}
|
||||||
|
buf2.Write(src[copied:])
|
||||||
|
return buf2.Bytes(), nil
|
||||||
|
}
|
Loading…
Reference in New Issue