Use XOR instead of AES for literal obfuscation.

Implement a literal obfuscator interface,
to allow the easy addition of new encodings.

Add literal obfuscation for byte literals, integers, booleans, floats.

Choose a random obfuscator on literal obfuscation,
useful when multiple obfuscators are implemented.

Fixes #62, #55.
pull/64/head
lu4p 5 years ago
parent 0c5e0a8944
commit ce9efab562

@ -1,57 +0,0 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
mathrand "math/rand"
"strings"
)
// 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 128-bit AES Key.
func genAesKey() []byte {
return genRandBytes(16)
}
// genAesKey generates a 128-bit 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)
if strings.HasPrefix(envGarbleSeed, "random;") {
_, err := rand.Read(buffer)
if err != nil {
panic(fmt.Sprintf("couldn't generate random key: %v", err))
}
} else {
mathrand.Read(buffer) // error is always nil so save to ignore
}
return buffer
}
// encAES encrypt data with key in AES GCM mode.
func encAES(data, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
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
}

@ -0,0 +1,374 @@
package literals
import (
"errors"
"fmt"
"go/ast"
"go/token"
"go/types"
mathrand "math/rand"
"strconv"
"strings"
"golang.org/x/tools/go/ast/astutil"
"mvdan.cc/garble/literals/obfuscators"
)
func getCallexpr(resultType ast.Expr, block *ast.BlockStmt) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.FuncLit{
Type: &ast.FuncType{
Params: &ast.FieldList{},
Results: &ast.FieldList{
List: []*ast.Field{
{Type: resultType},
},
},
},
Body: block,
},
}
}
func getObfuscator() obfuscators.Obfuscator {
randPos := mathrand.Intn(len(obfuscators.Obfuscators))
return obfuscators.Obfuscators[randPos]
}
func getReturnStmt(result ast.Expr) *ast.ReturnStmt {
return &ast.ReturnStmt{
Results: []ast.Expr{result},
}
}
func isTypeDefStr(typ types.Type) bool {
strType := types.Typ[types.String]
if named, ok := typ.(*types.Named); ok {
return types.Identical(named.Underlying(), strType)
}
return false
}
func containsTypeDefStr(expr ast.Expr, info *types.Info) bool {
typ := info.TypeOf(expr)
// log.Println(expr, typ, reflect.TypeOf(expr), reflect.TypeOf(typ))
if sig, ok := typ.(*types.Signature); ok {
for i := 0; i < sig.Params().Len(); i++ {
if isTypeDefStr(sig.Params().At(i).Type()) {
return true
}
}
}
if mapT, ok := typ.(*types.Map); ok {
return isTypeDefStr(mapT.Elem()) || isTypeDefStr(mapT.Key())
}
if named, ok := typ.(*types.Named); ok {
return isTypeDefStr(named)
}
return false
}
// Obfuscate replace literals with obfuscated lambda functions
func Obfuscate(files []*ast.File, info *types.Info, blacklist map[types.Object]struct{}) []*ast.File {
pre := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.ValueSpec:
return !containsTypeDefStr(x.Type, info)
case *ast.AssignStmt:
for _, expr := range x.Lhs {
if index, ok := expr.(*ast.IndexExpr); ok {
return !containsTypeDefStr(index.X, info)
}
if ident, ok := expr.(*ast.Ident); ok {
return !containsTypeDefStr(ident, info)
}
}
case *ast.CallExpr:
return !containsTypeDefStr(x.Fun, info)
case *ast.CompositeLit:
if t, ok := x.Type.(*ast.MapType); ok {
return !(containsTypeDefStr(t.Key, info) || containsTypeDefStr(t.Value, info))
}
case *ast.FuncDecl:
if x.Type.Results == nil {
return true
}
for _, result := range x.Type.Results.List {
for _, name := range result.Names {
return !containsTypeDefStr(name, info)
}
}
case *ast.KeyValueExpr:
if ident, ok := x.Key.(*ast.Ident); ok {
return !containsTypeDefStr(ident, info)
}
case *ast.GenDecl:
if x.Tok != token.CONST {
return true
}
for _, spec := range x.Specs {
spec, ok := spec.(*ast.ValueSpec)
if !ok {
return false
}
for _, name := range spec.Names {
obj := info.ObjectOf(name)
// The object itself is blacklisted, e.g. a value that needs to be constant
if _, ok := blacklist[obj]; ok {
return false
}
}
for _, val := range spec.Values {
if _, ok := val.(*ast.BasicLit); !ok {
return false // skip the block if it contains non basic literals
}
}
}
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
}
post := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.Ident:
obj := info.ObjectOf(x)
if obj == nil {
return true
}
if obj.Type() == types.Typ[types.Bool] || obj.Type() == types.Typ[types.UntypedBool] {
if obj.Name() == "true" || obj.Name() == "false" {
cursor.Replace(obfuscateBool(x.Name == "true"))
}
}
case *ast.CompositeLit:
if info.TypeOf(x.Type).String() == "[]byte" {
var data []byte
for _, el := range x.Elts {
lit, ok := el.(*ast.BasicLit)
if !ok {
return true
}
value, err := strconv.Atoi(lit.Value)
if err != nil {
return true
}
data = append(data, byte(value))
}
cursor.Replace(obfuscateByte(data))
}
case *ast.UnaryExpr:
switch cursor.Name() {
case "Values", "Rhs", "Value", "Args", "X":
default:
return true // we don't want to obfuscate imports etc.
}
obfuscateNumberLiteral(cursor, info)
case *ast.BasicLit:
_, ok := cursor.Parent().(*ast.UnaryExpr)
if ok {
break
}
switch cursor.Name() {
case "Values", "Rhs", "Value", "Args", "X":
default:
return true // we don't want to obfuscate imports etc.
}
switch x.Kind {
case token.FLOAT, token.INT:
obfuscateNumberLiteral(cursor, info)
case token.STRING:
value, err := strconv.Unquote(x.Value)
if err != nil {
panic(fmt.Sprintf("cannot unquote string: %v", err))
}
cursor.Replace(obfuscateString(value))
}
}
return true
}
for i := range files {
files[i] = astutil.Apply(files[i], pre, post).(*ast.File)
}
return files
}
func obfuscateNumberLiteral(cursor *astutil.Cursor, info *types.Info) error {
var (
call *ast.CallExpr
basic *ast.BasicLit
ok bool
typeInfo types.Type
)
sign := ""
node := cursor.Node()
switch x := node.(type) {
case *ast.UnaryExpr:
basic, ok = x.X.(*ast.BasicLit)
if !ok {
return errors.New("UnaryExpr doesn't contain basic literal")
}
typeInfo = info.TypeOf(x)
if x.Op != token.SUB {
return errors.New("UnaryExpr has a non SUB token")
}
sign = "-"
switch y := cursor.Parent().(type) {
case *ast.ValueSpec:
tempInfo := info.TypeOf(y.Type)
if tempInfo != nil {
typeInfo = tempInfo
}
}
case *ast.BasicLit:
basic = x
typeInfo = info.TypeOf(x)
default:
return errors.New("Wrong node Type")
}
strValue := sign + basic.Value
switch typeInfo {
case types.Typ[types.Float32]:
fV, err := strconv.ParseFloat(strValue, 32)
if err != nil {
panic(err)
}
call = obfuscateFloat32(float32(fV))
case types.Typ[types.Float64], types.Typ[types.UntypedFloat]:
fV, err := strconv.ParseFloat(strValue, 64)
if err != nil {
panic(err)
}
call = obfuscateFloat64(fV)
}
if call != nil {
cursor.Replace(call)
return nil
}
// Explicitly typed integers can have a decimal place
splitStrValue := strings.Split(strValue, ".")
intValue, err := strconv.Atoi(splitStrValue[0])
if err != nil {
panic(err)
}
switch typeInfo {
case types.Typ[types.Int], types.Typ[types.UntypedInt]:
call = obfuscateInt(intValue)
case types.Typ[types.Int8]:
call = obfuscateInt8(int8(intValue))
case types.Typ[types.Int16]:
call = obfuscateInt16(int16(intValue))
case types.Typ[types.Int32]:
call = obfuscateInt32(int32(intValue))
case types.Typ[types.Int64]:
call = obfuscateInt64(int64(intValue))
case types.Typ[types.Uint]:
call = obfuscateUint(uint(intValue))
case types.Typ[types.Uint8]:
call = obfuscateUint8(uint8(intValue))
case types.Typ[types.Uint16]:
call = obfuscateUint16(uint16(intValue))
case types.Typ[types.Uint32]:
call = obfuscateUint32(uint32(intValue))
case types.Typ[types.Uint64]:
call = obfuscateUint64(uint64(intValue))
case types.Typ[types.Uintptr]:
call = obfuscateUintptr(uintptr(intValue))
}
if call == nil {
return errors.New("Node is not Integer")
}
cursor.Replace(call)
return nil
}
// ConstBlacklist blacklist identifieres used in constant expressions
func ConstBlacklist(node ast.Node, info *types.Info, blacklist map[types.Object]struct{}) {
blacklistObjects := func(node ast.Node) bool {
ident, ok := node.(*ast.Ident)
if !ok {
return true
}
obj := info.ObjectOf(ident)
blacklist[obj] = struct{}{}
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, blacklistObjects)
}
}
// in an array type the length must be a constant representable
case *ast.ArrayType:
if x.Len != nil {
ast.Inspect(x.Len, blacklistObjects)
}
// 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, blacklistObjects)
}
}
}
}

