use the "simple" obfuscator for large literals

Changes literal obfuscation such that literals of any size will be obfuscated,
but beyond `maxSize` we only use the `simple` obfuscator.
This one seems to apply AND, OR, or XOR operators byte-wise and should be safe to use,
unlike some of the other obfuscators which are quadratic on the literal size or worse.

The test for literals is changed a bit to verify that obfuscation is applied.
The code written to the `extra_literals.go` file by the test helper now ensures
that Go does not optimize the literals away when we build the binary.
We also append a unique string to all literals so that we can test that
an unobfuscated build contains this string while an obfuscated build does not.
pull/724/head
Dominic Breuker 1 year ago committed by GitHub
parent 4d7546703a
commit b587d8c01a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -20,14 +20,9 @@ import (
// moderate, this also decreases the likelihood for performance slowdowns. // moderate, this also decreases the likelihood for performance slowdowns.
const minSize = 8 const minSize = 8
// maxSize is the upper bound limit, of the size of string-like literals // maxSize is the upper limit of the size of string-like literals
// which we will obfuscate. This is important, because otherwise garble can take // which we will obfuscate with any of the available obfuscators.
// a very long time to obfuscate huge code-generated literals, such as those // Beyond that we apply only a subset of obfuscators which are guaranteed to run efficiently.
// corresponding to large assets.
//
// 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 maxSize = 2 << 10 // KiB const maxSize = 2 << 10 // KiB
// Obfuscate replaces literals with obfuscated anonymous functions. // Obfuscate replaces literals with obfuscated anonymous functions.
@ -66,7 +61,7 @@ func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkString
if typeAndValue.Type == types.Typ[types.String] && typeAndValue.Value != nil { if typeAndValue.Type == types.Typ[types.String] && typeAndValue.Value != nil {
value := constant.StringVal(typeAndValue.Value) value := constant.StringVal(typeAndValue.Value)
if len(value) < minSize || len(value) > maxSize { if len(value) < minSize {
return true return true
} }
@ -124,7 +119,7 @@ func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkString
// //
// If the input node cannot be obfuscated nil is returned. // If the input node cannot be obfuscated nil is returned.
func handleCompositeLiteral(obfRand *obfRand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node { func handleCompositeLiteral(obfRand *obfRand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node {
if len(node.Elts) < minSize || len(node.Elts) > maxSize { if len(node.Elts) < minSize {
return nil return nil
} }
@ -218,8 +213,8 @@ func withPos(node ast.Node, pos token.Pos) ast.Node {
} }
func obfuscateString(obfRand *obfRand, data string) *ast.CallExpr { func obfuscateString(obfRand *obfRand, data string) *ast.CallExpr {
obfuscator := obfRand.nextObfuscator() obf := getNextObfuscator(obfRand, len(data))
block := obfuscator.obfuscate(obfRand.Rand, []byte(data)) block := obf.obfuscate(obfRand.Rand, []byte(data))
block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(ast.NewIdent("string"), ast.NewIdent("data")))) block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(ast.NewIdent("string"), ast.NewIdent("data"))))
@ -227,8 +222,8 @@ func obfuscateString(obfRand *obfRand, data string) *ast.CallExpr {
} }
func obfuscateByteSlice(obfRand *obfRand, isPointer bool, data []byte) *ast.CallExpr { func obfuscateByteSlice(obfRand *obfRand, isPointer bool, data []byte) *ast.CallExpr {
obfuscator := obfRand.nextObfuscator() obf := getNextObfuscator(obfRand, len(data))
block := obfuscator.obfuscate(obfRand.Rand, data) block := obf.obfuscate(obfRand.Rand, data)
if isPointer { if isPointer {
block.List = append(block.List, ah.ReturnStmt(&ast.UnaryExpr{ block.List = append(block.List, ah.ReturnStmt(&ast.UnaryExpr{
@ -245,8 +240,8 @@ func obfuscateByteSlice(obfRand *obfRand, isPointer bool, data []byte) *ast.Call
} }
func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length int64) *ast.CallExpr { func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length int64) *ast.CallExpr {
obfuscator := obfRand.nextObfuscator() obf := getNextObfuscator(obfRand, len(data))
block := obfuscator.obfuscate(obfRand.Rand, data) block := obf.obfuscate(obfRand.Rand, data)
arrayType := &ast.ArrayType{ arrayType := &ast.ArrayType{
Len: ah.IntLit(int(length)), Len: ah.IntLit(int(length)),
@ -291,3 +286,11 @@ func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length in
return ah.LambdaCall(arrayType, block) return ah.LambdaCall(arrayType, block)
} }
func getNextObfuscator(obfRand *obfRand, size int) obfuscator {
if size <= maxSize {
return obfRand.nextObfuscator()
} else {
return obfRand.nextLinearTimeObfuscator()
}
}

@ -16,15 +16,22 @@ type obfuscator interface {
} }
var ( var (
simpleObfuscator = simple{}
// Obfuscators contains all types which implement the obfuscator Interface // Obfuscators contains all types which implement the obfuscator Interface
Obfuscators = []obfuscator{ Obfuscators = []obfuscator{
simple{}, simpleObfuscator,
swap{}, swap{},
split{}, split{},
shuffle{}, shuffle{},
seed{}, seed{},
} }
// LinearTimeObfuscators contains all types which implement the obfuscator Interface and can safely be used on large literals
LinearTimeObfuscators = []obfuscator{
simpleObfuscator,
}
TestObfuscator string TestObfuscator string
testPkgToObfuscatorMap map[string]obfuscator testPkgToObfuscatorMap map[string]obfuscator
) )
@ -84,6 +91,13 @@ func (r *obfRand) nextObfuscator() obfuscator {
return Obfuscators[r.Intn(len(Obfuscators))] return Obfuscators[r.Intn(len(Obfuscators))]
} }
func (r *obfRand) nextLinearTimeObfuscator() obfuscator {
if r.testObfuscator != nil {
return r.testObfuscator
}
return Obfuscators[r.Intn(len(LinearTimeObfuscators))]
}
func newObfRand(rand *mathrand.Rand, file *ast.File) *obfRand { func newObfRand(rand *mathrand.Rand, file *ast.File) *obfRand {
testObf := testPkgToObfuscatorMap[file.Name.Name] testObf := testPkgToObfuscatorMap[file.Name.Name]
return &obfRand{rand, testObf} return &obfRand{rand, testObf}

@ -241,16 +241,23 @@ func bincmp(ts *testscript.TestScript, neg bool, args []string) {
var testRand = mathrand.New(mathrand.NewSource(time.Now().UnixNano())) var testRand = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
func generateStringLit(size int) *ast.BasicLit { func generateStringLit(minSize int) *ast.BasicLit {
buffer := make([]byte, size) buffer := make([]byte, minSize)
_, err := testRand.Read(buffer) _, err := testRand.Read(buffer)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return ah.StringLit(string(buffer)) return ah.StringLit(string(buffer) + "a_unique_string_that_is_part_of_all_extra_literals")
} }
// generateLiterals creates a new source code file with a few random literals inside.
// All literals contain the string "a_unique_string_that_is_part_of_all_extra_literals"
// so we can later check if they are all obfuscated by looking for this substring.
// The code is designed such that the Go compiler does not optimize away the literals,
// which would destroy the test.
// This is achieved by defining a global variable `var x = ""` and an `init` function
// which appends all literals to `x`.
func generateLiterals(ts *testscript.TestScript, neg bool, args []string) { func generateLiterals(ts *testscript.TestScript, neg bool, args []string) {
if neg { if neg {
ts.Fatalf("unsupported: ! generate-literals") ts.Fatalf("unsupported: ! generate-literals")
@ -261,35 +268,64 @@ func generateLiterals(ts *testscript.TestScript, neg bool, args []string) {
codePath := args[0] codePath := args[0]
// Add 100 randomly small literals. // Global string variable to which which we append string literals: `var x = ""`
globalVar := &ast.GenDecl{
Tok: token.VAR,
Specs: []ast.Spec{
&ast.ValueSpec{
Names: []*ast.Ident{ast.NewIdent("x")},
Values: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: `""`},
},
},
},
}
var statements []ast.Stmt var statements []ast.Stmt
// Assignments which append 100 random small literals to x: `x += "the_small_random_literal"`
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
literal := generateStringLit(1 + testRand.Intn(255)) statements = append(
statements = append(statements, &ast.AssignStmt{ statements,
Lhs: []ast.Expr{ast.NewIdent("_")}, &ast.AssignStmt{
Tok: token.ASSIGN, Lhs: []ast.Expr{ast.NewIdent("x")},
Rhs: []ast.Expr{literal}, Tok: token.ADD_ASSIGN,
}) Rhs: []ast.Expr{generateStringLit(1 + testRand.Intn(255))},
} },
// Add 5 huge literals, to make sure we don't try to obfuscate them. )
}
// Assignments which append 5 random huge literals to x: `x += "the_huge_random_literal"`
// We add huge literals to make sure we obfuscate them fast.
// 5 * 128KiB is large enough that it would take a very, very long time // 5 * 128KiB is large enough that it would take a very, very long time
// to obfuscate those literals with our simple code. // to obfuscate those literals if too complex obfuscators are used.
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
literal := generateStringLit(128 << 10) statements = append(
statements = append(statements, &ast.AssignStmt{ statements,
Lhs: []ast.Expr{ast.NewIdent("_")}, &ast.AssignStmt{
Tok: token.ASSIGN, Lhs: []ast.Expr{ast.NewIdent("x")},
Rhs: []ast.Expr{literal}, Tok: token.ADD_ASSIGN,
}) Rhs: []ast.Expr{generateStringLit(128 << 10)},
},
)
}
// An `init` function which includes all assignments from above
initFunc := &ast.FuncDecl{
Name: &ast.Ident{
Name: "init",
},
Type: &ast.FuncType{},
Body: ah.BlockStmt(statements...),
} }
// A file with the global string variable and init function
file := &ast.File{ file := &ast.File{
Name: ast.NewIdent("main"), Name: ast.NewIdent("main"),
Decls: []ast.Decl{&ast.FuncDecl{ Decls: []ast.Decl{
Name: ast.NewIdent("extraLiterals"), globalVar,
Type: &ast.FuncType{Params: &ast.FieldList{}}, initFunc,
Body: ah.BlockStmt(statements...), },
}},
} }
codeFile := createFile(ts, codePath) codeFile := createFile(ts, codePath)

@ -25,9 +25,15 @@ binsubstr main$exe 'Lorem Ipsum' 'dolor sit amet' 'second assign' 'First Line' '
# seconds, it means we're trying to obfuscate them. # seconds, it means we're trying to obfuscate them.
generate-literals extra_literals.go generate-literals extra_literals.go
# ensure we find the extra literals in an unobfuscated build
go build
binsubstr main$exe 'a_unique_string_that_is_part_of_all_extra_literals'
# ensure we don't find the extra literals in an obfuscated build
garble -literals -debugdir=debug1 build garble -literals -debugdir=debug1 build
exec ./main$exe exec ./main$exe
cmp stderr main.stderr cmp stderr main.stderr
! binsubstr main$exe 'a_unique_string_that_is_part_of_all_extra_literals'
# Check obfuscators. # Check obfuscators.

Loading…
Cancel
Save