From 077d02d43acf8bf5226e65758aa0595ae76c9d5e Mon Sep 17 00:00:00 2001 From: lu4p Date: Wed, 3 Jun 2020 21:44:47 +0200 Subject: [PATCH] add basic literal obfuscation, starting with strings Fixes #16. --- crypto.go | 42 +++++ main.go | 107 +++++++++--- strings.go | 324 +++++++++++++++++++++++++++++++++++ testdata/scripts/strings.txt | 73 ++++++++ 4 files changed, 524 insertions(+), 22 deletions(-) create mode 100644 crypto.go create mode 100644 strings.go create mode 100644 testdata/scripts/strings.txt diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..197c8d4 --- /dev/null +++ b/crypto.go @@ -0,0 +1,42 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "math/rand" +) + +// If math/rand.Seed() is not called, the generator behaves as if seeded by rand.Seed(1), +// so the generator is deterministic. + +// genAesKey generates a 128bit AES Key +func genAesKey() []byte { + return genRandBytes(16) +} + +// genAesKey generates a 128bit nonce +func genNonce() []byte { + return genRandBytes(12) +} + +// genRandBytes return a random []byte with the length of size +func genRandBytes(size int) []byte { + buffer := make([]byte, size) + rand.Read(buffer) // error is always nil so save to ignore + return buffer +} + +// encAes encrypt data with AesKey in AES gcm mode +func encAes(data []byte, AesKey []byte) ([]byte, error) { + block, _ := aes.NewCipher(AesKey) + nonce := genNonce() + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + ciphertext := aesgcm.Seal(nil, nonce, data, nil) + encData := append(nonce, ciphertext...) + return encData, nil +} diff --git a/main.go b/main.go index d9df8d0..555f775 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,12 @@ import ( var flagSet = flag.NewFlagSet("garble", flag.ContinueOnError) -func init() { flagSet.Usage = usage } +var garbleLiterals bool + +func init() { + flagSet.Usage = usage + flagSet.BoolVar(&garbleLiterals, "literals", false, "Encrypt all literals with AES, currently only literal strings are supported") +} func usage() { fmt.Fprintf(os.Stderr, ` @@ -43,8 +48,6 @@ instead of "go cmd [args]" to add obfuscation: build test - -garble does not have flags of its own at this moment. `[1:]) flagSet.PrintDefaults() os.Exit(2) @@ -56,40 +59,52 @@ var ( deferred []func() error fset = token.NewFileSet() - b64 = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_z") - printerConfig = printer.Config{Mode: printer.RawFormat} - origTypesConfig = types.Config{Importer: importer.ForCompiler(fset, "gc", origLookup)} + b64 = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_z") + printerConfig = printer.Config{Mode: printer.RawFormat} + + // listPackage helps implement a types.Importer which finds the export + // data for the original dependencies, not their garbled counterparts. + // This is useful to typecheck a package before it's garbled, so we can + // make decisions on how to garble it. + origTypesConfig = types.Config{Importer: importer.ForCompiler(fset, "gc", func(path string) (io.ReadCloser, error) { + pkg, err := listPackage(path) + if err != nil { + return nil, err + } + return os.Open(pkg.Export) + })} buildInfo = packageInfo{imports: make(map[string]importedPkg)} garbledImporter = importer.ForCompiler(fset, "gc", func(path string) (io.ReadCloser, error) { return os.Open(buildInfo.imports[path].packagefile) }).(types.ImporterFrom) - envGarbleDir = os.Getenv("GARBLE_DIR") - envGoPrivate string // filled via 'go env' below to support 'go env -w' + envGarbleDir = os.Getenv("GARBLE_DIR") + envGarbleLiterals = os.Getenv("GARBLE_LITERALS") == "true" + envGoPrivate string // filled via 'go env' below to support 'go env -w' ) -// origLookup helps implement a types.Importer which finds the export data for -// the original dependencies, not their garbled counterparts. This is useful to -// typecheck a package before it's garbled, so we can make decisions on how to -// garble it. -func origLookup(path string) (io.ReadCloser, error) { +type listedPackage struct { + Export string + Deps []string +} + +// listPackage is a simple wrapper around 'go list -json'. +func listPackage(path string) (listedPackage, error) { + var pkg listedPackage cmd := exec.Command("go", "list", "-json", "-export", path) if envGarbleDir == "" { - return nil, fmt.Errorf("$GARBLE_DIR unset; did you run via 'garble build'?") + return pkg, fmt.Errorf("$GARBLE_DIR unset; did you run via 'garble build'?") } cmd.Dir = envGarbleDir out, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("go list error: %v: %s", err, out) - } - var res struct { - Export string + return pkg, fmt.Errorf("go list error: %v: %s", err, out) } - if err := json.Unmarshal(out, &res); err != nil { - return nil, err + if err := json.Unmarshal(out, &pkg); err != nil { + return pkg, err } - return os.Open(res.Export) + return pkg, nil } func garbledImport(path string) (*types.Package, error) { @@ -164,6 +179,7 @@ func mainErr(args []string) error { return err } os.Setenv("GARBLE_DIR", wd) + os.Setenv("GARBLE_LITERALS", fmt.Sprint(garbleLiterals)) // If GOPRIVATE isn't set and we're in a module, use its module // path as a GOPRIVATE default. Include a _test variant too. @@ -297,6 +313,10 @@ func transformCompile(args []string) ([]string, error) { files = append(files, file) } + if envGarbleLiterals { + files = obfuscateLiterals(files) + } + info := &types.Info{ Defs: make(map[*ast.Ident]types.Object), Uses: make(map[*ast.Ident]types.Object), @@ -379,7 +399,12 @@ func readBuildIDs(flags []string) error { if importcfg == "" { return fmt.Errorf("could not find -importcfg argument") } - data, err := ioutil.ReadFile(importcfg) + f, err := os.OpenFile(importcfg, os.O_RDWR, 0) + if err != nil { + return err + } + defer f.Close() + data, err := ioutil.ReadAll(f) if err != nil { return err } @@ -414,6 +439,40 @@ func readBuildIDs(flags []string) error { } } // log.Printf("%#v", buildInfo) + + // Since string obfuscation adds crypto dependencies, ensure they are + // also part of the importcfg. Otherwise, the compiler or linker might + // error when trying to locate them. + // TODO: only do this when string obfuscation is enabled. + // TODO: this means these packages can't be garbled. never garble std? + toAdd := []string{ + "crypto/aes", + "crypto/cipher", + } + for len(toAdd) > 0 { + // Use a stack, to reuse memory. + path := toAdd[len(toAdd)-1] + toAdd = toAdd[:len(toAdd)-1] + if _, ok := buildInfo.imports[path]; ok { + continue + } + pkg, err := listPackage(path) + if err != nil { + return err + } + if pkg.Export == "" { + continue // e.g. unsafe + } + if _, err := fmt.Fprintf(f, "packagefile %s=%s\n", path, pkg.Export); err != nil { + return err + } + // Add their dependencies too, without adding duplicates. + buildInfo.imports[path] = importedPkg{packagefile: pkg.Export} + toAdd = append(toAdd, pkg.Deps...) + } + if err := f.Close(); err != nil { + return err + } return nil } @@ -501,6 +560,10 @@ func buildBlacklist(files []*ast.File, info *types.Info, pkg *types.Package) map } fnType := info.ObjectOf(sel.Sel) + if fnType.Pkg() == nil { + return true + } + if fnType.Pkg().Path() == "reflect" && (fnType.Name() == "TypeOf" || fnType.Name() == "ValueOf") { reflectCallLevel = level } diff --git a/strings.go b/strings.go new file mode 100644 index 0000000..9ebded6 --- /dev/null +++ b/strings.go @@ -0,0 +1,324 @@ +package main + +import ( + "encoding/hex" + "go/ast" + "go/token" + "log" + "strconv" + "strings" + + "golang.org/x/tools/go/ast/astutil" +) + +func obfuscateLiterals(files []*ast.File) []*ast.File { + + pre := func(cursor *astutil.Cursor) bool { + t, ok := cursor.Node().(*ast.GenDecl) + if !ok { + return true + } + + // constants are not possibly if we want to obfuscate literals, therfore + // remove all constants and replace them by variables + if t.Tok == token.CONST { + t.Tok = token.VAR + } + + return true + } + + var ( + key = genAesKey() + fset = token.NewFileSet() + addedToPkg bool // we only want to inject the code and imports once + ) + + post := func(cursor *astutil.Cursor) bool { + + switch x := cursor.Node().(type) { + case *ast.File: + if !addedToPkg { + x.Decls = append(x.Decls, funcStmt) + x.Decls = append(x.Decls, keyStmt(key)) + + if x.Imports == nil { + var newDecls = []ast.Decl{ + cryptoAesImportSpec, + } + + for _, decl := range x.Decls { + newDecls = append(newDecls, decl) + } + + x.Decls = newDecls + } else { + astutil.AddImport(fset, x, "crypto/aes") + astutil.AddImport(fset, x, "crypto/cipher") + } + + addedToPkg = true + + return true + } + case *ast.BasicLit: + if !(cursor.Name() == "Values" || cursor.Name() == "Rhs" || cursor.Name() == "Value" || cursor.Name() == "Args") { + return true // we don't want to obfuscate imports etc. + } + if x.Kind != token.STRING { + return true // TODO: garble literals other than strings + } + + value, err := strconv.Unquote(x.Value) + if err != nil { + log.Fatalln("[Fatal]: Could not unqote string", err) + return false + } + + ciphertext, err := encAes([]byte(value), key) + if err != nil { + + log.Fatalln("[Fatal]: Could not encrypt string:", err) + return false + } + + cursor.Replace(ciphertextStmt(ciphertext)) + } + + return true + } + + for _, file := range files { + file = astutil.Apply(file, pre, post).(*ast.File) + } + + return files +} + +// ast definitions for injection +var ( + aesCipherStmt = &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{Name: "block"}, + &ast.Ident{Name: "err"}, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "aes"}, + Sel: &ast.Ident{Name: "NewCipher"}, + }, + Args: []ast.Expr{ + &ast.Ident{Name: "garbleKey"}, + }, + }, + }, + } + + aesGcmCipherStmt = &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{Name: "aesgcm"}, + &ast.Ident{Name: "err"}, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "cipher"}, + Sel: &ast.Ident{Name: "NewGCM"}, + }, + Args: []ast.Expr{ + &ast.Ident{Name: "block"}, + }, + }, + }, + } + + plaintextStmt = &ast.AssignStmt{ + Lhs: []ast.Expr{ + &ast.Ident{Name: "plaintext"}, + &ast.Ident{Name: "err"}, + }, + Tok: token.DEFINE, + Rhs: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "aesgcm"}, + Sel: &ast.Ident{Name: "Open"}, + }, + Args: []ast.Expr{ + &ast.Ident{Name: "nil"}, + &ast.SliceExpr{ + X: &ast.Ident{Name: "ciphertext"}, + High: &ast.BasicLit{ + Kind: token.INT, + Value: "12", + }, + }, + &ast.SliceExpr{ + X: &ast.Ident{Name: "ciphertext"}, + Low: &ast.BasicLit{ + Kind: token.INT, + Value: "12", + }, + }, + &ast.Ident{Name: "nil"}, + }, + }, + }, + } + + returnStmt = &ast.ReturnStmt{ + Results: []ast.Expr{ + &ast.CallExpr{ + Fun: &ast.Ident{Name: "string"}, + Args: []ast.Expr{ + &ast.Ident{Name: "plaintext"}, + }, + }, + }, + } +) + +func decErrStmt() *ast.IfStmt { + return &ast.IfStmt{ + Cond: &ast.BinaryExpr{ + X: &ast.Ident{Name: "err"}, + Op: token.NEQ, + Y: &ast.Ident{Name: "nil"}, + }, + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + &ast.ExprStmt{ + X: &ast.CallExpr{ + Fun: &ast.Ident{Name: "panic"}, + Args: []ast.Expr{ + &ast.BinaryExpr{ + X: &ast.BasicLit{ + Kind: token.STRING, + Value: `"[garble] Literal couldn't be decrypted: "`, + }, + Op: token.ADD, + Y: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "err"}, + Sel: &ast.Ident{Name: "Error"}, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +var funcStmt = &ast.FuncDecl{ + Name: &ast.Ident{Name: "garbleDecrypt"}, + Type: &ast.FuncType{ + Params: &ast.FieldList{ + List: []*ast.Field{ + { + Names: []*ast.Ident{{Name: "ciphertext"}}, + Type: &ast.ArrayType{ + Elt: &ast.Ident{Name: "byte"}, + }, + }, + }, + }, + Results: &ast.FieldList{ + List: []*ast.Field{ + { + Type: &ast.Ident{Name: "string"}, + }, + }, + }, + }, + Body: &ast.BlockStmt{ + List: []ast.Stmt{ + aesCipherStmt, + decErrStmt(), + aesGcmCipherStmt, + decErrStmt(), + plaintextStmt, + decErrStmt(), + returnStmt, + }, + }, +} + +func ciphertextStmt(ciphertext []byte) *ast.CallExpr { + ciphertextLit := byteToByteLit(ciphertext) + + return &ast.CallExpr{ + Fun: &ast.Ident{Name: "garbleDecrypt"}, + Args: []ast.Expr{ + ciphertextLit, + }, + } +} + +func byteToByteLit(buffer []byte) *ast.CallExpr { + hexstr := hex.EncodeToString(buffer) + + var b strings.Builder + + b.WriteString(`"`) + for i := 0; i < len(hexstr); i += 2 { + b.WriteString("\\x" + hexstr[i:i+2]) + } + + b.WriteString(`"`) + + return &ast.CallExpr{ + Fun: &ast.ArrayType{ + Elt: &ast.Ident{Name: "byte"}, + }, + Args: []ast.Expr{ + &ast.BasicLit{ + Kind: token.STRING, + Value: b.String(), + }, + }, + } +} + +func keyStmt(key []byte) (decl *ast.GenDecl) { + keyLit := byteToByteLit(key) + + decl = &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{ + &ast.ValueSpec{ + Names: []*ast.Ident{ + {Name: "garbleKey"}, + }, + Values: []ast.Expr{ + keyLit, + }, + }, + }, + } + + return +} + +var cryptoAesImportSpec = &ast.GenDecl{ + Tok: token.IMPORT, + Specs: []ast.Spec{ + &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: `"crypto/aes"`, + }, + }, + &ast.ImportSpec{ + Path: &ast.BasicLit{ + Kind: token.STRING, + Value: `"crypto/cipher"`, + }, + }, + }, +} diff --git a/testdata/scripts/strings.txt b/testdata/scripts/strings.txt new file mode 100644 index 0000000..9ac2c40 --- /dev/null +++ b/testdata/scripts/strings.txt @@ -0,0 +1,73 @@ + +garble -literals build main.go +exec ./main +cmp stdout main.stdout +! binsubstr main$exe 'Lorem' 'ipsum' 'dolor' 'first assign' 'second assign' 'First Line' 'Second Line' 'map value' 'to obfuscate' 'also obfuscate' + +[short] stop # checking that the build is reproducible is slow + +# Also check that the binary is reproducible. +cp main$exe main_old$exe +rm main$exe +garble -literals build main.go +bincmp main$exe main_old$exe + +-- main.go -- +package main + +import "fmt" + +type strucTest struct { + field string + anotherfield string +} + +const ( + cnst = "Lorem" + multiline = `First Line +Second Line` +) + +var variable = "ipsum" + +func main() { + empty := "" + + localVar := "dolor" + + reassign := "first assign" + reassign = "second assign" + + fmt.Println(cnst) + fmt.Println(multiline) + fmt.Println(variable) + fmt.Println(localVar) + fmt.Println(reassign) + fmt.Println(empty) + + x := strucTest{ + field: "to obfuscate", + anotherfield: "also obfuscate", + } + + fmt.Println(x.field) + fmt.Println(x.anotherfield) + + testMap := map[string]string{"map key": "map value"} + fmt.Println(testMap["map key"]) + + fmt.Println("another literal") +} + +-- main.stdout -- +Lorem +First Line +Second Line +ipsum +dolor +second assign + +to obfuscate +also obfuscate +map value +another literal \ No newline at end of file