@ -0,0 +1,459 @@
package literals
import (
"encoding/binary"
"go/ast"
"go/token"
)
func getBoundsCheck(pos string) *ast.AssignStmt {
return &ast.AssignStmt{
Lhs: []ast.Expr{&ast.Ident{Name: "_"}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: pos,
},
},
},
}
}
func obfuscateUint8(data uint8) *ast.CallExpr {
obfuscator := getObfuscator()
block := obfuscator.Obfuscate([]byte{byte(data)})
block.List = append(block.List,
&ast.ReturnStmt{
Results: []ast.Expr{
&ast.CallExpr{
Fun: &ast.Ident{Name: "uint8"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "0",
},
},
},
},
},
})
return getCallexpr(&ast.Ident{Name: "uint8"}, block)
}
func obfuscateUint16(data uint16) *ast.CallExpr {
obfuscator := getObfuscator()
b := make([]byte, 2)
binary.LittleEndian.PutUint16(b, data)
block := obfuscator.Obfuscate(b)
convertExpr := &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint16"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "0",
},
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint16"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "1",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "8",
},
},
}
block.List = append(block.List, getBoundsCheck("1"), getReturnStmt(convertExpr))
return getCallexpr(&ast.Ident{Name: "uint16"}, block)
}
func obfuscateUint32(data uint32) *ast.CallExpr {
obfuscator := getObfuscator()
b := make([]byte, 4)
binary.LittleEndian.PutUint32(b, data)
block := obfuscator.Obfuscate(b)
convertExpr := &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint32"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "0",
},
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint32"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "1",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "8",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint32"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "2",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "16",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint32"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "3",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "24",
},
},
}
block.List = append(block.List, getBoundsCheck("3"), getReturnStmt(convertExpr))
return getCallexpr(&ast.Ident{Name: "uint32"}, block)
}
func obfuscateUint64(data uint64) *ast.CallExpr {
obfuscator := getObfuscator()
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, data)
block := obfuscator.Obfuscate(b)
convertExpr := &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "0",
},
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "1",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "8",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "2",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "16",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "3",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "24",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "4",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "32",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "5",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "40",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "6",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "48",
},
},
},
Op: token.OR,
Y: &ast.BinaryExpr{
X: &ast.CallExpr{
Fun: &ast.Ident{Name: "uint64"},
Args: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.BasicLit{
Kind: token.INT,
Value: "7",
},
},
},
},
Op: token.SHL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "56",
},
},
}
block.List = append(block.List, getBoundsCheck("7"), getReturnStmt(convertExpr))
return getCallexpr(&ast.Ident{Name: "uint64"}, block)
}
func obfuscateUint(data uint) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "uint",
},
Args: []ast.Expr{
obfuscateUint64(uint64(data)),
},
}
}
func obfuscateUintptr(data uintptr) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "uintptr",
},
Args: []ast.Expr{
obfuscateUint64(uint64(data)),
},
}
}
func obfuscateInt8(data int8) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "int8",
},
Args: []ast.Expr{
obfuscateUint8(uint8(data)),
},
}
}
func obfuscateInt16(data int16) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "int16",
},
Args: []ast.Expr{
obfuscateUint16(uint16(data)),
},
}
}
func obfuscateInt32(data int32) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "int32",
},
Args: []ast.Expr{
obfuscateUint32(uint32(data)),
},
}
}
func obfuscateInt64(data int64) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "int64",
},
Args: []ast.Expr{
obfuscateUint64(uint64(data)),
},
}
}
func obfuscateInt(data int) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "int",
},
Args: []ast.Expr{
obfuscateUint64(uint64(data)),
},
}
}
func obfuscateFloat32(data float32) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "float32",
},
Args: []ast.Expr{
obfuscateUint32(uint32(data)),
},
}
}
func obfuscateFloat64(data float64) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.Ident{
Name: "float64",
},
Args: []ast.Expr{
obfuscateUint64(uint64(data)),
},
}
}

