You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
garble/internal/ctrlflow/ctrlflow.go

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
}