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/transform.go

312 lines
8.9 KiB
Go

package ctrlflow
import (
"go/constant"
"go/token"
"go/types"
mathrand "math/rand"
"strconv"
"golang.org/x/tools/go/ssa"
"mvdan.cc/garble/internal/ssa2ast"
)
type blockMapping struct {
Fake, Target *ssa.BasicBlock
}
type cfgInfo struct {
CompareVar ssa.Value
StoreVar ssa.Value
}
type dispatcherInfo []cfgInfo
// applyFlattening adds a dispatcher block and uses ssa.Phi to redirect all ssa.Jump and ssa.If to the dispatcher,
// additionally shuffle all blocks
func applyFlattening(ssaFunc *ssa.Function, obfRand *mathrand.Rand) dispatcherInfo {
if len(ssaFunc.Blocks) < 3 {
return nil
}
phiInstr := &ssa.Phi{Comment: "ctrflow.phi"}
setType(phiInstr, types.Typ[types.Int])
entryBlock := &ssa.BasicBlock{
Comment: "ctrflow.entry",
Instrs: []ssa.Instruction{phiInstr},
}
setBlockParent(entryBlock, ssaFunc)
makeJumpBlock := func(from *ssa.BasicBlock) *ssa.BasicBlock {
jumpBlock := &ssa.BasicBlock{
Comment: "ctrflow.jump",
Instrs: []ssa.Instruction{&ssa.Jump{}},
Preds: []*ssa.BasicBlock{from},
Succs: []*ssa.BasicBlock{entryBlock},
}
setBlockParent(jumpBlock, ssaFunc)
return jumpBlock
}
// map for track fake block -> real block jump
var blocksMapping []blockMapping
for _, block := range ssaFunc.Blocks {
existInstr := block.Instrs[len(block.Instrs)-1]
switch existInstr.(type) {
case *ssa.Jump:
targetBlock := block.Succs[0]
fakeBlock := makeJumpBlock(block)
blocksMapping = append(blocksMapping, blockMapping{fakeBlock, targetBlock})
block.Succs[0] = fakeBlock
case *ssa.If:
tblock, fblock := block.Succs[0], block.Succs[1]
fakeTblock, fakeFblock := makeJumpBlock(tblock), makeJumpBlock(fblock)
blocksMapping = append(blocksMapping, blockMapping{fakeTblock, tblock})
blocksMapping = append(blocksMapping, blockMapping{fakeFblock, fblock})
block.Succs[0] = fakeTblock
block.Succs[1] = fakeFblock
case *ssa.Return, *ssa.Panic:
// control flow flattening is not applicable
default:
panic("unreachable")
}
}
phiIdxs := obfRand.Perm(len(blocksMapping))
for i := range phiIdxs {
phiIdxs[i]++ // 0 reserved for real entry block
}
var info dispatcherInfo
var entriesBlocks []*ssa.BasicBlock
obfuscatedBlocks := ssaFunc.Blocks
for i, m := range blocksMapping {
entryBlock.Preds = append(entryBlock.Preds, m.Fake)
val := phiIdxs[i]
cfg := cfgInfo{StoreVar: makeSsaInt(val), CompareVar: makeSsaInt(val)}
info = append(info, cfg)
phiInstr.Edges = append(phiInstr.Edges, cfg.StoreVar)
obfuscatedBlocks = append(obfuscatedBlocks, m.Fake)
cond := &ssa.BinOp{X: phiInstr, Op: token.EQL, Y: cfg.CompareVar}
setType(cond, types.Typ[types.Bool])
*phiInstr.Referrers() = append(*phiInstr.Referrers(), cond)
ifInstr := &ssa.If{Cond: cond}
*cond.Referrers() = append(*cond.Referrers(), ifInstr)
ifBlock := &ssa.BasicBlock{
Instrs: []ssa.Instruction{cond, ifInstr},
Succs: []*ssa.BasicBlock{m.Target, nil}, // false branch fulfilled in next iteration or linked to real entry block
}
setBlockParent(ifBlock, ssaFunc)
setBlock(cond, ifBlock)
setBlock(ifInstr, ifBlock)
entriesBlocks = append(entriesBlocks, ifBlock)
if i == 0 {
entryBlock.Instrs = append(entryBlock.Instrs, &ssa.Jump{})
entryBlock.Succs = []*ssa.BasicBlock{ifBlock}
ifBlock.Preds = append(ifBlock.Preds, entryBlock)
} else {
// link previous block to current
entriesBlocks[i-1].Succs[1] = ifBlock
ifBlock.Preds = append(ifBlock.Preds, entriesBlocks[i-1])
}
}
lastFakeEntry := entriesBlocks[len(entriesBlocks)-1]
realEntryBlock := ssaFunc.Blocks[0]
lastFakeEntry.Succs[1] = realEntryBlock
realEntryBlock.Preds = append(realEntryBlock.Preds, lastFakeEntry)
obfuscatedBlocks = append(obfuscatedBlocks, entriesBlocks...)
obfRand.Shuffle(len(obfuscatedBlocks), func(i, j int) {
obfuscatedBlocks[i], obfuscatedBlocks[j] = obfuscatedBlocks[j], obfuscatedBlocks[i]
})
ssaFunc.Blocks = append([]*ssa.BasicBlock{entryBlock}, obfuscatedBlocks...)
return info
}
// addJunkBlocks adds junk jumps into random blocks. Can create chains of junk jumps.
func addJunkBlocks(ssaFunc *ssa.Function, count int, obfRand *mathrand.Rand) {
if count == 0 {
return
}
var candidates []*ssa.BasicBlock
for _, block := range ssaFunc.Blocks {
if len(block.Succs) > 0 {
candidates = append(candidates, block)
}
}
if len(candidates) == 0 {
return
}
for i := 0; i < count; i++ {
targetBlock := candidates[obfRand.Intn(len(candidates))]
succsIdx := obfRand.Intn(len(targetBlock.Succs))
succs := targetBlock.Succs[succsIdx]
fakeBlock := &ssa.BasicBlock{
Comment: "ctrflow.fake." + strconv.Itoa(i),
Instrs: []ssa.Instruction{&ssa.Jump{}},
Preds: []*ssa.BasicBlock{targetBlock},
Succs: []*ssa.BasicBlock{succs},
}
setBlockParent(fakeBlock, ssaFunc)
targetBlock.Succs[succsIdx] = fakeBlock
ssaFunc.Blocks = append(ssaFunc.Blocks, fakeBlock)
candidates = append(candidates, fakeBlock)
}
}
// applySplitting splits biggest block into 2 parts of random size.
// Returns false if no block large enough for splitting is found
func applySplitting(ssaFunc *ssa.Function, obfRand *mathrand.Rand) bool {
var targetBlock *ssa.BasicBlock
for _, block := range ssaFunc.Blocks {
if targetBlock == nil || len(block.Instrs) > len(targetBlock.Instrs) {
targetBlock = block
}
}
const minInstrCount = 1 + 1 // 1 exit instruction + 1 any instruction
if targetBlock == nil || len(targetBlock.Instrs) <= minInstrCount {
return false
}
splitIdx := 1 + obfRand.Intn(len(targetBlock.Instrs)-2)
firstPart := make([]ssa.Instruction, splitIdx+1)
copy(firstPart, targetBlock.Instrs)
firstPart[len(firstPart)-1] = &ssa.Jump{}
secondPart := targetBlock.Instrs[splitIdx:]
targetBlock.Instrs = firstPart
newBlock := &ssa.BasicBlock{
Comment: "ctrflow.split." + strconv.Itoa(targetBlock.Index),
Instrs: secondPart,
Preds: []*ssa.BasicBlock{targetBlock},
Succs: targetBlock.Succs,
}
setBlockParent(newBlock, ssaFunc)
for _, instr := range newBlock.Instrs {
setBlock(instr, newBlock)
}
// Fix preds for ssa.Phi working
for _, succ := range targetBlock.Succs {
for i, pred := range succ.Preds {
if pred == targetBlock {
succ.Preds[i] = newBlock
}
}
}
ssaFunc.Blocks = append(ssaFunc.Blocks, newBlock)
targetBlock.Succs = []*ssa.BasicBlock{newBlock}
return true
}
// randomAlwaysFalseCond generates two random int32 and a random compare operator that always returns false, examples:
// 1350205738 <= 734900678
// 1400381511 >= 1621623831
// 2062290251 < 1908004916
// 1228588894 > 1819094321
// 2094727349 == 955574490
func randomAlwaysFalseCond(obfRand *mathrand.Rand) (*ssa.Const, token.Token, *ssa.Const) {
tokens := []token.Token{token.EQL, token.NEQ, token.LSS, token.LEQ, token.GTR, token.GEQ}
val1, val2 := constant.MakeInt64(int64(obfRand.Int31())), constant.MakeInt64(int64(obfRand.Int31()))
var candidates []token.Token
for _, t := range tokens {
if !constant.Compare(val1, t, val2) {
candidates = append(candidates, t)
}
}
return ssa.NewConst(val1, types.Typ[types.Int]), candidates[obfRand.Intn(len(candidates))], ssa.NewConst(val2, types.Typ[types.Int])
}
// addTrashBlockMarkers adds unreachable blocks with ssa2ast.MarkerInstr to further generate trash statements
func addTrashBlockMarkers(ssaFunc *ssa.Function, count int, obfRand *mathrand.Rand) {
var candidates []*ssa.BasicBlock
for _, block := range ssaFunc.Blocks {
if len(block.Succs) > 0 {
candidates = append(candidates, block)
}
}
if len(candidates) == 0 {
return
}
for i := 0; i < count; i++ {
targetBlock := candidates[obfRand.Intn(len(candidates))]
succsIdx := obfRand.Intn(len(targetBlock.Succs))
succs := targetBlock.Succs[succsIdx]
val1, op, val2 := randomAlwaysFalseCond(obfRand)
phiInstr := &ssa.Phi{
Edges: []ssa.Value{val1},
}
setType(phiInstr, types.Typ[types.Int])
binOpInstr := &ssa.BinOp{
X: phiInstr,
Op: op,
Y: val2,
}
setType(binOpInstr, types.Typ[types.Bool])
jmpInstr := &ssa.If{Cond: binOpInstr}
*binOpInstr.Referrers() = append(*binOpInstr.Referrers(), jmpInstr)
trashBlock := &ssa.BasicBlock{
Comment: "ctrflow.trash." + strconv.Itoa(targetBlock.Index),
Instrs: []ssa.Instruction{
ssa2ast.MarkerInstr,
&ssa.Jump{},
},
}
setBlockParent(trashBlock, ssaFunc)
trashBlockDispatch := &ssa.BasicBlock{
Comment: "ctrflow.trash.cond." + strconv.Itoa(targetBlock.Index),
Instrs: []ssa.Instruction{
phiInstr,
binOpInstr,
jmpInstr,
},
Preds: []*ssa.BasicBlock{targetBlock},
Succs: []*ssa.BasicBlock{trashBlock, succs},
}
setBlockParent(trashBlockDispatch, ssaFunc)
targetBlock.Succs[succsIdx] = trashBlockDispatch
trashBlock.Preds = []*ssa.BasicBlock{trashBlockDispatch, trashBlock}
trashBlock.Succs = []*ssa.BasicBlock{trashBlock}
ssaFunc.Blocks = append(ssaFunc.Blocks, trashBlockDispatch, trashBlock)
}
}
func fixBlockIndexes(ssaFunc *ssa.Function) {
for i, block := range ssaFunc.Blocks {
block.Index = i
}
}