From a64592915126e848a99f30648c0b67829ec1e2a0 Mon Sep 17 00:00:00 2001 From: lu4p Date: Sat, 27 Nov 2021 18:30:09 +0100 Subject: [PATCH] obfuscate literals via constant folding Constants don't need to be added to ignoreObjs anymore, because go/types now does this work for us. Fixes #360 --- go.mod | 5 +- go.sum | 21 ++-- internal/literals/literals.go | 208 ++++++++++++------------------- internal/literals/obfuscators.go | 2 - main.go | 4 - testdata/scripts/literals.txt | 13 +- 6 files changed, 96 insertions(+), 157 deletions(-) diff --git a/go.mod b/go.mod index 2cd50e3..7b4e46d 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,13 @@ go 1.17 require ( github.com/google/go-cmp v0.5.6 github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 - golang.org/x/mod v0.5.0 - golang.org/x/tools v0.1.5 + golang.org/x/mod v0.5.1 + golang.org/x/tools v0.1.7 ) require ( github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/errgo.v2 v2.1.0 // indirect ) diff --git a/go.sum b/go.sum index 21ed33e..65b1e27 100644 --- a/go.sum +++ b/go.sum @@ -7,33 +7,32 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w= github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/literals/literals.go b/internal/literals/literals.go index 454232e..1cc1a46 100644 --- a/internal/literals/literals.go +++ b/internal/literals/literals.go @@ -6,6 +6,7 @@ package literals import ( "fmt" "go/ast" + "go/constant" "go/token" "go/types" mathrand "math/rand" @@ -38,49 +39,45 @@ func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, ignoreObj pre := func(cursor *astutil.Cursor) bool { switch x := cursor.Node().(type) { case *ast.GenDecl: - if x.Tok != token.CONST { - return true + // constants are obfuscated by replacing all references with the obfuscated value + if x.Tok == token.CONST { + return false } - for _, spec := range x.Specs { - spec := spec.(*ast.ValueSpec) // guaranteed for Tok==CONST - if len(spec.Values) == 0 { - // skip constants with inferred values - return false - } + } + return true + } - for _, name := range spec.Names { - obj := info.ObjectOf(name) + post := func(cursor *astutil.Cursor) bool { + node, ok := cursor.Node().(ast.Expr) + if !ok { + return true + } - // We only obfuscate const declarations with typed string values. - if obj.Type() != types.Typ[types.String] { - return false - } + typeAndValue := info.Types[node] + if !typeAndValue.IsValue() { + return true + } - // The object cannot be obfuscated, e.g. a value that needs to be constant - if ignoreObj[obj] { - return false - } - } + if typeAndValue.Type == types.Typ[types.String] && typeAndValue.Value != nil { + value := constant.StringVal(typeAndValue.Value) + if len(value) == 0 || len(value) > maxSizeBytes { + return true } - x.Tok = token.VAR - // constants are not possible if we want to obfuscate literals, therefore - // move all constant blocks which only contain strings to variables - } - return true - } + cursor.Replace(withPos(obfuscateString(value), node.Pos())) - post := func(cursor *astutil.Cursor) bool { - switch x := cursor.Node().(type) { - case *ast.CompositeLit: - byteType := types.Universe.Lookup("byte").Type() + return true + } - if len(x.Elts) == 0 || len(x.Elts) > maxSizeBytes { + 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(x.Type).(type) { + switch y := info.TypeOf(node.Type).(type) { case *types.Array: if y.Elem() != byteType { return true @@ -97,74 +94,73 @@ func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, ignoreObj return true } - data := make([]byte, 0, len(x.Elts)) + data := make([]byte, 0, len(node.Elts)) - for _, el := range x.Elts { - lit, ok := el.(*ast.BasicLit) - if !ok { + for _, el := range node.Elts { + elType := info.Types[el] + + if elType.Value == nil || elType.Value.Kind() != constant.Int { return true } - var value byte - if lit.Kind == token.CHAR { - val, err := strconv.Unquote(lit.Value) - if err != nil { - panic(fmt.Sprintf("cannot unquote character: %v", err)) - } - - value = byte(val[0]) - } else { - val, err := strconv.ParseUint(lit.Value, 0, 8) - if err != nil { - panic(fmt.Sprintf("cannot parse integer: %v", err)) - } - - value = byte(val) + + value, ok := constant.Uint64Val(elType.Value) + if !ok { + panic(fmt.Sprintf("cannot parse byte value: %v", elType.Value)) } - data = append(data, value) + data = append(data, byte(value)) } if arrayLen > 0 { - cursor.Replace(withPos(obfuscateByteArray(data, arrayLen), x.Pos())) + cursor.Replace(withPos(obfuscateByteArray(data, arrayLen), node.Pos())) } else { - cursor.Replace(withPos(obfuscateByteSlice(data), x.Pos())) + cursor.Replace(withPos(obfuscateByteSlice(data), node.Pos())) } + } - return true - - case *ast.BasicLit: - switch cursor.Name() { - case "Values", "Rhs", "Value", "Args", "X", "Y", "Results", "Elts": - default: - return true // we don't want to obfuscate imports etc. - } + return true + } - if x.Kind != token.STRING { - return true - } - if len(x.Value) > maxSizeBytes { - return true - } - typeInfo := info.TypeOf(x) - if typeInfo != types.Typ[types.String] && typeInfo != types.Typ[types.UntypedString] { - return true - } - value, err := strconv.Unquote(x.Value) - if err != nil { - panic(fmt.Sprintf("cannot unquote string: %v", err)) - } + // 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) + } - if len(value) == 0 { - return true - } + file = astutil.Apply(file, pre, post).(*ast.File) - cursor.Replace(withPos(obfuscateString(value), x.Pos())) + // 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 } - return true + if !astutil.DeleteImport(fset, file, path) { + panic(fmt.Sprintf("cannot delete unused import: %v", path)) + } } - return astutil.Apply(file, pre, post).(*ast.File) + return file } // withPos sets any token.Pos fields under node which affect printing to pos. @@ -266,53 +262,3 @@ func obfuscateByteArray(data []byte, length int64) *ast.CallExpr { return ah.LambdaCall(arrayType, block) } - -// RecordUsedAsConstants records identifiers used in constant expressions. -func RecordUsedAsConstants(node ast.Node, info *types.Info, ignoreObj map[types.Object]bool) { - visit := func(node ast.Node) bool { - ident, ok := node.(*ast.Ident) - if !ok { - return true - } - - // Only record *types.Const objects. - // Other objects, such as builtins or type names, - // must not be recorded as they would be false positives. - obj := info.ObjectOf(ident) - if _, ok := obj.(*types.Const); ok { - ignoreObj[obj] = true - } - - return true - } - - switch x := node.(type) { - // in a slice or array composite literal all explicit keys must be constant representable - case *ast.CompositeLit: - if _, ok := x.Type.(*ast.ArrayType); !ok { - break - } - for _, elt := range x.Elts { - if kv, ok := elt.(*ast.KeyValueExpr); ok { - ast.Inspect(kv.Key, visit) - } - } - // in an array type the length must be a constant representable - case *ast.ArrayType: - if x.Len != nil { - ast.Inspect(x.Len, visit) - } - // in a const declaration all values must be constant representable - case *ast.GenDecl: - if x.Tok != token.CONST { - break - } - for _, spec := range x.Specs { - spec := spec.(*ast.ValueSpec) - - for _, val := range spec.Values { - ast.Inspect(val, visit) - } - } - } -} diff --git a/internal/literals/obfuscators.go b/internal/literals/obfuscators.go index d7cf286..f94b42e 100644 --- a/internal/literals/obfuscators.go +++ b/internal/literals/obfuscators.go @@ -8,7 +8,6 @@ import ( "go/ast" "go/token" mathrand "math/rand" - "os" ) // obfuscator takes a byte slice and converts it to a ast.BlockStmt @@ -25,7 +24,6 @@ var ( shuffle{}, seed{}, } - envGarbleSeed = os.Getenv("GARBLE_SEED") ) // If math/rand.Seed() is not called, the generator behaves as if seeded by rand.Seed(1), diff --git a/main.go b/main.go index 864863e..624e087 100644 --- a/main.go +++ b/main.go @@ -1070,10 +1070,6 @@ func (tf *transformer) prefillIgnoreObjects(files []*ast.File) { tf.ignoreObjects = make(map[types.Object]bool) visit := func(node ast.Node) bool { - if opts.ObfuscateLiterals { - literals.RecordUsedAsConstants(node, tf.info, tf.ignoreObjects) - } - call, ok := node.(*ast.CallExpr) if !ok { return true diff --git a/testdata/scripts/literals.txt b/testdata/scripts/literals.txt index 178c3b3..cb7f602 100644 --- a/testdata/scripts/literals.txt +++ b/testdata/scripts/literals.txt @@ -4,8 +4,8 @@ garble -literals build exec ./main$exe cmp stderr main.stderr -binsubstr main$exe 'Skip this block' 'also skip this' 'skip typed const' 'skip typed var' 'skip typed var assign' 'stringTypeField strType' 'stringType lambda func return' 'testMap1 key' 'testMap2 key' 'testMap3 key' 'testMap1 value' 'testMap3 value' 'testMap1 new value' 'testMap3 new value' 'stringType func param' 'stringType return' 'skip untyped const' -! binsubstr main$exe 'garbleDecrypt' 'Lorem' 'dolor' 'first assign' 'second assign' 'First Line' 'Second Line' 'map value' 'obfuscated with shadowed builtins' '1: literal in' 'an array' '2: literal in' 'a slice' 'to obfuscate' 'also obfuscate' 'stringTypeField String' +binsubstr main$exe 'skip typed const' 'skip typed var' 'skip typed var assign' 'stringTypeField strType' 'stringType lambda func return' 'testMap2 key' 'testMap3 key' 'testMap1 value' 'testMap3 value' 'testMap1 new value' 'testMap3 new value' 'stringType func param' 'stringType return' 'skip untyped const' +! binsubstr main$exe 'garbleDecrypt' 'Lorem' 'dolor' 'first assign' 'second assign' 'First Line' 'Second Line' 'map value' 'obfuscated with shadowed builtins' '1: literal in' 'an array' '2: literal in' 'a slice' 'to obfuscate' 'also obfuscate' 'stringTypeField String' 'testMap1 key' 'Obfuscate this block' 'also obfuscate this' [short] stop # checking that the build is reproducible is slow @@ -78,14 +78,14 @@ const ( i = 1 boolean = true - skip1 = "Skip this block" + mixedBlock = "Obfuscate this block" ) const ( foo = iota bar - skip2 = "also skip this" + iotaBlock = "also obfuscate this" ) // We used to conver this to a var in an attempt of obfuscating the literal. @@ -131,7 +131,7 @@ func main() { testMap["map key"] = "new value" println(testMap["map key"]) println("another literal") - println(skip1, skip2) + println(mixedBlock, iotaBlock) println(i, foo, bar) typedTest() constantTest() @@ -267,7 +267,6 @@ func stringTypeFunc(s stringType) stringType { return "stringType return" // skip } - // obfuscating this broke before const ( iota0 uint8 = iota @@ -357,7 +356,7 @@ second assign to obfuscate also obfuscate new value another literal -Skip this block also skip this +Obfuscate this block also obfuscate this 1 0 1 skip untyped const skip typed const skip typed var skip typed var assign