@ -0,0 +1,60 @@
package obfuscators
import (
cryptrand "crypto/rand"
"fmt"
"go/ast"
"go/token"
mathrand "math/rand"
"os"
"strings"
)
// Obfuscator takes a byte slice and converts it to a ast.BlockStmt
type Obfuscator interface {
Obfuscate(data []byte) *ast.BlockStmt
}
var (
// Obfuscators contains all types which implement the Obfuscator Interface
Obfuscators = []Obfuscator{
xor{},
}
envGarbleSeed = os.Getenv("GARBLE_SEED")
)
// dataToByteSlice turns a byte slice like []byte{1, 2, 3} into an AST
// expression
func dataToByteSlice(data []byte) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.ArrayType{
Elt: &ast.Ident{Name: "byte"},
},
Args: []ast.Expr{&ast.BasicLit{
Kind: token.STRING,
Value: fmt.Sprintf("%q", data),
}},
}
}
// If math/rand.Seed() is not called, the generator behaves as if seeded by rand.Seed(1),
// so the generator is deterministic.
// genRandBytes return a random []byte with the length of size.
func genRandBytes(buffer []byte) {
if strings.HasPrefix(envGarbleSeed, "random;") {
_, err := cryptrand.Read(buffer)
if err != nil {
panic(fmt.Sprintf("couldn't generate random key: %v", err))
}
} else {
_, err := mathrand.Read(buffer)
if err != nil {
panic(fmt.Sprintf("couldn't generate random key: %v", err))
}
}
}
func genRandInt() int {
return mathrand.Int()
}

