diff --git a/hash.go b/hash.go index 90d07a8..d814a2a 100644 --- a/hash.go +++ b/hash.go @@ -12,6 +12,7 @@ import ( "go/token" "go/types" "io" + mathrand "math/rand" "os/exec" "strconv" "strings" @@ -310,6 +311,15 @@ const ( neededSumBytes = 9 ) +// randomName generates a random name derived from the given baseName, using the provided random source. +func randomName(rand *mathrand.Rand, baseName string) string { + salt := make([]byte, buildIDHashLength) + if _, err := rand.Read(salt); err != nil { + panic(err) + } + return hashWithCustomSalt(salt, baseName) +} + // hashWithCustomSalt returns a hashed version of name, // including the provided salt as well as opts.Seed into the hash input. // diff --git a/internal/asthelper/asthelper.go b/internal/asthelper/asthelper.go index a251c00..31df657 100644 --- a/internal/asthelper/asthelper.go +++ b/internal/asthelper/asthelper.go @@ -43,11 +43,11 @@ func CallExpr(fun ast.Expr, args ...ast.Expr) *ast.CallExpr { } } -// LambdaCall "func() resultType {block}()" -func LambdaCall(resultType ast.Expr, block *ast.BlockStmt) *ast.CallExpr { +// LambdaCall "func(params) resultType {block}(args)" +func LambdaCall(params *ast.FieldList, resultType ast.Expr, block *ast.BlockStmt, args []ast.Expr) *ast.CallExpr { funcLit := &ast.FuncLit{ Type: &ast.FuncType{ - Params: &ast.FieldList{}, + Params: params, Results: &ast.FieldList{ List: []*ast.Field{ {Type: resultType}, @@ -56,7 +56,7 @@ func LambdaCall(resultType ast.Expr, block *ast.BlockStmt) *ast.CallExpr { }, Body: block, } - return CallExpr(funcLit) + return CallExpr(funcLit, args...) } // ReturnStmt "return result" @@ -140,6 +140,63 @@ func IndexExprByExpr(xExpr, indexExpr ast.Expr) *ast.IndexExpr { return &ast.IndexExpr{X: xExpr, Index: indexExpr} } +// UnaryExpr creates a unary expression with the given operator and operand +func UnaryExpr(op token.Token, x ast.Expr) *ast.UnaryExpr { + return &ast.UnaryExpr{ + Op: op, + X: x, + } +} + +// StarExpr creates a pointer type expression "*x" +func StarExpr(x ast.Expr) *ast.StarExpr { + return &ast.StarExpr{X: x} +} + +// ArrayType creates an array type expression "[len]eltType" +func ArrayType(len ast.Expr, eltType ast.Expr) *ast.ArrayType { + return &ast.ArrayType{ + Len: len, + Elt: eltType, + } +} + +// ByteArrayType creates a byte array type "[len]byte" +func ByteArrayType(len int64) *ast.ArrayType { + lenLit := IntLit(int(len)) + return ArrayType(lenLit, ast.NewIdent("byte")) +} + +// ByteSliceType creates a byte slice type "[]byte" +func ByteSliceType() *ast.ArrayType { + return &ast.ArrayType{Elt: ast.NewIdent("byte")} +} + +// BinaryExpr creates a binary expression "x op y" +func BinaryExpr(x ast.Expr, op token.Token, y ast.Expr) *ast.BinaryExpr { + return &ast.BinaryExpr{ + X: x, + Op: op, + Y: y, + } +} + +// UintLit returns an ast.BasicLit of kind INT for uint64 values +func UintLit(value uint64) *ast.BasicLit { + return &ast.BasicLit{ + Kind: token.INT, + Value: fmt.Sprint(value), + } +} + +// Field creates a field with names and type for function parameters or struct fields +func Field(typ ast.Expr, names ...*ast.Ident) *ast.Field { + return &ast.Field{ + Names: names, + Type: typ, + } +} + func ConstToAst(val constant.Value) ast.Expr { switch val.Kind() { case constant.Bool: diff --git a/internal/literals/fuzz_test.go b/internal/literals/fuzz_test.go index 1dd7abd..5855580 100644 --- a/internal/literals/fuzz_test.go +++ b/internal/literals/fuzz_test.go @@ -72,7 +72,9 @@ func FuzzObfuscate(f *testing.F) { // Obfuscate the literals and print the source back. rand := mathrand.New(mathrand.NewSource(randSeed)) - srcSyntax = literals.Obfuscate(rand, srcSyntax, &info, nil) + srcSyntax = literals.Obfuscate(rand, srcSyntax, &info, nil, func(rand *mathrand.Rand, baseName string) string { + return fmt.Sprintf("%s%d", baseName, rand.Uint64()) + }) count := tdirCounter.Add(1) f, err := os.Create(filepath.Join(tdir, fmt.Sprintf("src_%d.go", count))) qt.Assert(t, qt.IsNil(err)) diff --git a/internal/literals/literals.go b/internal/literals/literals.go index a373c55..c664989 100644 --- a/internal/literals/literals.go +++ b/internal/literals/literals.go @@ -25,9 +25,19 @@ const MinSize = 8 // Beyond that we apply only a subset of obfuscators which are guaranteed to run efficiently. const maxSize = 2 << 10 // KiB +const ( + // minStringJunkBytes defines the minimum number of junk bytes to prepend or append during string obfuscation. + minStringJunkBytes = 2 + // maxStringJunkBytes defines the maximum number of junk bytes to prepend or append during string obfuscation. + maxStringJunkBytes = 8 +) + +// NameProviderFunc defines a function type that generates a string based on a random source and a base name. +type NameProviderFunc func(rand *mathrand.Rand, baseName string) string + // Obfuscate replaces literals with obfuscated anonymous functions. -func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkStrings map[*types.Var]string) *ast.File { - obfRand := newObfRand(rand, file) +func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkStrings map[*types.Var]string, nameFunc NameProviderFunc) *ast.File { + obfRand := newObfRand(rand, file, nameFunc) pre := func(cursor *astutil.Cursor) bool { switch node := cursor.Node().(type) { case *ast.GenDecl: @@ -110,7 +120,9 @@ func Obfuscate(rand *mathrand.Rand, file *ast.File, info *types.Info, linkString return true } - return astutil.Apply(file, pre, post).(*ast.File) + newFile := astutil.Apply(file, pre, post).(*ast.File) + obfRand.proxyDispatcher.AddToFile(newFile) + return newFile } // handleCompositeLiteral checks if the input node is []byte or [...]byte and @@ -213,39 +225,86 @@ func withPos(node ast.Node, pos token.Pos) ast.Node { func obfuscateString(obfRand *obfRand, data string) *ast.CallExpr { obf := getNextObfuscator(obfRand, len(data)) - block := obf.obfuscate(obfRand.Rand, []byte(data)) - block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(ast.NewIdent("string"), ast.NewIdent("data")))) + // Generate junk bytes to to prepend and append to the data. + // This is to prevent the obfuscated string from being easily fingerprintable. + junkBytes := make([]byte, obfRand.Intn(maxStringJunkBytes-minStringJunkBytes)+minStringJunkBytes) + obfRand.Read(junkBytes) + splitIdx := obfRand.Intn(len(junkBytes)) - return ah.LambdaCall(ast.NewIdent("string"), block) + extKeys := randExtKeys(obfRand.Rand) + + plainData := []byte(data) + plainDataWithJunkBytes := append(append(junkBytes[:splitIdx], plainData...), junkBytes[splitIdx:]...) + + block := obf.obfuscate(obfRand.Rand, plainDataWithJunkBytes, extKeys) + params, args := extKeysToParams(obfRand, extKeys) + + // Generate unique cast bytes to string function and hide it using proxyDispatcher: + // + // func(x []byte) string { + // return string(x[:]) + // } + funcTyp := &ast.FuncType{ + Params: &ast.FieldList{List: []*ast.Field{{ + Type: ah.ByteSliceType(), + }}}, + Results: &ast.FieldList{List: []*ast.Field{{ + Type: ast.NewIdent("string"), + }}}, + } + funcVal := &ast.FuncLit{ + Type: &ast.FuncType{ + Params: &ast.FieldList{List: []*ast.Field{{ + Names: []*ast.Ident{ast.NewIdent("x")}, + Type: ah.ByteSliceType(), + }}}, + Results: &ast.FieldList{List: []*ast.Field{{ + Type: ast.NewIdent("string"), + }}}, + }, + Body: ah.BlockStmt( + ah.ReturnStmt( + ah.CallExprByName("string", + &ast.SliceExpr{ + X: ast.NewIdent("x"), + Low: ah.IntLit(splitIdx), + High: ah.IntLit(splitIdx + len(plainData)), + }, + ), + ), + ), + } + block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(obfRand.proxyDispatcher.HideValue(funcVal, funcTyp), ast.NewIdent("data")))) + return ah.LambdaCall(params, ast.NewIdent("string"), block, args) } func obfuscateByteSlice(obfRand *obfRand, isPointer bool, data []byte) *ast.CallExpr { obf := getNextObfuscator(obfRand, len(data)) - block := obf.obfuscate(obfRand.Rand, data) + + extKeys := randExtKeys(obfRand.Rand) + block := obf.obfuscate(obfRand.Rand, data, extKeys) + params, args := extKeysToParams(obfRand, extKeys) if isPointer { - block.List = append(block.List, ah.ReturnStmt(&ast.UnaryExpr{ - Op: token.AND, - X: ast.NewIdent("data"), - })) - return ah.LambdaCall(&ast.StarExpr{ - X: &ast.ArrayType{Elt: ast.NewIdent("byte")}, - }, block) + block.List = append(block.List, ah.ReturnStmt( + ah.UnaryExpr(token.AND, ast.NewIdent("data")), + )) + return ah.LambdaCall(params, ah.StarExpr(ah.ByteSliceType()), block, args) } block.List = append(block.List, ah.ReturnStmt(ast.NewIdent("data"))) - return ah.LambdaCall(&ast.ArrayType{Elt: ast.NewIdent("byte")}, block) + return ah.LambdaCall(params, ah.ByteSliceType(), block, args) } func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length int64) *ast.CallExpr { obf := getNextObfuscator(obfRand, len(data)) - block := obf.obfuscate(obfRand.Rand, data) - arrayType := &ast.ArrayType{ - Len: ah.IntLit(int(length)), - Elt: ast.NewIdent("byte"), - } + extKeys := randExtKeys(obfRand.Rand) + block := obf.obfuscate(obfRand.Rand, data, extKeys) + params, args := extKeysToParams(obfRand, extKeys) + + arrayType := ah.ByteArrayType(length) sliceToArray := []ast.Stmt{ &ast.DeclStmt{ @@ -261,29 +320,28 @@ func obfuscateByteArray(obfRand *obfRand, isPointer bool, data []byte, length in Key: ast.NewIdent("i"), Tok: token.DEFINE, X: ast.NewIdent("data"), - Body: &ast.BlockStmt{List: []ast.Stmt{ - &ast.AssignStmt{ - Lhs: []ast.Expr{ah.IndexExpr("newdata", ast.NewIdent("i"))}, - Tok: token.ASSIGN, - Rhs: []ast.Expr{ah.IndexExpr("data", ast.NewIdent("i"))}, - }, - }}, + Body: ah.BlockStmt( + ah.AssignStmt( + ah.IndexExprByExpr(ast.NewIdent("newdata"), ast.NewIdent("i")), + ah.IndexExprByExpr(ast.NewIdent("data"), ast.NewIdent("i")), + ), + ), }, } var retexpr ast.Expr = ast.NewIdent("newdata") if isPointer { - retexpr = &ast.UnaryExpr{X: retexpr, Op: token.AND} + retexpr = ah.UnaryExpr(token.AND, retexpr) } sliceToArray = append(sliceToArray, ah.ReturnStmt(retexpr)) block.List = append(block.List, sliceToArray...) if isPointer { - return ah.LambdaCall(&ast.StarExpr{X: arrayType}, block) + return ah.LambdaCall(params, ah.StarExpr(arrayType), block, args) } - return ah.LambdaCall(arrayType, block) + return ah.LambdaCall(params, arrayType, block, args) } func getNextObfuscator(obfRand *obfRand, size int) obfuscator { diff --git a/internal/literals/obfuscators.go b/internal/literals/obfuscators.go index 82639b1..1920979 100644 --- a/internal/literals/obfuscators.go +++ b/internal/literals/obfuscators.go @@ -7,12 +7,56 @@ import ( "fmt" "go/ast" "go/token" + "math" mathrand "math/rand" + "slices" + "strconv" + + ah "mvdan.cc/garble/internal/asthelper" ) +// externalKeyProbability probability of using an external key. +// Larger value, greater probability of using an external key. +// Must be between 0 and 1 +type externalKeyProbability float32 + +const ( + lowProb externalKeyProbability = 0.4 + normalProb externalKeyProbability = 0.6 + highProb externalKeyProbability = 0.8 +) + +func (r externalKeyProbability) Try(rand *mathrand.Rand) bool { + return rand.Float32() < float32(r) +} + +// externalKey contains all information about the external key +type externalKey struct { + name, typ string + value uint64 + bits int + refs int +} + +func (k *externalKey) Type() *ast.Ident { + return ast.NewIdent(k.typ) +} + +func (k *externalKey) Name() *ast.Ident { + return ast.NewIdent(k.name) +} + +func (k *externalKey) AddRef() { + k.refs++ +} + +func (k *externalKey) IsUsed() bool { + return k.refs > 0 +} + // obfuscator takes a byte slice and converts it to a ast.BlockStmt type obfuscator interface { - obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt + obfuscate(obfRand *mathrand.Rand, data []byte, extKeys []*externalKey) *ast.BlockStmt } var ( @@ -63,25 +107,158 @@ func evalOperator(t token.Token, x, y byte) byte { } func operatorToReversedBinaryExpr(t token.Token, x, y ast.Expr) *ast.BinaryExpr { - expr := &ast.BinaryExpr{X: x, Y: y} - + var op token.Token switch t { case token.XOR: - expr.Op = token.XOR + op = token.XOR case token.ADD: - expr.Op = token.SUB + op = token.SUB case token.SUB: - expr.Op = token.ADD + op = token.ADD default: panic(fmt.Sprintf("unknown operator: %s", t)) } + return ah.BinaryExpr(x, op, y) +} - return expr +const ( + // minExtKeyCount is minimum number of external keys for one lambda call + minExtKeyCount = 2 + // maxExtKeyCount is maximum number of external keys for one lambda call + maxExtKeyCount = 6 + + // minByteSliceExtKeyOps minimum number of operations with external keys for one byte slice + minByteSliceExtKeyOps = 2 + // maxByteSliceExtKeyOps maximum number of operations with external keys for one byte slice + maxByteSliceExtKeyOps = 12 +) + +// extKeyRanges contains a list of different ranges of random numbers for external keys +// Different types and bitnesses will increase the chance of changing patterns +var extKeyRanges = []struct { + typ string + max uint64 + bits int +}{ + {"uint8", math.MaxUint8, 8}, + {"uint16", math.MaxUint16, 16}, + {"uint32", math.MaxUint32, 32}, + {"uint64", math.MaxUint64, 64}, +} + +// randExtKey generates a random external key with a unique name, type, value, and bitnesses +func randExtKey(rand *mathrand.Rand, idx int) *externalKey { + r := extKeyRanges[rand.Intn(len(extKeyRanges))] + return &externalKey{ + name: "garbleExternalKey" + strconv.Itoa(idx), + typ: r.typ, + value: rand.Uint64() & r.max, + bits: r.bits, + } +} + +func randExtKeys(rand *mathrand.Rand) []*externalKey { + count := minExtKeyCount + rand.Intn(maxExtKeyCount-minExtKeyCount) + keys := make([]*externalKey, count) + for i := 0; i < count; i++ { + keys[i] = randExtKey(rand, i) + } + return keys +} + +// extKeysToParams converts a list of extKeys into a parameter list and argument expressions for function calls. +// It ensures unused keys have placeholder names and sometimes use proxyDispatcher.HideValue for key values +func extKeysToParams(objRand *obfRand, keys []*externalKey) (params *ast.FieldList, args []ast.Expr) { + params = &ast.FieldList{} + for _, key := range keys { + name := key.Name() + if !key.IsUsed() { + name.Name = "_" + } + params.List = append(params.List, ah.Field(key.Type(), name)) + + var extKeyExpr ast.Expr = ah.UintLit(key.value) + if lowProb.Try(objRand.Rand) { + extKeyExpr = objRand.proxyDispatcher.HideValue(extKeyExpr, ast.NewIdent(key.typ)) + } + args = append(args, extKeyExpr) + } + return +} + +// extKeyToExpr converts an external key into an AST expression like: +// +// uint8(key >> b) +func (key *externalKey) ToExpr(b int) ast.Expr { + var x ast.Expr = key.Name() + if b > 0 { + x = ah.BinaryExpr(x, token.SHR, ah.IntLit(b*8)) + } + if key.typ != "uint8" { + x = ah.CallExprByName("byte", x) + } + return x +} + +// dataToByteSliceWithExtKeys scramble and turn a byte slice into an AST expression like: +// +// func() []byte { +// data := []byte("") +// data[] = data[] byte( >> ) // repeated random times +// return data +// }() +func dataToByteSliceWithExtKeys(rand *mathrand.Rand, data []byte, extKeys []*externalKey) ast.Expr { + extKeyOpCount := minByteSliceExtKeyOps + rand.Intn(maxByteSliceExtKeyOps-minByteSliceExtKeyOps) + + var stmts []ast.Stmt + for i := 0; i < extKeyOpCount; i++ { + key := extKeys[rand.Intn(len(extKeys))] + key.AddRef() + + idx, op, b := rand.Intn(len(data)), randOperator(rand), rand.Intn(key.bits/8) + data[idx] = evalOperator(op, data[idx], byte(key.value>>(b*8))) + stmts = append(stmts, ah.AssignStmt( + ah.IndexExpr("data", ah.IntLit(idx)), + operatorToReversedBinaryExpr(op, + ah.IndexExpr("data", ah.IntLit(idx)), + key.ToExpr(b), + ), + )) + } + + // External keys can be applied several times to the same array element, + // and it is important to invert the order of execution to correctly restore the original value + slices.Reverse(stmts) + + stmts = append([]ast.Stmt{ah.AssignDefineStmt(ast.NewIdent("data"), ah.DataToByteSlice(data))}, append(stmts, ah.ReturnStmt(ast.NewIdent("data")))...) + return ah.LambdaCall(nil, ah.ByteSliceType(), ah.BlockStmt(stmts...), nil) +} + +// dataToByteSliceWithExtKeys scramble and turns a byte into an AST expression like: +// +// byte() byte( >> ) +func byteLitWithExtKey(rand *mathrand.Rand, val byte, extKeys []*externalKey, extKeyProb externalKeyProbability) ast.Expr { + if !extKeyProb.Try(rand) { + return ah.IntLit(int(val)) + } + + key := extKeys[rand.Intn(len(extKeys))] + key.AddRef() + + op, b := randOperator(rand), rand.Intn(key.bits/8) + newVal := evalOperator(op, val, byte(key.value>>(b*8))) + + return operatorToReversedBinaryExpr(op, + ah.CallExprByName("byte", ah.IntLit(int(newVal))), + key.ToExpr(b), + ) } type obfRand struct { *mathrand.Rand testObfuscator obfuscator + + proxyDispatcher *proxyDispatcher } func (r *obfRand) nextObfuscator() obfuscator { @@ -98,7 +275,7 @@ func (r *obfRand) nextLinearTimeObfuscator() obfuscator { return Obfuscators[r.Intn(len(LinearTimeObfuscators))] } -func newObfRand(rand *mathrand.Rand, file *ast.File) *obfRand { +func newObfRand(rand *mathrand.Rand, file *ast.File, nameFunc NameProviderFunc) *obfRand { testObf := testPkgToObfuscatorMap[file.Name.Name] - return &obfRand{rand, testObf} + return &obfRand{rand, testObf, newProxyDispatcher(rand, nameFunc)} } diff --git a/internal/literals/proxy.go b/internal/literals/proxy.go new file mode 100644 index 0000000..3e600ba --- /dev/null +++ b/internal/literals/proxy.go @@ -0,0 +1,247 @@ +package literals + +import ( + "go/ast" + "go/token" + mathrand "math/rand" + "strconv" + + ah "mvdan.cc/garble/internal/asthelper" +) + +const ( + // minStructCount is the minimum number of proxyStructs initialized in the dispatcher. + minStructCount = 4 + // maxStructCount is the maximum number of proxyStructs initialized in the dispatcher. + maxStructCount = 8 + //minChildCount defines the minimum number of child elements that can be assigned to proxy structs. + minChildCount = 1 + // maxChildCount defines the maximum number of child elements that can be assigned to proxy structs. + maxChildCount = 3 + // minJunkValueCount defines the minimum number of junk values that can be added to a structure. + minJunkValueCount = 1 + // maxJunkValueCount defines the maximum number of junk values that can be added to a structure. + maxJunkValueCount = 3 + // minJunkArraySize defines the minimum size of a randomized byte array for junk data generation. + minJunkArraySize = 1 + // maxJunkArraySize defines the maximum size of a randomized byte array for junk data generation. + maxJunkArraySize = 8 +) + +// proxyValue represents a named field with a type and value used in a proxy structure. +type proxyValue struct { + name string + typ, val ast.Expr +} + +type proxyStruct struct { + typeName, name string + isPointer bool + + values []*proxyValue + + children []*proxyStruct + parent *proxyStruct +} + +type proxyDispatcher struct { + rand *mathrand.Rand + nameFunc NameProviderFunc + + root *proxyStruct + flattenStructs []*proxyStruct +} + +func (d *proxyDispatcher) initialize() { + flattenStructs := make([]*proxyStruct, d.rand.Intn(maxStructCount-minStructCount)+minStructCount) + for i := 0; i < len(flattenStructs); i++ { + flattenStructs[i] = &proxyStruct{ + typeName: d.nameFunc(d.rand, "proxyStructName"+strconv.Itoa(i)), + name: d.nameFunc(d.rand, "proxyStructFieldName"+strconv.Itoa(i)), + isPointer: d.rand.Intn(2) == 0, + } + } + + root := &proxyStruct{ + name: d.nameFunc(d.rand, "rootStructName"), + typeName: d.nameFunc(d.rand, "rootStructType"), + parent: nil, + } + d.root = root + + unassigned := append([]*proxyStruct(nil), flattenStructs...) + queue := []*proxyStruct{root} + for len(unassigned) > 0 && len(queue) > 0 { + current := queue[0] + queue = queue[1:] + + childCount := d.rand.Intn(maxChildCount-minChildCount) + minChildCount + if childCount > len(unassigned) { + childCount = len(unassigned) + } + + for i := 0; i < childCount; i++ { + child := unassigned[0] + unassigned = unassigned[1:] + + child.parent = current + current.children = append(current.children, child) + queue = append(queue, child) + } + } + + d.flattenStructs = append(flattenStructs, root) +} + +// buildPath creates an AST expression that represents a field access path +// from the root of a nested struct hierarchy to a specific field. +// Example: root.child.grandchild.fieldName +func buildPath(strct *proxyStruct, valueName string) ast.Expr { + var stack []*proxyStruct + for s := strct; s != nil; s = s.parent { + stack = append(stack, s) + } + + var expr ast.Expr = ast.NewIdent(stack[len(stack)-1].name) + for i := len(stack) - 2; i >= 0; i-- { + expr = &ast.SelectorExpr{ + X: expr, + Sel: ast.NewIdent(stack[i].name), + } + } + + return &ast.SelectorExpr{ + X: expr, + Sel: ast.NewIdent(valueName), + } +} + +func (d *proxyDispatcher) HideValue(val, typ ast.Expr) ast.Expr { + if d.root == nil { + d.initialize() + } + + strct := d.flattenStructs[d.rand.Intn(len(d.flattenStructs))] + valueName := d.nameFunc(d.rand, strct.name+"_"+strct.typeName+"_"+strconv.Itoa(len(strct.values))) + strct.values = append(strct.values, &proxyValue{ + name: valueName, + typ: typ, + val: val, + }) + return buildPath(strct, valueName) +} + +func (d *proxyDispatcher) generateStructLiteral(s *proxyStruct) ast.Expr { + var fields []ast.Expr + for _, child := range s.children { + expr := d.generateStructLiteral(child) + if child.isPointer { + expr = &ast.UnaryExpr{ + Op: token.AND, + X: expr, + } + } + fields = append(fields, &ast.KeyValueExpr{ + Key: ast.NewIdent(child.name), + Value: expr, + }) + } + + for _, val := range s.values { + fields = append(fields, &ast.KeyValueExpr{ + Key: ast.NewIdent(val.name), + Value: val.val, + }) + } + + d.rand.Shuffle(len(fields), func(i, j int) { + fields[i], fields[j] = fields[j], fields[i] + }) + + return &ast.CompositeLit{ + Type: ast.NewIdent(s.typeName), + Elts: fields, + } +} + +// junkValue generates and returns a proxyValue containing a randomized byte array of variable size +func (d *proxyDispatcher) junkValue() *proxyValue { + size := d.rand.Intn(maxJunkArraySize-minJunkArraySize+1) + minJunkArraySize + data := make([]byte, size) + d.rand.Read(data) + + dummyDataExpr := &ast.CompositeLit{ + Type: &ast.ArrayType{ + Len: ah.IntLit(size), + Elt: ast.NewIdent("byte"), + }, + Elts: ah.DataToArray(data).Elts, + } + return &proxyValue{ + name: d.nameFunc(d.rand, "junkValue"), + typ: &ast.ArrayType{ + Len: ah.IntLit(size), + Elt: ast.NewIdent("byte"), + }, + val: dummyDataExpr, + } +} + +func (d *proxyDispatcher) AddToFile(file *ast.File) { + if d.root == nil { + return + } + + for _, strct := range d.flattenStructs { + dummyCount := d.rand.Intn(maxJunkValueCount-minJunkValueCount+1) + minJunkValueCount + for i := 0; i < dummyCount; i++ { + strct.values = append(strct.values, d.junkValue()) + } + + structType := &ast.StructType{Fields: &ast.FieldList{}} + for _, child := range strct.children { + var typ ast.Expr = ast.NewIdent(child.typeName) + if child.isPointer { + typ = &ast.StarExpr{X: typ} + } + structType.Fields.List = append(structType.Fields.List, &ast.Field{ + Names: []*ast.Ident{ast.NewIdent(child.name)}, + Type: typ, + }) + } + + for _, value := range strct.values { + structType.Fields.List = append(structType.Fields.List, &ast.Field{ + Names: []*ast.Ident{ast.NewIdent(value.name)}, + Type: value.typ, + }) + } + + d.rand.Shuffle(len(structType.Fields.List), func(i, j int) { + structType.Fields.List[i], structType.Fields.List[j] = structType.Fields.List[j], structType.Fields.List[i] + }) + + file.Decls = append(file.Decls, &ast.GenDecl{ + Tok: token.TYPE, + Specs: []ast.Spec{&ast.TypeSpec{ + Name: ast.NewIdent(strct.typeName), + Type: structType, + }}, + }) + } + + file.Decls = append(file.Decls, &ast.GenDecl{ + Tok: token.VAR, + Specs: []ast.Spec{&ast.ValueSpec{ + Names: []*ast.Ident{ast.NewIdent(d.root.name)}, + Values: []ast.Expr{d.generateStructLiteral(d.root)}, + }}, + }) +} + +func newProxyDispatcher(rand *mathrand.Rand, nameFunc NameProviderFunc) *proxyDispatcher { + return &proxyDispatcher{ + rand: rand, + nameFunc: nameFunc, + } +} diff --git a/internal/literals/seed.go b/internal/literals/seed.go index d842702..6c7b6af 100644 --- a/internal/literals/seed.go +++ b/internal/literals/seed.go @@ -16,30 +16,29 @@ type seed struct{} // check that the obfuscator interface is implemented var _ obfuscator = seed{} -func (seed) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { +func (seed) obfuscate(obfRand *mathrand.Rand, data []byte, extKeys []*externalKey) *ast.BlockStmt { seed := byte(obfRand.Uint32()) originalSeed := seed op := randOperator(obfRand) - var callExpr *ast.CallExpr for i, b := range data { encB := evalOperator(op, b, seed) seed += encB if i == 0 { - callExpr = ah.CallExpr(ast.NewIdent("fnc"), ah.IntLit(int(encB))) + callExpr = ah.CallExpr(ast.NewIdent("fnc"), byteLitWithExtKey(obfRand, encB, extKeys, highProb)) continue } - callExpr = ah.CallExpr(callExpr, ah.IntLit(int(encB))) + callExpr = ah.CallExpr(callExpr, byteLitWithExtKey(obfRand, encB, extKeys, lowProb)) } return ah.BlockStmt( &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("seed")}, Tok: token.DEFINE, - Rhs: []ast.Expr{ah.CallExpr(ast.NewIdent("byte"), ah.IntLit(int(originalSeed)))}, + Rhs: []ast.Expr{ah.CallExprByName("byte", byteLitWithExtKey(obfRand, originalSeed, extKeys, highProb))}, }, &ast.DeclStmt{ Decl: &ast.GenDecl{ diff --git a/internal/literals/shuffle.go b/internal/literals/shuffle.go index fbe41bd..dfeee4f 100644 --- a/internal/literals/shuffle.go +++ b/internal/literals/shuffle.go @@ -16,9 +16,9 @@ type shuffle struct{} // check that the obfuscator interface is implemented var _ obfuscator = shuffle{} -func (shuffle) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { +func (shuffle) obfuscate(rand *mathrand.Rand, data []byte, extKeys []*externalKey) *ast.BlockStmt { key := make([]byte, len(data)) - obfRand.Read(key) + rand.Read(key) const ( minIdxKeySize = 2 @@ -26,7 +26,7 @@ func (shuffle) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { ) idxKeySize := minIdxKeySize - if tmp := obfRand.Intn(len(data)); tmp > idxKeySize { + if tmp := rand.Intn(len(data)); tmp > idxKeySize { idxKeySize = tmp } if idxKeySize > maxIdxKeySize { @@ -34,19 +34,19 @@ func (shuffle) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { } idxKey := make([]byte, idxKeySize) - obfRand.Read(idxKey) + rand.Read(idxKey) fullData := make([]byte, len(data)+len(key)) operators := make([]token.Token, len(fullData)) for i := range operators { - operators[i] = randOperator(obfRand) + operators[i] = randOperator(rand) } for i, b := range key { fullData[i], fullData[i+len(data)] = evalOperator(operators[i], data[i], b), b } - shuffledIdxs := obfRand.Perm(len(fullData)) + shuffledIdxs := rand.Perm(len(fullData)) shuffledFullData := make([]byte, len(fullData)) for i, b := range fullData { @@ -55,7 +55,7 @@ func (shuffle) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { args := []ast.Expr{ast.NewIdent("data")} for i := range data { - keyIdx := obfRand.Intn(idxKeySize) + keyIdx := rand.Intn(idxKeySize) k := int(idxKey[keyIdx]) args = append(args, operatorToReversedBinaryExpr( @@ -69,12 +69,12 @@ func (shuffle) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("fullData")}, Tok: token.DEFINE, - Rhs: []ast.Expr{ah.DataToByteSlice(shuffledFullData)}, + Rhs: []ast.Expr{dataToByteSliceWithExtKeys(rand, shuffledFullData, extKeys)}, }, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("idxKey")}, Tok: token.DEFINE, - Rhs: []ast.Expr{ah.DataToByteSlice(idxKey)}, + Rhs: []ast.Expr{dataToByteSliceWithExtKeys(rand, idxKey, extKeys)}, }, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("data")}, diff --git a/internal/literals/simple.go b/internal/literals/simple.go index ee2c0e5..6543ac3 100644 --- a/internal/literals/simple.go +++ b/internal/literals/simple.go @@ -16,11 +16,11 @@ type simple struct{} // check that the obfuscator interface is implemented var _ obfuscator = simple{} -func (simple) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { +func (simple) obfuscate(rand *mathrand.Rand, data []byte, extKeys []*externalKey) *ast.BlockStmt { key := make([]byte, len(data)) - obfRand.Read(key) + rand.Read(key) - op := randOperator(obfRand) + op := randOperator(rand) for i, b := range key { data[i] = evalOperator(op, data[i], b) } @@ -29,12 +29,12 @@ func (simple) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("key")}, Tok: token.DEFINE, - Rhs: []ast.Expr{ah.DataToByteSlice(key)}, + Rhs: []ast.Expr{dataToByteSliceWithExtKeys(rand, key, extKeys)}, }, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("data")}, Tok: token.DEFINE, - Rhs: []ast.Expr{ah.DataToByteSlice(data)}, + Rhs: []ast.Expr{dataToByteSliceWithExtKeys(rand, data, extKeys)}, }, &ast.RangeStmt{ Key: ast.NewIdent("i"), diff --git a/internal/literals/split.go b/internal/literals/split.go index 7f223f9..14a9df9 100644 --- a/internal/literals/split.go +++ b/internal/literals/split.go @@ -66,33 +66,33 @@ func encryptChunks(chunks [][]byte, op token.Token, key byte) { } } -func (split) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { +func (split) obfuscate(rand *mathrand.Rand, data []byte, extKeys []*externalKey) *ast.BlockStmt { var chunks [][]byte // Short arrays should be divided into single-byte fragments if len(data)/maxChunkSize < minCaseCount { chunks = splitIntoOneByteChunks(data) } else { - chunks = splitIntoRandomChunks(obfRand, data) + chunks = splitIntoRandomChunks(rand, data) } // Generate indexes for cases chunk count + 1 decrypt case + 1 exit case - indexes := obfRand.Perm(len(chunks) + 2) + indexes := rand.Perm(len(chunks) + 2) - decryptKeyInitial := byte(obfRand.Uint32()) + decryptKeyInitial := byte(rand.Uint32()) decryptKey := decryptKeyInitial // Calculate decrypt key based on indexes and position. Ignore exit index for i, index := range indexes[:len(indexes)-1] { decryptKey ^= byte(index * i) } - op := randOperator(obfRand) + op := randOperator(rand) encryptChunks(chunks, op, decryptKey) decryptIndex := indexes[len(indexes)-2] exitIndex := indexes[len(indexes)-1] switchCases := []ast.Stmt{&ast.CaseClause{ List: []ast.Expr{ah.IntLit(decryptIndex)}, - Body: shuffleStmts(obfRand, + Body: shuffleStmts(rand, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("i")}, Tok: token.ASSIGN, @@ -131,15 +131,15 @@ func (split) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { } if len(chunk) != 1 { - appendCallExpr.Args = append(appendCallExpr.Args, ah.StringLit(string(chunk))) + appendCallExpr.Args = append(appendCallExpr.Args, dataToByteSliceWithExtKeys(rand, chunk, extKeys)) appendCallExpr.Ellipsis = 1 } else { - appendCallExpr.Args = append(appendCallExpr.Args, ah.IntLit(int(chunk[0]))) + appendCallExpr.Args = append(appendCallExpr.Args, byteLitWithExtKey(rand, chunk[0], extKeys, lowProb)) } switchCases = append(switchCases, &ast.CaseClause{ List: []ast.Expr{ah.IntLit(index)}, - Body: shuffleStmts(obfRand, + Body: shuffleStmts(rand, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("i")}, Tok: token.ASSIGN, @@ -168,7 +168,7 @@ func (split) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("decryptKey")}, Tok: token.DEFINE, - Rhs: []ast.Expr{ah.IntLit(int(decryptKeyInitial))}, + Rhs: []ast.Expr{ah.CallExprByName("int", byteLitWithExtKey(rand, decryptKeyInitial, extKeys, normalProb))}, }, &ast.ForStmt{ Init: &ast.AssignStmt{ @@ -199,7 +199,7 @@ func (split) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { }, &ast.SwitchStmt{ Tag: ast.NewIdent("i"), - Body: ah.BlockStmt(shuffleStmts(obfRand, switchCases...)...), + Body: ah.BlockStmt(shuffleStmts(rand, switchCases...)...), }), }, ) diff --git a/internal/literals/swap.go b/internal/literals/swap.go index b5c50fd..7beeb7e 100644 --- a/internal/literals/swap.go +++ b/internal/literals/swap.go @@ -58,13 +58,13 @@ func generateSwapCount(obfRand *mathrand.Rand, dataLen int) int { return swapCount } -func (swap) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { - swapCount := generateSwapCount(obfRand, len(data)) - shiftKey := byte(obfRand.Uint32()) +func (swap) obfuscate(rand *mathrand.Rand, data []byte, extKeys []*externalKey) *ast.BlockStmt { + swapCount := generateSwapCount(rand, len(data)) + shiftKey := byte(rand.Uint32()) - op := randOperator(obfRand) + op := randOperator(rand) - positions := genRandIntSlice(obfRand, len(data), swapCount) + positions := genRandIntSlice(rand, len(data), swapCount) for i := len(positions) - 2; i >= 0; i -= 2 { // Generate local key for xor based on random key and byte position localKey := byte(i) + byte(positions[i]^positions[i+1]) + shiftKey @@ -76,7 +76,7 @@ func (swap) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("data")}, Tok: token.DEFINE, - Rhs: []ast.Expr{ah.DataToByteSlice(data)}, + Rhs: []ast.Expr{dataToByteSliceWithExtKeys(rand, data, extKeys)}, }, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("positions")}, @@ -118,7 +118,7 @@ func (swap) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { }), }, Op: token.ADD, - Y: ah.IntLit(int(shiftKey)), + Y: byteLitWithExtKey(rand, shiftKey, extKeys, highProb), }}, }, &ast.AssignStmt{ diff --git a/main.go b/main.go index 68c70ff..1b8143a 100644 --- a/main.go +++ b/main.go @@ -1819,7 +1819,7 @@ func (tf *transformer) transformGoFile(file *ast.File) *ast.File { // because obfuscated literals sometimes escape to heap, // and that's not allowed in the runtime itself. if flagLiterals && tf.curPkg.ToObfuscate { - file = literals.Obfuscate(tf.obfRand, file, tf.info, tf.linkerVariableStrings) + file = literals.Obfuscate(tf.obfRand, file, tf.info, tf.linkerVariableStrings, randomName) // some imported constants might not be needed anymore, remove unnecessary imports tf.useAllImports(file) diff --git a/testdata/script/literals.txtar b/testdata/script/literals.txtar index 1a399d0..8c4c173 100644 --- a/testdata/script/literals.txtar +++ b/testdata/script/literals.txtar @@ -53,6 +53,9 @@ grep '^(\s+)?\w+ = .*\bappend\(\w+,(\s+\w+\[\d+\^\s.+\][\^\-+]\w+\[\d+\^\s.+\],? # XorSeed obfuscator. Detect type decFunc func(byte) decFunc grep '^\s+type \w+ func\(byte\) \w+$' debug1/test/main/extra_literals.go +# Check external keys +grep 'garbleExternalKey' debug1/test/main/extra_literals.go + # Finally, sanity check that we can build all of std with -literals. # Analogous to gogarble.txt. exec garble -literals build std