Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/ctrlflow/hardening.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (xorHardening) Apply(dispatcher []cfgInfo, ssaRemap map[ssa.Value]ast.Expr,
globalKeyName, localKeyName := getRandomName(rnd), getRandomName(rnd)

firstKey := int(rnd.Int31())
secondKey := make([]byte, literals.MinSize+mathrand.Intn(literals.MinSize)) // make second part of key literals obfuscation friendly
secondKey := make([]byte, literals.MinSize+rnd.Intn(literals.MinSize)) // make second part of key literals obfuscation friendly
if _, err := rnd.Read(secondKey); err != nil {
panic(err)
}
Expand Down Expand Up @@ -149,7 +149,7 @@ func (xorHardening) Apply(dispatcher []cfgInfo, ssaRemap map[ssa.Value]ast.Expr,
type delegateTableHardening struct{}

func (delegateTableHardening) Apply(dispatcher []cfgInfo, ssaRemap map[ssa.Value]ast.Expr, rnd *mathrand.Rand) (ast.Decl, ast.Stmt) {
keySize := literals.MinSize + mathrand.Intn(literals.MinSize)
keySize := literals.MinSize + rnd.Intn(literals.MinSize)

// Reusing multiple times one decryption function is fine,
// but it doesn't make sense to generate more functions than keys.
Expand Down
34 changes: 32 additions & 2 deletions internal/ctrlflow/trash.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,22 @@ func (d *definedVar) HasRefs() bool {

// initialize scans and writes all supported functions in all non-internal packages used in the program
func (t *trashGenerator) initialize(ssaProg *ssa.Program) {
for _, p := range ssaProg.AllPackages() {
// AllPackages and Package.Members are backed by maps, whose iteration order
// is random. The candidate pools built here are later indexed with the
// seeded PRNG, so a random order would pick different globals and functions
// on each run and break reproducible builds. Walk both in a stable order.
pkgs := ssaProg.AllPackages()
slices.SortFunc(pkgs, func(a, b *ssa.Package) int {
return strings.Compare(a.Pkg.Path(), b.Pkg.Path())
})
for _, p := range pkgs {
if isInternal(p.Pkg.Path()) || p.Pkg.Name() == "main" {
continue
}
var pkgFuncs []*types.Func
for _, member := range p.Members {
memberNames := slices.Sorted(maps.Keys(p.Members))
for _, memberName := range memberNames {
member := p.Members[memberName]
if !token.IsExported(member.Name()) {
continue
}
Expand Down Expand Up @@ -283,6 +293,8 @@ func (t *trashGenerator) chooseRandomVar(typ types.Type, vars map[string]*define
if len(candidates) == 0 {
return nil
}
// vars is a map, so sort before the seeded random pick to stay reproducible.
slices.Sort(candidates)

targetVarName := candidates[t.rand.Intn(len(candidates))]
targetVar := vars[targetVarName]
Expand Down Expand Up @@ -326,6 +338,10 @@ func (t *trashGenerator) generateRandomConst(p types.Type, rand *mathrand.Rand)
if len(candidates) == 0 {
panic(fmt.Errorf("unsupported type: %v", p))
}
// valueGenerators is a map, so sort before the seeded random pick.
slices.SortFunc(candidates, func(a, b types.Type) int {
return strings.Compare(a.String(), b.String())
})

generatorType := candidates[rand.Intn(len(candidates))]
generator := valueGenerators[generatorType]
Expand Down Expand Up @@ -396,7 +412,19 @@ func (t *trashGenerator) chooseRandomMethod(vars map[string]*definedVar) (string
return "", nil
}

// groupedCandidates is a map keyed by type; its values are built by
// iterating the vars map. Sort each group's names, then order the types by
// their first name. Variable names are unique across vars, so this is a
// strict total order, unlike sorting by Type.String() (two distinct type
// objects can share a String, and slices.SortFunc is not stable, so a tie
// would leave the order at the mercy of the random map iteration).
for _, names := range groupedCandidates {
slices.Sort(names)
}
candidateTypes := slices.Collect(maps.Keys(groupedCandidates))
slices.SortFunc(candidateTypes, func(a, b types.Type) int {
return strings.Compare(groupedCandidates[a][0], groupedCandidates[b][0])
})
candidateType := candidateTypes[t.rand.Intn(len(candidateTypes))]
candidates := groupedCandidates[candidateType]

Expand Down Expand Up @@ -497,6 +525,8 @@ func (t *trashGenerator) generateAssign(vars map[string]*definedVar) ast.Stmt {
varNames = append(varNames, name)
}
}
// vars is a map: sort first so the seeded shuffle permutes a stable order.
slices.Sort(varNames)
t.rand.Shuffle(len(varNames), func(i, j int) {
varNames[i], varNames[j] = varNames[j], varNames[i]
})
Expand Down
46 changes: 46 additions & 0 deletions internal/ctrlflow/trash_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package ctrlflow

import (
"bytes"
"fmt"
"go/ast"
"go/importer"
"go/parser"
"go/printer"
"go/token"
"go/types"
Expand Down Expand Up @@ -98,3 +100,47 @@ func Test_generateTrashBlock(t *testing.T) {
printer.Fprint(os.Stdout, fset, file)
buildPkg(file)
}

// TestObfuscateDeterministic ensures control flow obfuscation is reproducible:
// obfuscating the same package with the same seed must produce identical
// output. It exercises the flatten hardening (which once drew its key sizes
// from the global, unseeded math/rand source), trash generation and the
// ssa2ast var block, whose candidate pools were ordered by Go map iteration.
func TestObfuscateDeterministic(t *testing.T) {
const seed = 12345
const src = `package main

import (
_ "fmt"
_ "os"
)

//garble:controlflow flatten_passes=1 junk_jumps=5 block_splits=3 trash_blocks=8 flatten_hardening=xor,delegate_table
func compute(n int) int {
if n > 0 {
return n * 2
}
return -n
}

func main() {
_ = compute(10)
}
`
run := func() string {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "main.go", src, parser.ParseComments)
qt.Assert(t, qt.IsNil(err))
ssaPkg, _, err := ssautil.BuildPackage(&types.Config{Importer: importer.Default()}, fset, types.NewPackage("test/main", ""), []*ast.File{file}, 0)
qt.Assert(t, qt.IsNil(err))
_, newFile, _, err := Obfuscate(fset, ssaPkg, []*ast.File{file}, mathrand.New(mathrand.NewSource(seed)))
qt.Assert(t, qt.IsNil(err))
var buf bytes.Buffer
qt.Assert(t, qt.IsNil(printer.Fprint(&buf, fset, newFile)))
return buf.String()
}

first := run()
second := run()
qt.Assert(t, qt.Equals(second, first))
}
6 changes: 6 additions & 0 deletions internal/ssa2ast/func.go
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,12 @@ func (fc *funcConverter) convertToStmts(ssaFunc *ssa.Function) ([]ast.Stmt, erro
}
specs = append(specs, spec)
}
// groupedVar is a map, so its iteration order is random. Sort the grouped
// specs by their first (sorted, globally unique) name to keep the emitted
// var block deterministic, which reproducible builds depend on.
sort.Slice(specs, func(i, j int) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use slices.Sort consistently

return specs[i].(*ast.ValueSpec).Names[0].Name < specs[j].(*ast.ValueSpec).Names[0].Name
})
if len(specs) > 0 {
stmts = append(stmts, &ast.DeclStmt{Decl: &ast.GenDecl{
Tok: token.VAR,
Expand Down
Loading