@ -0,0 +1,69 @@
package obfuscators
import (
"go/ast"
"go/token"
)
type xor struct{}
// check that the obfuscator interface is implemented
var _ Obfuscator = xor{}
func (x xor) Obfuscate(data []byte) *ast.BlockStmt {
key := make([]byte, len(data))
genRandBytes(key)
for i, b := range key {
data[i] = data[i] ^ b
}
return &ast.BlockStmt{
List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "key"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{dataToByteSlice(key)},
},
&ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: "data"},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{dataToByteSlice(data)},
},
&ast.RangeStmt{
Key: &ast.Ident{Name: "i"},
Value: &ast.Ident{Name: "b"},
Tok: token.DEFINE,
X: &ast.Ident{Name: "key"},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{
&ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.Ident{Name: "i"},
},
},
Tok: token.ASSIGN,
Rhs: []ast.Expr{
&ast.BinaryExpr{
X: &ast.IndexExpr{
X: &ast.Ident{Name: "data"},
Index: &ast.Ident{Name: "i"},
},
Op: token.XOR,
Y: &ast.Ident{Name: "b"},
},
},
},
},
},
},
},
}
}

@ -0,0 +1,50 @@
package literals
import (
"go/ast"
"go/token"
)
func obfuscateString(data string) *ast.CallExpr {
obfuscator := getObfuscator()
block := obfuscator.Obfuscate([]byte(data))
block.List = append(block.List, &ast.ReturnStmt{
Results: []ast.Expr{
&ast.CallExpr{
Fun: &ast.Ident{Name: "string"},
Args: []ast.Expr{
&ast.Ident{Name: "data"},
},
},
},
})
return getCallexpr(&ast.Ident{Name: "string"}, block)
}
func obfuscateByte(data []byte) *ast.CallExpr {
obfuscator := getObfuscator()
block := obfuscator.Obfuscate(data)
block.List = append(block.List, &ast.ReturnStmt{
Results: []ast.Expr{
&ast.Ident{Name: "data"},
},
})
return getCallexpr(&ast.ArrayType{Elt: &ast.Ident{Name: "byte"}}, block)
}
func obfuscateBool(data bool) *ast.BinaryExpr {
var dataUint8 uint8 = 0
if data {
dataUint8 = 1
}
return &ast.BinaryExpr{
X: obfuscateUint8(dataUint8),
Op: token.EQL,
Y: &ast.BasicLit{
Kind: token.INT,
Value: "1",
},
}
}

