-
-
Notifications
You must be signed in to change notification settings - Fork 268
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Without hardening, obfuscation is vulnerable to analysis via symbolic execution because all keys are opened, and it is easy to trace their connections. Added extendable (contribution-friendly) hardening mechanism that makes it harder to determine relationship between key and execution block through key obfuscation. There are 2 hardeners implemented and both are compatible with literal obfuscation, which can make analysis even more difficult.
- Loading branch information
Showing
7 changed files
with
431 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
package ctrlflow | ||
|
||
import ( | ||
"fmt" | ||
"go/ast" | ||
"go/token" | ||
mathrand "math/rand" | ||
"strconv" | ||
|
||
"golang.org/x/exp/rand" | ||
"golang.org/x/tools/go/ssa" | ||
ah "mvdan.cc/garble/internal/asthelper" | ||
"mvdan.cc/garble/internal/literals" | ||
) | ||
|
||
var hardeningMap = map[string]dispatcherHardening{ | ||
"xor": xorHardening{}, | ||
"delegate_table": delegateTableHardening{}, | ||
} | ||
|
||
func newDispatcherHardening(names []string) dispatcherHardening { | ||
hardenings := make([]dispatcherHardening, len(names)) | ||
for i, name := range names { | ||
h, ok := hardeningMap[name] | ||
if !ok { | ||
panic(fmt.Sprintf("unknown dispatcher hardening %q", name)) | ||
} | ||
hardenings[i] = h | ||
} | ||
if len(hardenings) == 1 { | ||
return hardenings[0] | ||
} | ||
return multiHardening(hardenings) | ||
} | ||
|
||
func getRandomName(rnd *mathrand.Rand) string { | ||
return "_garble" + strconv.FormatUint(rnd.Uint64(), 32) | ||
} | ||
|
||
// generateKeys is used to generate a list of pseudo-random unique keys. | ||
// Blacklist is needed to ensure that the result of a xor operation is not zero, | ||
// which can lead to incorrect obfuscation of the control flow. | ||
func generateKeys(count int, blacklistedKeys []int, rnd *mathrand.Rand) []int { | ||
m := make(map[int]bool, count) | ||
for _, i := range blacklistedKeys { | ||
m[i] = true | ||
} | ||
arr := make([]int, 0, count) | ||
for count > len(arr) { | ||
key := int(rnd.Int31()) | ||
if key == 0 || m[key] { | ||
continue | ||
} | ||
arr = append(arr, key) | ||
m[key] = true | ||
} | ||
return arr | ||
} | ||
|
||
type dispatcherHardening interface { | ||
Apply(dispatcher []cfgInfo, ssaRemap map[ssa.Value]ast.Expr, rnd *mathrand.Rand) (ast.Decl, ast.Stmt) | ||
} | ||
|
||
type multiHardening []dispatcherHardening | ||
|
||
func (r multiHardening) Apply(info []cfgInfo, ssaRemap map[ssa.Value]ast.Expr, rnd *mathrand.Rand) (ast.Decl, ast.Stmt) { | ||
return r[rnd.Intn(len(r))].Apply(info, ssaRemap, rnd) | ||
} | ||
|
||
// xorHardening replaces simple keys with obfuscated ones using xor with a global key | ||
// that is decrypted when the package is initialized. | ||
// Note: This hardening can be improved by literals obfuscation. | ||
type xorHardening struct{} | ||
|
||
func (xorHardening) Apply(dispatcher []cfgInfo, ssaRemap map[ssa.Value]ast.Expr, rnd *mathrand.Rand) (ast.Decl, ast.Stmt) { | ||
globalKeyName, localKeyName := getRandomName(rnd), getRandomName(rnd) | ||
|
||
firstKey := int(rnd.Int31()) | ||
secondKey := make([]byte, literals.MinSize+rand.Intn(literals.MinSize)) // make second part of key literals obfuscation friendly | ||
if _, err := rnd.Read(secondKey); err != nil { | ||
panic(err) | ||
} | ||
|
||
globalKey := firstKey | ||
for _, b := range secondKey { | ||
globalKey ^= int(b) | ||
} | ||
|
||
newKeys := generateKeys(len(dispatcher), []int{globalKey}, rnd) | ||
for i, info := range dispatcher { | ||
k := newKeys[i] | ||
|
||
ssaRemap[info.CompareVar] = ah.IntLit(k ^ globalKey) | ||
ssaRemap[info.StoreVar] = &ast.ParenExpr{X: &ast.BinaryExpr{X: ast.NewIdent(localKeyName), Op: token.XOR, Y: ah.IntLit(k)}} | ||
} | ||
|
||
// Global key decryption code: | ||
/* | ||
var <globalKeyName> = func(secondKey []byte) int { | ||
r := <firstKey> | ||
for _, b := range secondKey { | ||
r ^= int(b) | ||
} | ||
return r | ||
}([]byte{ <secondKey> }) | ||
*/ | ||
globalKeyDecl := &ast.GenDecl{ | ||
Tok: token.VAR, | ||
Specs: []ast.Spec{ | ||
&ast.ValueSpec{ | ||
Names: []*ast.Ident{ast.NewIdent(globalKeyName)}, | ||
Values: []ast.Expr{ah.CallExpr(&ast.FuncLit{ | ||
Type: &ast.FuncType{ | ||
Params: &ast.FieldList{List: []*ast.Field{{ | ||
Names: []*ast.Ident{ast.NewIdent("secondKey")}, | ||
Type: &ast.ArrayType{Len: ah.IntLit(len(secondKey)), Elt: ast.NewIdent("byte")}, | ||
}}}, | ||
Results: &ast.FieldList{List: []*ast.Field{{ | ||
Type: ast.NewIdent("int"), | ||
}}}, | ||
}, | ||
Body: &ast.BlockStmt{List: []ast.Stmt{ | ||
ah.AssignDefineStmt(ast.NewIdent("r"), ah.IntLit(firstKey)), | ||
&ast.RangeStmt{ | ||
Key: ast.NewIdent("_"), | ||
Value: ast.NewIdent("b"), | ||
Tok: token.DEFINE, | ||
X: ast.NewIdent("secondKey"), | ||
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.AssignStmt{ | ||
Lhs: []ast.Expr{ast.NewIdent("r")}, | ||
Tok: token.XOR_ASSIGN, | ||
Rhs: []ast.Expr{&ast.CallExpr{ | ||
Fun: ast.NewIdent("int"), | ||
Args: []ast.Expr{ast.NewIdent("b")}, | ||
}}, | ||
}}}, | ||
}, | ||
ah.ReturnStmt(ast.NewIdent("r")), | ||
}}, | ||
}, ah.DataToArray(secondKey))}, | ||
}, | ||
}, | ||
} | ||
return globalKeyDecl, ah.AssignDefineStmt(ast.NewIdent(localKeyName), ast.NewIdent(globalKeyName)) | ||
} | ||
|
||
// delegateTableHardening replaces simple keys with a decryption function call | ||
// from a table of randomly generated key decryption functions | ||
// Note: This hardening can be improved by literals obfuscation. | ||
type delegateTableHardening struct{} | ||
|
||
func (delegateTableHardening) Apply(dispatcher []cfgInfo, ssaRemap map[ssa.Value]ast.Expr, rnd *mathrand.Rand) (ast.Decl, ast.Stmt) { | ||
keySize := literals.MinSize + rand.Intn(literals.MinSize) | ||
delegateCount := keySize | ||
|
||
// Reusing multiple times one decryption function is fine, | ||
// but it doesn't make sense to generate more functions than keys. | ||
if delegateCount > len(dispatcher) { | ||
delegateCount = len(dispatcher) | ||
} | ||
|
||
delegateKeyIdxs := rnd.Perm(keySize)[:delegateCount] | ||
delegateLocalKeys := generateKeys(delegateCount, nil, rnd) | ||
|
||
key := make([]byte, keySize) | ||
if _, err := rnd.Read(key); err != nil { | ||
panic(err) | ||
} | ||
|
||
delegateIndexes := make([]int, len(dispatcher)) | ||
delegateKeys := make([]int, len(dispatcher)) | ||
for i := range delegateIndexes { | ||
delegateIdx := rnd.Intn(delegateCount) | ||
delegateIndexes[i] = delegateIdx | ||
delegateKeys[i] = int(key[delegateKeyIdxs[delegateIdx]]) ^ delegateLocalKeys[delegateIdx] | ||
} | ||
newKeys := generateKeys(len(dispatcher), delegateKeys, rnd) | ||
globalTableName := getRandomName(rnd) | ||
for i, info := range dispatcher { | ||
k, delegateIdx, delegateKey := newKeys[i], delegateIndexes[i], delegateKeys[i] | ||
encryptedKey := k ^ delegateKey | ||
|
||
ssaRemap[info.CompareVar] = ah.IntLit(k) | ||
ssaRemap[info.StoreVar] = ah.CallExpr(ah.IndexExprByExpr(ast.NewIdent(globalTableName), ah.IntLit(delegateIdx)), ah.IntLit(encryptedKey)) | ||
} | ||
|
||
delegatesAst := make([]ast.Expr, delegateCount) | ||
for i := 0; i < delegateCount; i++ { | ||
// Code for single decryption delegate: | ||
/* | ||
func(i int) int { | ||
return i ^ (int(key[<delegateKeyIdxs[i]>]) ^ <delegateLocalKeys[i]>) | ||
} | ||
*/ | ||
delegateAst := &ast.FuncLit{ | ||
Type: &ast.FuncType{ | ||
Params: &ast.FieldList{List: []*ast.Field{{ | ||
Names: []*ast.Ident{ast.NewIdent("i")}, | ||
Type: ast.NewIdent("int"), | ||
}}}, | ||
Results: &ast.FieldList{List: []*ast.Field{{ | ||
Type: ast.NewIdent("int"), | ||
}}}, | ||
}, | ||
Body: &ast.BlockStmt{List: []ast.Stmt{ | ||
&ast.ReturnStmt{Results: []ast.Expr{ | ||
&ast.BinaryExpr{ | ||
X: ast.NewIdent("i"), | ||
Op: token.XOR, | ||
Y: &ast.BinaryExpr{ | ||
X: ah.CallExprByName("int", &ast.IndexExpr{ | ||
X: ast.NewIdent("key"), | ||
Index: ah.IntLit(delegateKeyIdxs[i]), | ||
}), | ||
Op: token.XOR, | ||
Y: ah.IntLit(delegateLocalKeys[i]), | ||
}, | ||
}, | ||
}}, | ||
}}, | ||
} | ||
delegatesAst[i] = delegateAst | ||
} | ||
|
||
// Code for initialization of the decryption delegates table: | ||
/* | ||
var <globalTableName> = (func(key [<len(key)>]byte) [<len(key)>]func(int) int { | ||
return [<delegateCount>]func(int) int{ | ||
<delegatesAst> | ||
} | ||
})(<key>) | ||
*/ | ||
delegateTableDecl := &ast.GenDecl{ | ||
Tok: token.VAR, | ||
Specs: []ast.Spec{ | ||
&ast.ValueSpec{ | ||
Names: []*ast.Ident{ast.NewIdent(globalTableName)}, | ||
Values: []ast.Expr{ | ||
&ast.CallExpr{ | ||
Fun: &ast.ParenExpr{X: &ast.FuncLit{ | ||
Type: &ast.FuncType{ | ||
Params: &ast.FieldList{List: []*ast.Field{{ | ||
Names: []*ast.Ident{ast.NewIdent("key")}, | ||
Type: &ast.ArrayType{Len: ah.IntLit(len(key)), Elt: ast.NewIdent("byte")}, | ||
}}}, | ||
Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.ArrayType{ | ||
Len: ah.IntLit(delegateCount), | ||
Elt: &ast.FuncType{ | ||
Params: &ast.FieldList{List: []*ast.Field{{ | ||
Type: ast.NewIdent("int"), | ||
}}}, | ||
Results: &ast.FieldList{List: []*ast.Field{{ | ||
Type: ast.NewIdent("int"), | ||
}}}, | ||
}, | ||
}}}}, | ||
}, | ||
Body: &ast.BlockStmt{List: []ast.Stmt{ | ||
&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{ | ||
Type: &ast.ArrayType{ | ||
Len: ah.IntLit(delegateCount), | ||
Elt: &ast.FuncType{ | ||
Params: &ast.FieldList{List: []*ast.Field{{ | ||
Type: ast.NewIdent("int"), | ||
}}}, | ||
Results: &ast.FieldList{List: []*ast.Field{{ | ||
Type: ast.NewIdent("int"), | ||
}}}, | ||
}, | ||
}, | ||
Elts: delegatesAst, | ||
}}}, | ||
}}, | ||
}}, | ||
Args: []ast.Expr{ah.DataToArray(key)}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
return delegateTableDecl, nil | ||
} |
Oops, something went wrong.