diff --git a/internal/literals/literals.go b/internal/literals/literals.go index 9695adc..8619ad9 100644 --- a/internal/literals/literals.go +++ b/internal/literals/literals.go @@ -28,13 +28,8 @@ import ( // should largely stop being used. const maxSizeBytes = 2 << 10 // KiB -func randObfuscator() obfuscator { - randPos := mathrand.Intn(len(obfuscators)) - return obfuscators[randPos] -} - // Obfuscate replaces literals with obfuscated anonymous functions. -func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, linkStrings map[*types.Var]string) *ast.File { +func Obfuscate(obfRand *mathrand.Rand, file *ast.File, info *types.Info, linkStrings map[*types.Var]string) *ast.File { pre := func(cursor *astutil.Cursor) bool { switch node := cursor.Node().(type) { case *ast.GenDecl: @@ -72,7 +67,7 @@ func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, linkString return true } - cursor.Replace(withPos(obfuscateString(value), node.Pos())) + cursor.Replace(withPos(obfuscateString(obfRand, value), node.Pos())) return true } @@ -89,7 +84,7 @@ func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, linkString } if child, ok := node.X.(*ast.CompositeLit); ok { - newnode := handleCompositeLiteral(true, child, info) + newnode := handleCompositeLiteral(obfRand, true, child, info) if newnode != nil { cursor.Replace(newnode) } @@ -108,7 +103,7 @@ func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, linkString return true } - newnode := handleCompositeLiteral(false, node, info) + newnode := handleCompositeLiteral(obfRand, false, node, info) if newnode != nil { cursor.Replace(newnode) } @@ -126,7 +121,7 @@ func Obfuscate(file *ast.File, info *types.Info, fset *token.FileSet, linkString // // If the input is not a byte slice or array, the node is returned as-is and // the second return value will be false. -func handleCompositeLiteral(isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node { +func handleCompositeLiteral(obfRand *mathrand.Rand, isPointer bool, node *ast.CompositeLit, info *types.Info) ast.Node { if len(node.Elts) == 0 || len(node.Elts) > maxSizeBytes { return nil } @@ -169,10 +164,10 @@ func handleCompositeLiteral(isPointer bool, node *ast.CompositeLit, info *types. } if arrayLen > 0 { - return withPos(obfuscateByteArray(isPointer, data, arrayLen), node.Pos()) + return withPos(obfuscateByteArray(obfRand, isPointer, data, arrayLen), node.Pos()) } - return withPos(obfuscateByteSlice(isPointer, data), node.Pos()) + return withPos(obfuscateByteSlice(obfRand, isPointer, data), node.Pos()) } // withPos sets any token.Pos fields under node which affect printing to pos. @@ -220,18 +215,18 @@ func withPos(node ast.Node, pos token.Pos) ast.Node { return node } -func obfuscateString(data string) *ast.CallExpr { - obfuscator := randObfuscator() - block := obfuscator.obfuscate([]byte(data)) +func obfuscateString(obfRand *mathrand.Rand, data string) *ast.CallExpr { + obfuscator := obfuscators[obfRand.Intn(len(obfuscators))] + block := obfuscator.obfuscate(obfRand, []byte(data)) block.List = append(block.List, ah.ReturnStmt(ah.CallExpr(ast.NewIdent("string"), ast.NewIdent("data")))) return ah.LambdaCall(ast.NewIdent("string"), block) } -func obfuscateByteSlice(isPointer bool, data []byte) *ast.CallExpr { - obfuscator := randObfuscator() - block := obfuscator.obfuscate(data) +func obfuscateByteSlice(obfRand *mathrand.Rand, isPointer bool, data []byte) *ast.CallExpr { + obfuscator := obfuscators[obfRand.Intn(len(obfuscators))] + block := obfuscator.obfuscate(obfRand, data) if isPointer { block.List = append(block.List, ah.ReturnStmt(&ast.UnaryExpr{ @@ -247,9 +242,9 @@ func obfuscateByteSlice(isPointer bool, data []byte) *ast.CallExpr { return ah.LambdaCall(&ast.ArrayType{Elt: ast.NewIdent("byte")}, block) } -func obfuscateByteArray(isPointer bool, data []byte, length int64) *ast.CallExpr { - obfuscator := randObfuscator() - block := obfuscator.obfuscate(data) +func obfuscateByteArray(obfRand *mathrand.Rand, isPointer bool, data []byte, length int64) *ast.CallExpr { + obfuscator := obfuscators[obfRand.Intn(len(obfuscators))] + block := obfuscator.obfuscate(obfRand, data) arrayType := &ast.ArrayType{ Len: ah.IntLit(int(length)), diff --git a/internal/literals/obfuscators.go b/internal/literals/obfuscators.go index 3f13f11..24e66a6 100644 --- a/internal/literals/obfuscators.go +++ b/internal/literals/obfuscators.go @@ -12,7 +12,7 @@ import ( // obfuscator takes a byte slice and converts it to a ast.BlockStmt type obfuscator interface { - obfuscate(data []byte) *ast.BlockStmt + obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt } // obfuscators contains all types which implement the obfuscator Interface @@ -24,33 +24,17 @@ var obfuscators = []obfuscator{ // seed{}, TODO: re-enable once https://go.dev/issue/47631 is fixed in Go 1.20 } -// 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 _, err := mathrand.Read(buffer); err != nil { - panic(fmt.Sprintf("couldn't generate random key: %v", err)) - } -} - -func genRandByte() byte { - bytes := make([]byte, 1) - genRandBytes(bytes) - return bytes[0] -} - -func genRandIntSlice(max, count int) []int { +func genRandIntSlice(obfRand *mathrand.Rand, max, count int) []int { indexes := make([]int, count) for i := 0; i < count; i++ { - indexes[i] = mathrand.Intn(max) + indexes[i] = obfRand.Intn(max) } return indexes } -func randOperator() token.Token { +func randOperator(obfRand *mathrand.Rand) token.Token { operatorTokens := [...]token.Token{token.XOR, token.ADD, token.SUB} - return operatorTokens[mathrand.Intn(len(operatorTokens))] + return operatorTokens[obfRand.Intn(len(operatorTokens))] } func evalOperator(t token.Token, x, y byte) byte { diff --git a/internal/literals/seed.go b/internal/literals/seed.go index 32a2d01..d842702 100644 --- a/internal/literals/seed.go +++ b/internal/literals/seed.go @@ -6,6 +6,7 @@ package literals import ( "go/ast" "go/token" + mathrand "math/rand" ah "mvdan.cc/garble/internal/asthelper" ) @@ -15,11 +16,11 @@ type seed struct{} // check that the obfuscator interface is implemented var _ obfuscator = seed{} -func (seed) obfuscate(data []byte) *ast.BlockStmt { - seed := genRandByte() +func (seed) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { + seed := byte(obfRand.Uint32()) originalSeed := seed - op := randOperator() + op := randOperator(obfRand) var callExpr *ast.CallExpr for i, b := range data { diff --git a/internal/literals/shuffle.go b/internal/literals/shuffle.go index f9d3168..be1de5a 100644 --- a/internal/literals/shuffle.go +++ b/internal/literals/shuffle.go @@ -16,21 +16,21 @@ type shuffle struct{} // check that the obfuscator interface is implemented var _ obfuscator = shuffle{} -func (shuffle) obfuscate(data []byte) *ast.BlockStmt { +func (shuffle) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { key := make([]byte, len(data)) - genRandBytes(key) + obfRand.Read(key) fullData := make([]byte, len(data)+len(key)) operators := make([]token.Token, len(fullData)) for i := range operators { - operators[i] = randOperator() + operators[i] = randOperator(obfRand) } for i, b := range key { fullData[i], fullData[i+len(data)] = evalOperator(operators[i], data[i], b), b } - shuffledIdxs := mathrand.Perm(len(fullData)) + shuffledIdxs := obfRand.Perm(len(fullData)) shuffledFullData := make([]byte, len(fullData)) for i, b := range fullData { diff --git a/internal/literals/simple.go b/internal/literals/simple.go index 1a776aa..ee2c0e5 100644 --- a/internal/literals/simple.go +++ b/internal/literals/simple.go @@ -6,6 +6,7 @@ package literals import ( "go/ast" "go/token" + mathrand "math/rand" ah "mvdan.cc/garble/internal/asthelper" ) @@ -15,11 +16,11 @@ type simple struct{} // check that the obfuscator interface is implemented var _ obfuscator = simple{} -func (simple) obfuscate(data []byte) *ast.BlockStmt { +func (simple) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { key := make([]byte, len(data)) - genRandBytes(key) + obfRand.Read(key) - op := randOperator() + op := randOperator(obfRand) for i, b := range key { data[i] = evalOperator(op, data[i], b) } diff --git a/internal/literals/split.go b/internal/literals/split.go index 314ca22..584c5c2 100644 --- a/internal/literals/split.go +++ b/internal/literals/split.go @@ -23,14 +23,14 @@ type split struct{} // check that the obfuscator interface is implemented var _ obfuscator = split{} -func splitIntoRandomChunks(data []byte) [][]byte { +func splitIntoRandomChunks(obfRand *mathrand.Rand, data []byte) [][]byte { if len(data) == 1 { return [][]byte{data} } var chunks [][]byte for len(data) > 0 { - chunkSize := 1 + mathrand.Intn(maxChunkSize) + chunkSize := 1 + obfRand.Intn(maxChunkSize) if chunkSize > len(data) { chunkSize = len(data) } @@ -51,8 +51,8 @@ func splitIntoOneByteChunks(data []byte) [][]byte { // Shuffles the passed array and returns it back. // Applies for inline declaration of randomly shuffled statement arrays -func shuffleStmts(stmts ...ast.Stmt) []ast.Stmt { - mathrand.Shuffle(len(stmts), func(i, j int) { +func shuffleStmts(obfRand *mathrand.Rand, stmts ...ast.Stmt) []ast.Stmt { + obfRand.Shuffle(len(stmts), func(i, j int) { stmts[i], stmts[j] = stmts[j], stmts[i] }) return stmts @@ -69,33 +69,33 @@ func encryptChunks(chunks [][]byte, op token.Token, key byte) { } } -func (split) obfuscate(data []byte) *ast.BlockStmt { +func (split) obfuscate(obfRand *mathrand.Rand, data []byte) *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(data) + chunks = splitIntoRandomChunks(obfRand, data) } // Generate indexes for cases chunk count + 1 decrypt case + 1 exit case - indexes := mathrand.Perm(len(chunks) + 2) + indexes := obfRand.Perm(len(chunks) + 2) - decryptKeyInitial := genRandByte() + decryptKeyInitial := byte(obfRand.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() + op := randOperator(obfRand) 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( + Body: shuffleStmts(obfRand, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("i")}, Tok: token.ASSIGN, @@ -142,7 +142,7 @@ func (split) obfuscate(data []byte) *ast.BlockStmt { switchCases = append(switchCases, &ast.CaseClause{ List: []ast.Expr{ah.IntLit(index)}, - Body: shuffleStmts( + Body: shuffleStmts(obfRand, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("i")}, Tok: token.ASSIGN, @@ -206,7 +206,7 @@ func (split) obfuscate(data []byte) *ast.BlockStmt { }, &ast.SwitchStmt{ Tag: ast.NewIdent("i"), - Body: ah.BlockStmt(shuffleStmts(switchCases...)...), + Body: ah.BlockStmt(shuffleStmts(obfRand, switchCases...)...), }), }, ) diff --git a/internal/literals/swap.go b/internal/literals/swap.go index 5fd88c1..b5c50fd 100644 --- a/internal/literals/swap.go +++ b/internal/literals/swap.go @@ -45,12 +45,12 @@ func positionsToSlice(data []int) *ast.CompositeLit { } // Generates a random even swap count based on the length of data -func generateSwapCount(dataLen int) int { +func generateSwapCount(obfRand *mathrand.Rand, dataLen int) int { swapCount := dataLen maxExtraPositions := dataLen / 2 // Limit the number of extra positions to half the data length if maxExtraPositions > 1 { - swapCount += mathrand.Intn(maxExtraPositions) + swapCount += obfRand.Intn(maxExtraPositions) } if swapCount%2 != 0 { // Swap count must be even swapCount++ @@ -58,13 +58,13 @@ func generateSwapCount(dataLen int) int { return swapCount } -func (swap) obfuscate(data []byte) *ast.BlockStmt { - swapCount := generateSwapCount(len(data)) - shiftKey := genRandByte() +func (swap) obfuscate(obfRand *mathrand.Rand, data []byte) *ast.BlockStmt { + swapCount := generateSwapCount(obfRand, len(data)) + shiftKey := byte(obfRand.Uint32()) - op := randOperator() + op := randOperator(obfRand) - positions := genRandIntSlice(len(data), swapCount) + positions := genRandIntSlice(obfRand, 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 diff --git a/main.go b/main.go index 88befa0..8197c32 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ package main import ( "bytes" - "crypto/rand" + cryptorand "crypto/rand" "encoding/base64" "encoding/binary" "encoding/gob" @@ -78,7 +78,7 @@ func (f seedFlag) String() string { func (f *seedFlag) Set(s string) error { if s == "random" { f.bytes = make([]byte, 16) // random 128 bit seed - if _, err := rand.Read(f.bytes); err != nil { + if _, err := cryptorand.Read(f.bytes); err != nil { return fmt.Errorf("error generating random seed: %v", err) } } else { @@ -90,6 +90,8 @@ func (f *seedFlag) Set(s string) error { return fmt.Errorf("error decoding seed: %v", err) } + // TODO: Note that we always use 8 bytes; any bytes after that are + // entirely ignored. That may be confusing to the end user. if len(seed) < 8 { return fmt.Errorf("-seed needs at least 8 bytes, have %d", len(seed)) } @@ -151,6 +153,13 @@ var ( // Basic information about the package being currently compiled or linked. curPkg *listedPackage + + // obfRand is initialized by transformCompile and used during obfuscation. + // It is left nil at init time, so that we only use it after it has been + // properly initialized with a deterministic seed. + // It must only be used for deterministic obfuscation; + // if it is used for any other purpose, we may lose determinism. + obfRand *mathrand.Rand ) type importerWithMap func(path, dir string, mode types.ImportMode) (*types.Package, error) @@ -848,7 +857,7 @@ func transformCompile(args []string) ([]string, error) { randSeed = flagSeed.bytes } // log.Printf("seeding math/rand with %x\n", randSeed) - mathrand.Seed(int64(binary.BigEndian.Uint64(randSeed))) + obfRand = mathrand.New(mathrand.NewSource(int64(binary.BigEndian.Uint64(randSeed)))) if err := tf.prefillObjectMaps(files); err != nil { return nil, err @@ -1652,7 +1661,7 @@ func (tf *transformer) transformGo(file *ast.File) *ast.File { // because obfuscated literals sometimes escape to heap, // and that's not allowed in the runtime itself. if flagLiterals && curPkg.ToObfuscate { - file = literals.Obfuscate(file, tf.info, fset, tf.linkerVariableStrings) + file = literals.Obfuscate(obfRand, file, tf.info, tf.linkerVariableStrings) // some imported constants might not be needed anymore, remove unnecessary imports tf.removeUnnecessaryImports(file) diff --git a/main_test.go b/main_test.go index 0a001ed..f5825b2 100644 --- a/main_test.go +++ b/main_test.go @@ -230,9 +230,11 @@ func bincmp(ts *testscript.TestScript, neg bool, args []string) { } } +var testRand = mathrand.New(mathrand.NewSource(time.Now().UnixNano())) + func generateStringLit(size int) *ast.BasicLit { buffer := make([]byte, size) - _, err := mathrand.Read(buffer) + _, err := testRand.Read(buffer) if err != nil { panic(err) } @@ -253,7 +255,7 @@ func generateLiterals(ts *testscript.TestScript, neg bool, args []string) { // Add 100 randomly small literals. var statements []ast.Stmt for i := 0; i < 100; i++ { - literal := generateStringLit(1 + mathrand.Intn(255)) + literal := generateStringLit(1 + testRand.Intn(255)) statements = append(statements, &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("_")}, Tok: token.ASSIGN,