@ -29,6 +29,7 @@ import (
"strings"
"golang.org/x/tools/go/ast/astutil"
"mvdan.cc/garble/literals"
)
var flagSet = flag.NewFlagSet("garble", flag.ContinueOnError)
@ -383,8 +384,9 @@ func transformCompile(args []string) ([]string, error) {
}
info := &types.Info{
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
}
pkg, err := origTypesConfig.Check(pkgPath, fset, files, info)
if err != nil {
@ -394,7 +396,7 @@ func transformCompile(args []string) ([]string, error) {
blacklist := buildBlacklist(files, info, pkg)
if envGarbleLiterals {
files = obfuscateLiterals(files, info, blacklist)
files = literals.Obfuscate(files, info, blacklist)
// ast changed so we need to typecheck again
pkg, err = origTypesConfig.Check(pkgPath, fset, files, info)
if err != nil {
@ -575,38 +577,6 @@ 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: this means these packages can't be garbled. never garble std?
if envGarbleLiterals {
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
}
@ -682,7 +652,7 @@ func buildBlacklist(files []*ast.File, info *types.Info, pkg *types.Package) map
visit := func(node ast.Node) bool {
if envGarbleLiterals {
constBlacklist(node, info, blacklist)
literals.ConstBlacklist(node, info, blacklist)
}
call, ok := node.(*ast.CallExpr)

@ -1,362 +0,0 @@
package main
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strconv"
"golang.org/x/tools/go/ast/astutil"
)
func isTypeDefStr(typ types.Type) bool {
strType := types.Typ[types.String]
if named, ok := typ.(*types.Named); ok {
return types.Identical(named.Underlying(), strType)
}
return false
}
func containsTypeDefStr(expr ast.Expr, info *types.Info) bool {
typ := info.TypeOf(expr)
// log.Println(expr, typ, reflect.TypeOf(expr), reflect.TypeOf(typ))
if sig, ok := typ.(*types.Signature); ok {
for i := 0; i < sig.Params().Len(); i++ {
if isTypeDefStr(sig.Params().At(i).Type()) {
return true
}
}
}
if mapT, ok := typ.(*types.Map); ok {
return isTypeDefStr(mapT.Elem()) || isTypeDefStr(mapT.Key())
}
if named, ok := typ.(*types.Named); ok {
return isTypeDefStr(named)
}
return false
}
func obfuscateLiterals(files []*ast.File, info *types.Info, blacklist map[types.Object]struct{}) []*ast.File {
pre := func(cursor *astutil.Cursor) bool {
switch x := cursor.Node().(type) {
case *ast.ValueSpec:
return !containsTypeDefStr(x.Type, info)
case *ast.AssignStmt:
for _, expr := range x.Lhs {
if index, ok := expr.(*ast.IndexExpr); ok {
return !containsTypeDefStr(index.X, info)
}
if ident, ok := expr.(*ast.Ident); ok {
return !containsTypeDefStr(ident, info)
}
}
case *ast.CallExpr:
return !containsTypeDefStr(x.Fun, info)
case *ast.CompositeLit:
if t, ok := x.Type.(*ast.MapType); ok {
return !(containsTypeDefStr(t.Key, info) || containsTypeDefStr(t.Value, info))
}
case *ast.FuncDecl:
if x.Type.Results == nil {
return true
}
for _, result := range x.Type.Results.List {
for _, name := range result.Names {
return !containsTypeDefStr(name, info)
}
}
case *ast.KeyValueExpr:
if ident, ok := x.Key.(*ast.Ident); ok {
return !containsTypeDefStr(ident, info)
}
case *ast.GenDecl:
if x.Tok != token.CONST {
return true
}
for _, spec := range x.Specs {
spec, ok := spec.(*ast.ValueSpec)
if !ok {
return false
}
for _, name := range spec.Names {
obj := info.ObjectOf(name)
// The object itself is blacklisted, e.g. a value that needs to be constant
if _, ok := blacklist[obj]; ok {
return false
}
}
for _, val := range spec.Values {
if v, ok := val.(*ast.BasicLit); !ok || v.Kind != token.STRING {
return false // skip the block if it contains non basic literals
}
}
}
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
}
key := genAesKey()
addedToPkg := false // 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 {
break
}
x.Decls = append(x.Decls, funcStmt)
x.Decls = append(x.Decls, keyStmt(key))
astutil.AddImport(fset, x, "crypto/aes")
astutil.AddImport(fset, x, "crypto/cipher")
addedToPkg = true
case *ast.BasicLit:
switch cursor.Name() {
case "Values", "Rhs", "Value", "Args":
default:
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 {
panic(fmt.Sprintf("cannot unquote string: %v", err))
}
ciphertext, err := encAES([]byte(value), key)
if err != nil {
panic(fmt.Sprintf("cannot encrypt string: %v", err))
}
cursor.Replace(ciphertextStmt(ciphertext))
}
return true
}
for i := range files {
files[i] = astutil.Apply(files[i], 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 := dataToByteSlice(ciphertext)
return &ast.CallExpr{
Fun: &ast.Ident{Name: "garbleDecrypt"},
Args: []ast.Expr{ciphertextLit},
}
}
// dataToByteSlice turns a byte slice like []byte{1, 2, 3} into an AST
// expression
func dataToByteSlice(data []byte) *ast.CallExpr {
return &ast.CallExpr{
Fun: &ast.ArrayType{
Elt: &ast.Ident{Name: "byte"},
},
Args: []ast.Expr{&ast.BasicLit{
Kind: token.STRING,
Value: fmt.Sprintf("%q", data),
}},
}
}
func keyStmt(key []byte) *ast.GenDecl {
keyLit := dataToByteSlice(key)
return &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{&ast.ValueSpec{
Names: []*ast.Ident{{Name: "garbleKey"}},
Values: []ast.Expr{keyLit},
}},
}
}
func constBlacklist(node ast.Node, info *types.Info, blacklist map[types.Object]struct{}) {
blacklistObjects := func(node ast.Node) bool {
ident, ok := node.(*ast.Ident)
if !ok {
return true
}
obj := info.ObjectOf(ident)
blacklist[obj] = struct{}{}
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, blacklistObjects)
}
}
// in an array type the length must be a constant representable
case *ast.ArrayType:
if x.Len != nil {
ast.Inspect(x.Len, blacklistObjects)
}
// 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, blacklistObjects)
}
}
}
}

