Files
garble/internal/ctrlflow/ctrlflow.go
pagran e8fe80d627 add trash block generator (#825)
add trash block generator

For making static code analysis even more difficult, added feature for
generating trash blocks that will never be executed. In combination
with control flow flattening makes it hard to separate trash code from
the real one, plus it causes a large number of trash references to
different methods.

Trash blocks contain 2 types of statements:
1. Function/method call with writing the results into local variables
and passing them to other calls
2. Shuffling or assigning random values to local variables
2024-01-16 16:01:53 +01:00

267 lines
7.4 KiB
Go

package ctrlflow
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"log"
"math"
mathrand "math/rand"
"os"
"strconv"
"strings"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/ssa"
ah "mvdan.cc/garble/internal/asthelper"
"mvdan.cc/garble/internal/ssa2ast"
)
const (
mergedFileName = "GARBLE_controlflow.go"
directiveName = "//garble:controlflow"
importPrefix = "___garble_import"
defaultBlockSplits = 0
defaultJunkJumps = 0
defaultFlattenPasses = 1
defaultTrashBlocks = 0
maxBlockSplits = math.MaxInt32
maxJunkJumps = 256
maxFlattenPasses = 4
maxTrashBlocks = 1024
minTrashBlockStmts = 1
maxTrashBlockStmts = 32
)
type directiveParamMap map[string]string
func (m directiveParamMap) GetInt(name string, def, max int) int {
rawVal, ok := m[name]
if !ok {
return def
}
if rawVal == "max" {
return max
}
val, err := strconv.Atoi(rawVal)
if err != nil {
panic(fmt.Errorf("invalid flag %q format: %v", name, err))
}
if val > max {
panic(fmt.Errorf("too big flag %q value: %d (max: %d)", name, val, max))
}
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) {
fieldsStr, ok := strings.CutPrefix(directive, directiveName)
if !ok {
return nil, false
}
fields := strings.Fields(fieldsStr)
if len(fields) == 0 {
return nil, true
}
m := make(map[string]string)
for _, v := range fields {
key, value, ok := strings.Cut(v, "=")
if ok {
m[key] = value
} else {
m[key] = ""
}
}
return m, true
}
// Obfuscate obfuscates control flow of all functions with directive using control flattening.
// All obfuscated functions are removed from the original file and moved to the new one.
// Obfuscation can be customized by passing parameters from the directive, example:
//
// //garble:controlflow flatten_passes=1 junk_jumps=0 block_splits=0
// func someMethod() {}
//
// flatten_passes - controls number of passes of control flow flattening. Have exponential complexity and more than 3 passes are not recommended in most cases.
// junk_jumps - controls how many junk jumps are added. It does not affect final binary by itself, but together with flattening linearly increases complexity.
// block_splits - controls number of times largest block must be splitted. Together with flattening improves obfuscation of long blocks without branches.
func Obfuscate(fset *token.FileSet, ssaPkg *ssa.Package, files []*ast.File, obfRand *mathrand.Rand) (newFileName string, newFile *ast.File, affectedFiles []*ast.File, err error) {
var ssaFuncs []*ssa.Function
var ssaParams []directiveParamMap
for _, file := range files {
affected := false
for _, decl := range file.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok || funcDecl.Doc == nil {
continue
}
for _, comment := range funcDecl.Doc.List {
params, hasDirective := parseDirective(comment.Text)
if !hasDirective {
continue
}
path, _ := astutil.PathEnclosingInterval(file, funcDecl.Pos(), funcDecl.Pos())
ssaFunc := ssa.EnclosingFunction(ssaPkg, path)
if ssaFunc == nil {
panic("function exists in ast but not found in ssa")
}
ssaFuncs = append(ssaFuncs, ssaFunc)
ssaParams = append(ssaParams, params)
log.Printf("detected function for controlflow %s (params: %v)", funcDecl.Name.Name, params)
// Remove inplace function from original file
// TODO: implement a complete function removal
funcDecl.Name = ast.NewIdent("_")
funcDecl.Body = ah.BlockStmt()
funcDecl.Recv = nil
funcDecl.Type = &ast.FuncType{Params: &ast.FieldList{}}
affected = true
break
}
}
if affected {
affectedFiles = append(affectedFiles, file)
}
}
if len(ssaFuncs) == 0 {
return
}
newFile = &ast.File{
Package: token.Pos(fset.Base()),
Name: ast.NewIdent(files[0].Name.Name),
}
fset.AddFile(mergedFileName, int(newFile.Package), 1) // required for correct printer output
funcConfig := ssa2ast.DefaultConfig()
imports := make(map[string]string)
funcConfig.ImportNameResolver = func(pkg *types.Package) *ast.Ident {
if pkg == nil || pkg.Path() == ssaPkg.Pkg.Path() {
return nil
}
name, ok := imports[pkg.Path()]
if !ok {
name = importPrefix + strconv.Itoa(len(imports))
imports[pkg.Path()] = name
astutil.AddNamedImport(fset, newFile, name, pkg.Path())
}
return ast.NewIdent(name)
}
var trashGen *trashGenerator
for idx, ssaFunc := range ssaFuncs {
params := ssaParams[idx]
split := params.GetInt("block_splits", defaultBlockSplits, maxBlockSplits)
junkCount := params.GetInt("junk_jumps", defaultJunkJumps, maxJunkJumps)
passes := params.GetInt("flatten_passes", defaultFlattenPasses, maxFlattenPasses)
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")
trashBlockCount := params.GetInt("trash_blocks", defaultTrashBlocks, maxTrashBlocks)
if trashBlockCount > 0 && trashGen == nil {
trashGen = newTrashGenerator(ssaPkg.Prog, funcConfig.ImportNameResolver, obfRand)
}
applyObfuscation := func(ssaFunc *ssa.Function) []dispatcherInfo {
if trashBlockCount > 0 {
addTrashBlockMarkers(ssaFunc, trashBlockCount, obfRand)
}
for i := 0; i < split; i++ {
if !applySplitting(ssaFunc, obfRand) {
break // no more candidates for splitting
}
}
if junkCount > 0 {
addJunkBlocks(ssaFunc, junkCount, obfRand)
}
var dispatchers []dispatcherInfo
for i := 0; i < passes; i++ {
if info := applyFlattening(ssaFunc, obfRand); info != nil {
dispatchers = append(dispatchers, info)
}
}
fixBlockIndexes(ssaFunc)
return dispatchers
}
dispatchers := applyObfuscation(ssaFunc)
for _, anonFunc := range ssaFunc.AnonFuncs {
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
}
funcConfig.MarkerInstrCallback = nil
if trashBlockCount > 0 {
funcConfig.MarkerInstrCallback = func(m map[string]types.Type) []ast.Stmt {
return trashGen.Generate(minTrashBlockStmts+obfRand.Intn(maxTrashBlockStmts-minTrashBlockStmts), m)
}
}
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)
}
newFileName = mergedFileName
return
}