Prevent automated plaintext extraction of literals with current tools (#930)

Some programs which could automatically reverse string literals obfuscated with `-literals` exist.

They currently work by emulating the string literal decryption functions we insert.

We prevent this naive emulation from succeeding by making the decryption functions dependent on global state.

This can still be broken with enough effort, we are curious which approach reverse-engineers come up with next, we certainly still have some ideas to make this harder.

Fixes #926
---------

Co-authored-by: Paul Scheduikat <lu4p@pm.me>
This commit is contained in:
pagran
2025-06-03 02:37:51 +02:00
committed by GitHub
parent be4462bc23
commit d47e0761eb
13 changed files with 635 additions and 82 deletions

10
hash.go
View File

@@ -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.
//

View File

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

View File

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

View File

@@ -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[<splitIdx>:<splitIdx+len(plainData)>])
// }
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 {

View File

@@ -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[<index>] = data[<index>] <random operator> byte(<external key> >> <random shift>) // 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(<obfuscated value>) <random operator> byte(<external key> >> <random shift>)
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)}
}

247
internal/literals/proxy.go Normal file
View File

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

View File

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

View File

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

View File

@@ -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"),

View File

@@ -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...)...),
}),
},
)

View File

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

View File

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

View File

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