@ -2,9 +2,9 @@
garble -literals build
exec ./main$exe
cmp stderr main.stderr
! binsubstr main$exe 'garbleDecrypt' 'Lorem' 'ipsum' 'dolor' 'first assign' 'second assign' 'First Line' 'Second Line' 'map value' 'to obfuscate' 'also obfuscate' 'stringTypeField String' 'stringTypeStruct'
! binsubstr main$exe 'garbleDecrypt' 'Lorem' 'ipsum' 'dolor' 'first assign' 'second assign' 'First Line' 'Second Line' 'map value' 'to obfuscate' 'also obfuscate' 'stringTypeField String' 'stringTypeStruct' '2824583991413579605' '3735714531481032066'
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' 'testMap2 value' 'testMap3 value' 'testMap1 new value' 'testMap2 new value' 'testMap3 new value' 'stringType func param' 'stringType return'
binsubstr main$exe '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' 'testMap2 value' 'testMap3 value' 'testMap1 new value' 'testMap2 new value' 'testMap3 new value' 'stringType func param' 'stringType return'
[short] stop # checking that the build is reproducible is slow
# Also check that the binary is reproducible.
@ -18,9 +18,6 @@ module test/main
-- main.go --
package main
// The lack of imports is an edge case, since we have to add imports like
// crypto/aes.
type strucTest struct {
field string
anotherfield string
@ -33,8 +30,8 @@ Second Line`
)
const (
skip1 = "Skip this block,"
i = 1
i = 1
boolean = true
)
const (
@ -58,7 +55,7 @@ func main() {
reassign := "first assign"
reassign = "second assign"
println(cnst)
println(cnst, boolean)
println(multiline)
println(localVar)
println(reassign)
@ -75,10 +72,13 @@ func main() {
testMap["map key"] = "new value"
println(testMap["map key"])
println("another literal")
println(skip1, skip2)
println(i, foo, bar)
println(skip2)
println(boolean, i, foo, bar)
typedTest()
constantTest()
numTest()
byteTest()
boolTest()
}
type stringType string
@ -152,13 +152,91 @@ func constantTest() {
const i = length + len(f)
}
func numTest() {
const a = 1 // skip
const b = a + 2 // skip
const c = 2824583991413579605
d := 4
var e = 5
var f int
f = 3735714531481032066
println(a, b, c, d, e, f)
var (
untypedInt = -3453453534423 + 12
intVar int = -3453453534423
int8Var int8 = -122.0
int16Var int16 = 3534
int32Var int32 = 333453534
int64Var int64 = 3453453534423
uintVar uint = 3453453534423
uint8Var uint8 = 34
uint16Var uint16 = 3534
uint32Var uint32 = 333453534
uint64Var uint64 = 3453453534423
uintptrVar uintptr = 3453453534423
untypedFloat = 3.0
floatVar float64 = 75453453534
floatVar32 float32 = -435453453534
complexVar64 complex64 = -435453453534 // skip
complexVar128 complex128 = 1 + 4i // skip
)
floatShort := -435453453534.0
println(untypedInt, intVar, int8Var, int16Var, int32Var, int64Var)
println(uintVar, uint8Var, uint16Var, uint32Var, uint64Var, uintptrVar)
println(untypedFloat, floatVar, floatShort, floatVar32)
println(complexVar64, complexVar128)
}
func byteTest() {
a := []byte{12, 13}
for _, elm := range a {
print(elm, ", ")
}
println()
var b = []byte{12, 13}
for _, elm := range b {
print(elm, ", ")
}
println()
}
func boolTest() {
const a = true // skip
const b = false == a // skip
const c bool = false
d := true
var e = true
var f bool
f = false
println(a, b, c, d, e, f)
}
func stringTypeFunc(s stringType) stringType {
println(s)
return "stringType return" // skip
}
-- main.stderr --
Lorem
Lorem true
First Line
Second Line
dolor
@ -167,8 +245,8 @@ second assign
to obfuscate also obfuscate
new value
another literal
Skip this block, also skip this
1 0 1
also skip this
true 1 0 1
skip typed const skip typed var skip typed var assign
stringTypeField String stringTypeField strType
stringType lambda func return
@ -176,3 +254,11 @@ stringType func param
stringType return
foo
foo
1 3 2824583991413579605 4 5 3735714531481032066
-3453453534411 -3453453534423 -122 3534 333453534 3453453534423
3453453534423 34 3534 333453534 3453453534423 3453453534423
+3.000000e+000 +7.545345e+010 +1.844674e+019 +2.633204e+009
(-4.354535e+011+0.000000e+000i) (+1.000000e+000+4.000000e+000i)
12, 13,
12, 13,
true false false true true false
Loading…
Cancel
Save