You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
274 lines
6.9 KiB
Go
274 lines
6.9 KiB
Go
// Copyright (c) 2020, The Garble Authors.
|
|
// See LICENSE for licensing information.
|
|
|
|
package literals
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/token"
|
|
"go/types"
|
|
mathrand "math/rand"
|
|
"strconv"
|
|
|
|
"golang.org/x/tools/go/ast/astutil"
|
|
ah "mvdan.cc/garble/internal/asthelper"
|
|
)
|
|
|
|
// maxSizeBytes is the limit, in bytes, of the size of string-like literals
|
|
// which we will obfuscate. This is important, because otherwise garble can take
|
|
// a very long time to obfuscate huge code-generated literals, such as those
|
|
// corresponding to large assets.
|
|
//
|
|
// Note that this is the size of the literal in source code. For example, "\xab"
|
|
// counts as four bytes.
|
|
//
|
|
// If someone truly wants to obfuscate those, they should do that when they
|
|
// generate the code, not at build time. Plus, with Go 1.16 that technique
|
|
// should largely stop being used.
|
|
const maxSizeBytes = 2 << 10 // KiB
|
|
|
|
func randObfuscator() obfuscator {
|
|
randPos := mathrand.Intn(len(obfuscators))
|
|
return obfuscators[randPos]
|
|
}
|
|
|
|
// Obfuscate replaces literals with obfuscated anonymous functions.
|
|
func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, linkStrings map[types.Object]string) *ast.File {
|
|
pre := func(cursor *astutil.Cursor) bool {
|
|
switch node := cursor.Node().(type) {
|
|
case *ast.GenDecl:
|
|
// constants are obfuscated by replacing all references with the obfuscated value
|
|
if node.Tok == token.CONST {
|
|
return false
|
|
}
|
|
case *ast.ValueSpec:
|
|
for _, name := range node.Names {
|
|
obj := info.ObjectOf(name)
|
|
if _, e := linkStrings[obj]; e {
|
|
// Skip this entire ValueSpec to not break -ldflags=-X.
|
|
// TODO: support obfuscating those injected strings, too.
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
post := func(cursor *astutil.Cursor) bool {
|
|
node, ok := cursor.Node().(ast.Expr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
typeAndValue := info.Types[node]
|
|
if !typeAndValue.IsValue() {
|
|
return true
|
|
}
|
|
|
|
if typeAndValue.Type == types.Typ[types.String] && typeAndValue.Value != nil {
|
|
value := constant.StringVal(typeAndValue.Value)
|
|
if len(value) == 0 || len(value) > maxSizeBytes {
|
|
return true
|
|
}
|
|
|
|
cursor.Replace(withPos(obfuscateString(value), node.Pos()))
|
|
|
|
return true
|
|
}
|
|
|
|
if node, ok := node.(*ast.CompositeLit); ok {
|
|
if len(node.Elts) == 0 || len(node.Elts) > maxSizeBytes {
|
|
return true
|
|
}
|
|
|
|
byteType := types.Universe.Lookup("byte").Type()
|
|
|
|
var arrayLen int64
|
|
switch y := info.TypeOf(node.Type).(type) {
|
|
case *types.Array:
|
|
if y.Elem() != byteType {
|
|
return true
|
|
}
|
|
|
|
arrayLen = y.Len()
|
|
|
|
case *types.Slice:
|
|
if y.Elem() != byteType {
|
|
return true
|
|
}
|
|
|
|
default:
|
|
return true
|
|
}
|
|
|
|
data := make([]byte, 0, len(node.Elts))
|
|
|
|
for _, el := range node.Elts {
|
|
elType := info.Types[el]
|
|
|
|
if elType.Value == nil || elType.Value.Kind() != constant.Int {
|
|
return true
|
|
}
|
|
|
|
value, ok := constant.Uint64Val(elType.Value)
|
|
if !ok {
|
|
panic(fmt.Sprintf("cannot parse byte value: %v", elType.Value))
|
|
}
|
|
|
|
data = append(data, byte(value))
|
|
}
|
|
|
|
if arrayLen > 0 {
|
|
cursor.Replace(withPos(obfuscateByteArray(data, arrayLen), node.Pos()))
|
|
} else {
|
|
cursor.Replace(withPos(obfuscateByteSlice(data), node.Pos()))
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Imports which are used might be marked as unused by only looking at the ast,
|
|
// because packages can declare a name different from the last element of their import path.
|
|
//
|
|
// package main
|
|
//
|
|
// import "github.com/user/somepackage"
|
|
//
|
|
// func main(){
|
|
// // this line uses github.com/user/somepackage
|
|
// anotherpackage.Foo()
|
|
// }
|
|
//
|
|
// TODO: remove this check and detect used imports with go/types somehow
|
|
prevUsedImports := make(map[string]bool)
|
|
for _, imp := range file.Imports {
|
|
path, err := strconv.Unquote(imp.Path.Value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
prevUsedImports[path] = astutil.UsesImport(file, path)
|
|
}
|
|
|
|
file = astutil.Apply(file, pre, post).(*ast.File)
|
|
|
|
// some imported constants might not be needed anymore, remove unnessecary imports
|
|
for _, imp := range file.Imports {
|
|
path, err := strconv.Unquote(imp.Path.Value)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if !prevUsedImports[path] || astutil.UsesImport(file, path) {
|
|
continue
|
|
}
|
|
|
|
if !astutil.DeleteImport(fset, file, path) {
|
|
panic(fmt.Sprintf("cannot delete unused import: %v", path))
|
|
}
|
|
}
|
|
|
|
return file
|
|
}
|
|
|
|
// withPos sets any token.Pos fields under node which affect printing to pos.
|
|
// Note that we can't set all token.Pos fields, since some affect the semantics.
|
|
//
|
|
// This function is useful so that go/printer doesn't try to estimate position
|
|
// offsets, which can end up in printing comment directives too early.
|
|
//
|
|
// We don't set any "end" or middle positions, because they seem irrelevant.
|
|
func withPos(node ast.Node, pos token.Pos) ast.Node {
|
|
ast.Inspect(node, func(node ast.Node) bool {
|
|
switch node := node.(type) {
|
|
case *ast.BasicLit:
|
|
node.ValuePos = pos
|
|
case *ast.Ident:
|
|
node.NamePos = pos
|
|
case *ast.CompositeLit:
|
|
node.Lbrace = pos
|
|
node.Rbrace = pos
|
|
case *ast.ArrayType:
|
|
node.Lbrack = pos
|
|
case *ast.FuncType:
|
|
node.Func = pos
|
|
case *ast.BinaryExpr:
|
|
node.OpPos = pos
|
|
case *ast.StarExpr:
|
|
node.Star = pos
|
|
case *ast.CallExpr:
|
|
node.Lparen = pos
|
|
node.Rparen = pos
|
|
|
|
case *ast.GenDecl:
|
|
node.TokPos = pos
|
|
case *ast.ReturnStmt:
|
|
node.Return = pos
|
|
case *ast.ForStmt:
|
|
node.For = pos
|
|
case *ast.RangeStmt:
|
|
node.For = pos
|
|
case *ast.BranchStmt:
|
|
node.TokPos = pos
|
|
}
|
|
return true
|
|
})
|
|
return node
|
|
}
|
|
|
|
func obfuscateString(data string) *ast.CallExpr {
|
|
obfuscator := randObfuscator()
|
|
block := obfuscator.obfuscate([]byte(data))
|
|
|
|
block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(ast.NewIdent("string"), ast.NewIdent("data"))))
|
|
|
|
return ah.LambdaCall(ast.NewIdent("string"), block)
|
|
}
|
|
|
|
func obfuscateByteSlice(data []byte) *ast.CallExpr {
|
|
obfuscator := randObfuscator()
|
|
block := obfuscator.obfuscate(data)
|
|
block.List = append(block.List, ah.ReturnStmt(ast.NewIdent("data")))
|
|
return ah.LambdaCall(&ast.ArrayType{Elt: ast.NewIdent("byte")}, block)
|
|
}
|
|
|
|
func obfuscateByteArray(data []byte, length int64) *ast.CallExpr {
|
|
obfuscator := randObfuscator()
|
|
block := obfuscator.obfuscate(data)
|
|
|
|
arrayType := &ast.ArrayType{
|
|
Len: ah.IntLit(int(length)),
|
|
Elt: ast.NewIdent("byte"),
|
|
}
|
|
|
|
sliceToArray := []ast.Stmt{
|
|
&ast.DeclStmt{
|
|
Decl: &ast.GenDecl{
|
|
Tok: token.VAR,
|
|
Specs: []ast.Spec{&ast.ValueSpec{
|
|
Names: []*ast.Ident{ast.NewIdent("newdata")},
|
|
Type: arrayType,
|
|
}},
|
|
},
|
|
},
|
|
&ast.RangeStmt{
|
|
Key: ast.NewIdent("i"),
|
|
Tok: token.DEFINE,
|
|
X: ast.NewIdent("data"),
|
|
Body: &ast.BlockStmt{List: []ast.Stmt{
|
|
&ast.AssignStmt{
|
|
Lhs: []ast.Expr{ah.IndexExpr("newdata", ast.NewIdent("i"))},
|
|
Tok: token.ASSIGN,
|
|
Rhs: []ast.Expr{ah.IndexExpr("data", ast.NewIdent("i"))},
|
|
},
|
|
}},
|
|
},
|
|
ah.ReturnStmt(ast.NewIdent("newdata")),
|
|
}
|
|
|
|
block.List = append(block.List, sliceToArray...)
|
|
|
|
return ah.LambdaCall(arrayType, block)
|
|
}
|