mirror of
https://github.com/burrowers/garble.git
synced 2026-04-22 15:47:04 +08:00
ef2385ee97
The added benchmark script shows these numbers for building and running
with vanilla go on literal sizes between 16B and 2048B,
showing that vanilla Go isn't affected at all by these sizes:
│ go │
│ sec/op │
Build/16B 118.1m ± ∞ ¹
Build/64B 123.1m ± ∞ ¹
Build/256B 119.7m ± ∞ ¹
Build/1024B 119.1m ± ∞ ¹
Build/2048B 124.3m ± ∞ ¹
Run/16B 1.671m ± ∞ ¹
Run/64B 1.143m ± ∞ ¹
Run/256B 1.190m ± ∞ ¹
Run/1024B 1.222m ± ∞ ¹
Run/2048B 1.080m ± ∞ ¹
Our simple and swap obfuscators scale pretty well to these same sizes,
only causing moderate slow-downs to build and runtime speeds:
│ simple │ swap │
│ sec/op vs base │ sec/op vs base │
Build/16B 268.0m ± ∞ ¹ +126.88% (p=1.000 n=1) 262.0m ± ∞ ¹ +121.80% (p=1.000 n=1)
Build/64B 253.8m ± ∞ ¹ +106.16% (p=1.000 n=1) 252.4m ± ∞ ¹ +105.00% (p=1.000 n=1)
Build/256B 265.4m ± ∞ ¹ +121.78% (p=1.000 n=1) 276.7m ± ∞ ¹ +131.16% (p=1.000 n=1)
Build/1024B 267.4m ± ∞ ¹ +124.44% (p=1.000 n=1) 315.0m ± ∞ ¹ +164.48% (p=1.000 n=1)
Build/2048B 277.4m ± ∞ ¹ +123.11% (p=1.000 n=1) 383.8m ± ∞ ¹ +208.70% (p=1.000 n=1)
Run/16B 1.740m ± ∞ ¹ +4.12% (p=1.000 n=1) 1.463m ± ∞ ¹ -12.47% (p=1.000 n=1)
Run/64B 1.470m ± ∞ ¹ +28.66% (p=1.000 n=1) 1.455m ± ∞ ¹ +27.35% (p=1.000 n=1)
Run/256B 1.729m ± ∞ ¹ +45.25% (p=1.000 n=1) 1.812m ± ∞ ¹ +52.26% (p=1.000 n=1)
Run/1024B 1.315m ± ∞ ¹ +7.62% (p=1.000 n=1) 1.352m ± ∞ ¹ +10.60% (p=1.000 n=1)
Run/2048B 1.425m ± ∞ ¹ +31.93% (p=1.000 n=1) 1.316m ± ∞ ¹ +21.88% (p=1.000 n=1)
However, the other three cause huge slow-downs in both build and runtime speeds:
│ split │ shuffle │ seed │
│ sec/op vs base │ sec/op vs base │ sec/op vs base │
Build/16B 326.6m ± ∞ ¹ +176.53% (p=1.000 n=1) 363.1m ± ∞ ¹ +207.44% (p=1.000 n=1) 217.4m ± ∞ ¹ +84.05% (p=1.000 n=1)
Build/64B 400.6m ± ∞ ¹ +225.34% (p=1.000 n=1) 328.0m ± ∞ ¹ +166.39% (p=1.000 n=1) 262.1m ± ∞ ¹ +112.87% (p=1.000 n=1)
Build/256B 824.7m ± ∞ ¹ +589.08% (p=1.000 n=1) 588.2m ± ∞ ¹ +391.45% (p=1.000 n=1) 873.7m ± ∞ ¹ +630.05% (p=1.000 n=1)
Build/1024B 3257.7m ± ∞ ¹ +2634.84% (p=1.000 n=1) 1671.4m ± ∞ ¹ +1303.15% (p=1.000 n=1) 5000.0m ± ∞ ¹ +4097.53% (p=1.000 n=1)
Build/2048B 5000.0m ± ∞ ¹ +3921.73% (p=1.000 n=1) 3936.4m ± ∞ ¹ +3066.21% (p=1.000 n=1) 5000.0m ± ∞ ¹ +3921.73% (p=1.000 n=1)
Run/16B 1.680m ± ∞ ¹ +0.51% (p=1.000 n=1) 1.426m ± ∞ ¹ -14.67% (p=1.000 n=1) 1.908m ± ∞ ¹ +14.13% (p=1.000 n=1)
Run/64B 1.560m ± ∞ ¹ +36.53% (p=1.000 n=1) 1.345m ± ∞ ¹ +17.74% (p=1.000 n=1) 1.704m ± ∞ ¹ +49.10% (p=1.000 n=1)
Run/256B 2.133m ± ∞ ¹ +79.24% (p=1.000 n=1) 1.833m ± ∞ ¹ +53.98% (p=1.000 n=1) 1.838m ± ∞ ¹ +54.40% (p=1.000 n=1)
Run/1024B 2.863m ± ∞ ¹ +134.21% (p=1.000 n=1) 1.786m ± ∞ ¹ +46.12% (p=1.000 n=1) 5000.000m ± ∞ ¹ +408975.92% (p=1.000 n=1)
Run/2048B 5000.000m ± ∞ ¹ +462837.24% (p=1.000 n=1) 2.900m ± ∞ ¹ +168.54% (p=1.000 n=1) 5000.000m ± ∞ ¹ +462837.24% (p=1.000 n=1)
As such, limit the scope of when we apply our obfuscators in two ways.
First, always apply a limit of 2048 bytes for all obfuscators.
As we can see above, the two cheap obfuscators still add some overhead,
and we aren't testing what happens if they run on truly huge strings.
It's likely that they will still cause unexpected slow-down.
Second, split the obfuscators along this line and call them
"cheap" versus "expensive". The expensive ones are only used
for sizes of up to 256 bytes. We still measure moderate slow-downs
in build times of 400-600%, but this is a reasonable compromise for now.
We will be filing bugs with upstream Go about the compiler overhead.
We adjust generateLiterals accordingly;
it now generates 100 literals between MinSize and MaxSize,
which must be obfuscated in some way, and 5 literals past MaxSize,
which don't need to be obfuscated, to check for issues like crashes.
Fixes #928.
282 lines
7.5 KiB
Go
282 lines
7.5 KiB
Go
// Copyright (c) 2020, The Garble Authors.
|
|
// See LICENSE for licensing information.
|
|
|
|
package literals
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/token"
|
|
"math"
|
|
mathrand "math/rand"
|
|
"slices"
|
|
"strconv"
|
|
|
|
ah "mvdan.cc/garble/internal/asthelper"
|
|
)
|
|
|
|
// externalKeyProbability probability of using an external key.
|
|
// Larger value, greater probability of using an external key.
|
|
// Must be between 0 and 1
|
|
type externalKeyProbability float32
|
|
|
|
const (
|
|
lowProb externalKeyProbability = 0.4
|
|
normalProb externalKeyProbability = 0.6
|
|
highProb externalKeyProbability = 0.8
|
|
)
|
|
|
|
func (r externalKeyProbability) Try(rand *mathrand.Rand) bool {
|
|
return rand.Float32() < float32(r)
|
|
}
|
|
|
|
// externalKey contains all information about the external key
|
|
type externalKey struct {
|
|
name, typ string
|
|
value uint64
|
|
bits int
|
|
refs int
|
|
}
|
|
|
|
func (k *externalKey) Type() *ast.Ident {
|
|
return ast.NewIdent(k.typ)
|
|
}
|
|
|
|
func (k *externalKey) Name() *ast.Ident {
|
|
return ast.NewIdent(k.name)
|
|
}
|
|
|
|
func (k *externalKey) AddRef() {
|
|
k.refs++
|
|
}
|
|
|
|
func (k *externalKey) IsUsed() bool {
|
|
return k.refs > 0
|
|
}
|
|
|
|
// obfuscator takes a byte slice and converts it to a ast.BlockStmt
|
|
type obfuscator interface {
|
|
obfuscate(obfRand *mathrand.Rand, data []byte, extKeys []*externalKey) *ast.BlockStmt
|
|
}
|
|
|
|
var (
|
|
// Obfuscators contains all types which implement the obfuscator Interface.
|
|
Obfuscators = []obfuscator{
|
|
simple{},
|
|
swap{},
|
|
split{},
|
|
shuffle{},
|
|
seed{},
|
|
}
|
|
|
|
// CheapObfuscators contains obfuscators safe to use on large literals.
|
|
// The expensive obfuscators scale poorly, so they are excluded here.
|
|
CheapObfuscators = []obfuscator{
|
|
simple{},
|
|
swap{},
|
|
}
|
|
|
|
TestObfuscator string
|
|
testPkgToObfuscatorMap map[string]obfuscator
|
|
)
|
|
|
|
func genRandIntSlice(obfRand *mathrand.Rand, max, count int) []int {
|
|
indexes := make([]int, count)
|
|
for i := range count {
|
|
indexes[i] = obfRand.Intn(max)
|
|
}
|
|
return indexes
|
|
}
|
|
|
|
func randOperator(obfRand *mathrand.Rand) token.Token {
|
|
operatorTokens := [...]token.Token{token.XOR, token.ADD, token.SUB}
|
|
return operatorTokens[obfRand.Intn(len(operatorTokens))]
|
|
}
|
|
|
|
func evalOperator(t token.Token, x, y byte) byte {
|
|
switch t {
|
|
case token.XOR:
|
|
return x ^ y
|
|
case token.ADD:
|
|
return x + y
|
|
case token.SUB:
|
|
return x - y
|
|
default:
|
|
panic(fmt.Sprintf("unknown operator: %s", t))
|
|
}
|
|
}
|
|
|
|
func operatorToReversedBinaryExpr(t token.Token, x, y ast.Expr) *ast.BinaryExpr {
|
|
var op token.Token
|
|
switch t {
|
|
case token.XOR:
|
|
op = token.XOR // XOR is self-inverse: (a ^ b) ^ b = a
|
|
case token.ADD:
|
|
op = token.SUB
|
|
case token.SUB:
|
|
op = token.ADD
|
|
default:
|
|
panic(fmt.Sprintf("unknown operator: %s", t))
|
|
}
|
|
return ah.BinaryExpr(x, op, y)
|
|
}
|
|
|
|
const (
|
|
// minExtKeyCount is minimum number of external keys for one lambda call
|
|
minExtKeyCount = 2
|
|
// maxExtKeyCount is maximum number of external keys for one lambda call
|
|
maxExtKeyCount = 6
|
|
|
|
// minByteSliceExtKeyOps minimum number of operations with external keys for one byte slice
|
|
minByteSliceExtKeyOps = 2
|
|
// maxByteSliceExtKeyOps maximum number of operations with external keys for one byte slice
|
|
maxByteSliceExtKeyOps = 12
|
|
)
|
|
|
|
// extKeyRanges contains a list of different ranges of random numbers for external keys
|
|
// Different types and bitnesses will increase the chance of changing patterns
|
|
var extKeyRanges = []struct {
|
|
typ string
|
|
max uint64
|
|
bits int
|
|
}{
|
|
{"uint8", math.MaxUint8, 8},
|
|
{"uint16", math.MaxUint16, 16},
|
|
{"uint32", math.MaxUint32, 32},
|
|
{"uint64", math.MaxUint64, 64},
|
|
}
|
|
|
|
// randExtKey generates a random external key with a unique name, type, value, and bitnesses
|
|
func randExtKey(rand *mathrand.Rand, idx int) *externalKey {
|
|
r := extKeyRanges[rand.Intn(len(extKeyRanges))]
|
|
return &externalKey{
|
|
name: "garbleExternalKey" + strconv.Itoa(idx),
|
|
typ: r.typ,
|
|
value: rand.Uint64() & r.max,
|
|
bits: r.bits,
|
|
}
|
|
}
|
|
|
|
func randExtKeys(rand *mathrand.Rand) []*externalKey {
|
|
count := minExtKeyCount + rand.Intn(maxExtKeyCount-minExtKeyCount)
|
|
keys := make([]*externalKey, count)
|
|
for i := range count {
|
|
keys[i] = randExtKey(rand, i)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// extKeysToParams converts a list of extKeys into a parameter list and argument expressions for function calls.
|
|
// It ensures unused keys have placeholder names and sometimes use proxyDispatcher.HideValue for key values
|
|
func extKeysToParams(objRand *obfRand, keys []*externalKey) (params *ast.FieldList, args []ast.Expr) {
|
|
params = &ast.FieldList{}
|
|
for _, key := range keys {
|
|
name := key.Name()
|
|
if !key.IsUsed() {
|
|
name.Name = "_"
|
|
}
|
|
params.List = append(params.List, ah.Field(key.Type(), name))
|
|
|
|
var extKeyExpr ast.Expr = ah.UintLit(key.value)
|
|
if lowProb.Try(objRand.Rand) {
|
|
extKeyExpr = objRand.proxyDispatcher.HideValue(extKeyExpr, ast.NewIdent(key.typ))
|
|
}
|
|
args = append(args, extKeyExpr)
|
|
}
|
|
return
|
|
}
|
|
|
|
// extKeyToExpr converts an external key into an AST expression like:
|
|
//
|
|
// uint8(key >> b)
|
|
func (key *externalKey) ToExpr(b int) ast.Expr {
|
|
var x ast.Expr = key.Name()
|
|
if b > 0 {
|
|
x = ah.BinaryExpr(x, token.SHR, ah.IntLit(b*8))
|
|
}
|
|
if key.typ != "uint8" {
|
|
x = ah.CallExprByName("byte", x)
|
|
}
|
|
return x
|
|
}
|
|
|
|
// dataToByteSliceWithExtKeys scramble and turn a byte slice into an AST expression like:
|
|
//
|
|
// func() []byte {
|
|
// data := []byte("<data>")
|
|
// data[<index>] = data[<index>] <random operator> byte(<external key> >> <random shift>) // repeated random times
|
|
// return data
|
|
// }()
|
|
func dataToByteSliceWithExtKeys(rand *mathrand.Rand, data []byte, extKeys []*externalKey) ast.Expr {
|
|
extKeyOpCount := minByteSliceExtKeyOps + rand.Intn(maxByteSliceExtKeyOps-minByteSliceExtKeyOps)
|
|
|
|
var stmts []ast.Stmt
|
|
for range extKeyOpCount {
|
|
key := extKeys[rand.Intn(len(extKeys))]
|
|
key.AddRef()
|
|
|
|
idx, op, b := rand.Intn(len(data)), randOperator(rand), rand.Intn(key.bits/8)
|
|
data[idx] = evalOperator(op, data[idx], byte(key.value>>(b*8)))
|
|
stmts = append(stmts, ah.AssignStmt(
|
|
ah.IndexExpr("data", ah.IntLit(idx)),
|
|
operatorToReversedBinaryExpr(op,
|
|
ah.IndexExpr("data", ah.IntLit(idx)),
|
|
key.ToExpr(b),
|
|
),
|
|
))
|
|
}
|
|
|
|
// External keys can be applied several times to the same array element,
|
|
// and it is important to invert the order of execution to correctly restore the original value
|
|
slices.Reverse(stmts)
|
|
|
|
stmts = append([]ast.Stmt{ah.AssignDefineStmt(ast.NewIdent("data"), ah.DataToByteSlice(data))}, append(stmts, ah.ReturnStmt(ast.NewIdent("data")))...)
|
|
return ah.LambdaCall(nil, ah.ByteSliceType(), ah.BlockStmt(stmts...), nil)
|
|
}
|
|
|
|
// byteLitWithExtKey scrambles a byte value into an AST expression like:
|
|
//
|
|
// byte(<obfuscated value>) <random operator> byte(<external key> >> <random shift>)
|
|
func byteLitWithExtKey(rand *mathrand.Rand, val byte, extKeys []*externalKey, extKeyProb externalKeyProbability) ast.Expr {
|
|
if !extKeyProb.Try(rand) {
|
|
return ah.IntLit(int(val))
|
|
}
|
|
|
|
key := extKeys[rand.Intn(len(extKeys))]
|
|
key.AddRef()
|
|
|
|
op, b := randOperator(rand), rand.Intn(key.bits/8)
|
|
newVal := evalOperator(op, val, byte(key.value>>(b*8)))
|
|
|
|
return operatorToReversedBinaryExpr(op,
|
|
ah.CallExprByName("byte", ah.IntLit(int(newVal))),
|
|
key.ToExpr(b),
|
|
)
|
|
}
|
|
|
|
type obfRand struct {
|
|
*mathrand.Rand
|
|
testObfuscator obfuscator
|
|
|
|
proxyDispatcher *proxyDispatcher
|
|
}
|
|
|
|
func (r *obfRand) nextObfuscator() obfuscator {
|
|
if r.testObfuscator != nil {
|
|
return r.testObfuscator
|
|
}
|
|
return Obfuscators[r.Intn(len(Obfuscators))]
|
|
}
|
|
|
|
func (r *obfRand) nextCheapObfuscator() obfuscator {
|
|
if r.testObfuscator != nil {
|
|
return r.testObfuscator
|
|
}
|
|
return CheapObfuscators[r.Intn(len(CheapObfuscators))]
|
|
}
|
|
|
|
func newObfRand(rand *mathrand.Rand, file *ast.File, nameFunc NameProviderFunc) *obfRand {
|
|
testObf := testPkgToObfuscatorMap[file.Name.Name]
|
|
return &obfRand{rand, testObf, newProxyDispatcher(rand, nameFunc)}
|
|
}
|