add basic literal obfuscation, starting with strings

Fixes #16.
pull/28/head
lu4p 4 years ago committed by GitHub
parent 462f60a307
commit 077d02d43a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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
}

@ -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
}

@ -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"`,
},
},
},
}

@ -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
Loading…
Cancel
Save