Skip to content

Commit

Permalink
implement flattening hardening
Browse files Browse the repository at this point in the history
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
pagran authored and mvdan committed Nov 12, 2023
1 parent 978fd6d commit 5e80f12
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 16 deletions.
17 changes: 17 additions & 0 deletions internal/asthelper/asthelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ func DataToByteSlice(data []byte) *ast.CallExpr {
}
}

// DataToArray turns a byte slice like []byte{1, 2, 3} into an AST
// expression
func DataToArray(data []byte) *ast.CompositeLit {
elts := make([]ast.Expr, len(data))
for i, b := range data {
elts[i] = IntLit(int(b))
}

return &ast.CompositeLit{
Type: &ast.ArrayType{
Len: IntLit(len(data)),
Elt: ast.NewIdent("byte"),
},
Elts: elts,
}
}

// SelectExpr "x.sel"
func SelectExpr(x ast.Expr, sel *ast.Ident) *ast.SelectorExpr {
return &ast.SelectorExpr{
Expand Down
50 changes: 46 additions & 4 deletions internal/ctrlflow/ctrlflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ func (m directiveParamMap) GetInt(name string, def, max int) int {
return val
}

func (m directiveParamMap) StringSlice(name string) []string {
rawVal, ok := m[name]
if !ok {
return nil
}

slice := strings.Split(rawVal, ",")
if len(slice) == 0 {
return nil
}
return slice
}

// parseDirective parses a directive string and returns a map of directive parameters.
// Each parameter should be in the form "key=value" or "key"
func parseDirective(directive string) (directiveParamMap, bool) {
Expand Down Expand Up @@ -169,8 +182,9 @@ func Obfuscate(fset *token.FileSet, ssaPkg *ssa.Package, files []*ast.File, obfR
if passes == 0 {
fmt.Fprintf(os.Stderr, "control flow obfuscation for %q function has no effect on the resulting binary, to fix this flatten_passes must be greater than zero", ssaFunc)
}
flattenHardening := params.StringSlice("flatten_hardening")

applyObfuscation := func(ssaFunc *ssa.Function) {
applyObfuscation := func(ssaFunc *ssa.Function) []dispatcherInfo {
for i := 0; i < split; i++ {
if !applySplitting(ssaFunc, obfRand) {
break // no more candidates for splitting
Expand All @@ -179,21 +193,49 @@ func Obfuscate(fset *token.FileSet, ssaPkg *ssa.Package, files []*ast.File, obfR
if junkCount > 0 {
addJunkBlocks(ssaFunc, junkCount, obfRand)
}
var dispatchers []dispatcherInfo
for i := 0; i < passes; i++ {
applyFlattening(ssaFunc, obfRand)
if info := applyFlattening(ssaFunc, obfRand); info != nil {
dispatchers = append(dispatchers, info)
}
}
fixBlockIndexes(ssaFunc)
return dispatchers
}

applyObfuscation(ssaFunc)
dispatchers := applyObfuscation(ssaFunc)
for _, anonFunc := range ssaFunc.AnonFuncs {
applyObfuscation(anonFunc)
dispatchers = append(dispatchers, applyObfuscation(anonFunc)...)
}

// Because of ssa package api limitations, implementation of hardening for control flow flattening dispatcher
// is implemented during converting by replacing key values with obfuscated ast expressions
var prologues []ast.Stmt
if len(flattenHardening) > 0 && len(dispatchers) > 0 {
hardening := newDispatcherHardening(flattenHardening)

ssaRemap := make(map[ssa.Value]ast.Expr)
for _, dispatcher := range dispatchers {
decl, stmt := hardening.Apply(dispatcher, ssaRemap, obfRand)
if decl != nil {
newFile.Decls = append(newFile.Decls, decl)
}
if stmt != nil {
prologues = append(prologues, stmt)
}
}
funcConfig.SsaValueRemap = ssaRemap
} else {
funcConfig.SsaValueRemap = nil
}

astFunc, err := ssa2ast.Convert(ssaFunc, funcConfig)
if err != nil {
return "", nil, nil, err
}
if len(prologues) > 0 {
astFunc.Body.List = append(prologues, astFunc.Body.List...)
}
newFile.Decls = append(newFile.Decls, astFunc)
}

Expand Down
283 changes: 283 additions & 0 deletions internal/ctrlflow/hardening.go
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
}
Loading

0 comments on commit 5e80f12

Please sign in to comment.