mirror of
https://github.com/burrowers/garble.git
synced 2025-10-05 07:46:59 +08:00

Changes literal obfuscation such that literals of any size will be obfuscated, but beyond `maxSize` we only use the `simple` obfuscator. This one seems to apply AND, OR, or XOR operators byte-wise and should be safe to use, unlike some of the other obfuscators which are quadratic on the literal size or worse. The test for literals is changed a bit to verify that obfuscation is applied. The code written to the `extra_literals.go` file by the test helper now ensures that Go does not optimize the literals away when we build the binary. We also append a unique string to all literals so that we can test that an unobfuscated build contains this string while an obfuscated build does not.
491 lines
12 KiB
Go
491 lines
12 KiB
Go
// Copyright (c) 2019, The Garble Authors.
|
|
// See LICENSE for licensing information.
|
|
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/printer"
|
|
"go/token"
|
|
"io/fs"
|
|
mathrand "math/rand"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/rogpeppe/go-internal/goproxytest"
|
|
"github.com/rogpeppe/go-internal/gotooltest"
|
|
"github.com/rogpeppe/go-internal/testscript"
|
|
|
|
ah "mvdan.cc/garble/internal/asthelper"
|
|
)
|
|
|
|
var proxyURL string
|
|
|
|
func TestMain(m *testing.M) {
|
|
if os.Getenv("RUN_GARBLE_MAIN") == "true" {
|
|
os.Exit(main1())
|
|
}
|
|
os.Exit(testscript.RunMain(garbleMain{m}, map[string]func() int{
|
|
"garble": main1,
|
|
}))
|
|
}
|
|
|
|
type garbleMain struct {
|
|
m *testing.M
|
|
}
|
|
|
|
func (m garbleMain) Run() int {
|
|
// Start the Go proxy server running for all tests.
|
|
srv, err := goproxytest.NewServer("testdata/mod", "")
|
|
if err != nil {
|
|
panic(fmt.Sprintf("cannot start proxy: %v", err))
|
|
}
|
|
proxyURL = srv.URL
|
|
|
|
return m.m.Run()
|
|
}
|
|
|
|
var update = flag.Bool("u", false, "update testscript output files")
|
|
|
|
func TestScript(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tempCacheDir := t.TempDir()
|
|
|
|
hostCacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
p := testscript.Params{
|
|
Dir: filepath.Join("testdata", "script"),
|
|
Setup: func(env *testscript.Env) error {
|
|
// Use testdata/mod as our module proxy.
|
|
env.Setenv("GOPROXY", proxyURL)
|
|
|
|
// We use our own proxy, so avoid sum.golang.org.
|
|
env.Setenv("GONOSUMDB", "*")
|
|
|
|
// "go build" starts many short-lived Go processes,
|
|
// such as asm, buildid, compile, and link.
|
|
// They don't allocate huge amounts of memory,
|
|
// and they'll exit within seconds,
|
|
// so using the GC is basically a waste of CPU.
|
|
// Turn it off entirely, releasing memory on exit.
|
|
//
|
|
// We don't want this setting always on,
|
|
// as it could result in memory problems for users.
|
|
// But it helps for our test suite,
|
|
// as the packages are relatively small.
|
|
env.Setenv("GOGC", "off")
|
|
|
|
env.Setenv("gofullversion", runtime.Version())
|
|
env.Setenv("EXEC_PATH", execPath)
|
|
|
|
if os.Getenv("GOCOVERDIR") != "" {
|
|
// Don't share cache dirs with the host if we want to collect code
|
|
// coverage. Otherwise, the coverage info might be incomplete.
|
|
env.Setenv("GOCACHE", filepath.Join(tempCacheDir, "go-cache"))
|
|
env.Setenv("GARBLE_CACHE_DIR", filepath.Join(tempCacheDir, "garble-cache"))
|
|
} else {
|
|
// GOCACHE is initialized by gotooltest to use the host's cache.
|
|
env.Setenv("GARBLE_CACHE_DIR", hostCacheDir)
|
|
}
|
|
return nil
|
|
},
|
|
// TODO: this condition should probably be supported by gotooltest
|
|
Condition: func(cond string) (bool, error) {
|
|
switch cond {
|
|
case "cgo":
|
|
out, err := exec.Command("go", "env", "CGO_ENABLED").CombinedOutput()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
result := strings.TrimSpace(string(out))
|
|
switch result {
|
|
case "0", "1":
|
|
return result == "1", nil
|
|
default:
|
|
return false, fmt.Errorf("unknown CGO_ENABLED: %q", result)
|
|
}
|
|
}
|
|
return false, fmt.Errorf("unknown condition")
|
|
},
|
|
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
|
|
"sleep": sleep,
|
|
"binsubstr": binsubstr,
|
|
"bincmp": bincmp,
|
|
"generate-literals": generateLiterals,
|
|
"setenvfile": setenvfile,
|
|
"grepfiles": grepfiles,
|
|
},
|
|
UpdateScripts: *update,
|
|
}
|
|
if err := gotooltest.Setup(&p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testscript.Run(t, p)
|
|
}
|
|
|
|
func createFile(ts *testscript.TestScript, path string) *os.File {
|
|
file, err := os.Create(ts.MkAbs(path))
|
|
if err != nil {
|
|
ts.Fatalf("%v", err)
|
|
}
|
|
return file
|
|
}
|
|
|
|
// sleep is akin to a shell's sleep builtin.
|
|
// Note that tests should almost never use this; it's currently only used to
|
|
// work around a low-level Go syscall race on Linux.
|
|
func sleep(ts *testscript.TestScript, neg bool, args []string) {
|
|
if len(args) != 1 {
|
|
ts.Fatalf("usage: sleep duration")
|
|
}
|
|
d, err := time.ParseDuration(args[0])
|
|
if err != nil {
|
|
ts.Fatalf("%v", err)
|
|
}
|
|
time.Sleep(d)
|
|
}
|
|
|
|
func binsubstr(ts *testscript.TestScript, neg bool, args []string) {
|
|
if len(args) < 2 {
|
|
ts.Fatalf("usage: binsubstr file substr...")
|
|
}
|
|
data := ts.ReadFile(args[0])
|
|
var failed []string
|
|
for _, substr := range args[1:] {
|
|
match := strings.Contains(data, substr)
|
|
if match && neg {
|
|
failed = append(failed, substr)
|
|
} else if !match && !neg {
|
|
failed = append(failed, substr)
|
|
}
|
|
}
|
|
if len(failed) > 0 && neg {
|
|
ts.Fatalf("unexpected match for %q in %s", failed, args[0])
|
|
} else if len(failed) > 0 {
|
|
ts.Fatalf("expected match for %q in %s", failed, args[0])
|
|
}
|
|
}
|
|
|
|
func bincmp(ts *testscript.TestScript, neg bool, args []string) {
|
|
if len(args) != 2 {
|
|
ts.Fatalf("usage: bincmp file1 file2")
|
|
}
|
|
for _, arg := range args {
|
|
switch arg {
|
|
case "stdout", "stderr":
|
|
// Note that the diffoscope call below would not deal with
|
|
// stdout/stderr either.
|
|
ts.Fatalf("bincmp is for binary files. did you mean cmp?")
|
|
}
|
|
}
|
|
data1 := ts.ReadFile(args[0])
|
|
data2 := ts.ReadFile(args[1])
|
|
if neg {
|
|
if data1 == data2 {
|
|
ts.Fatalf("%s and %s don't differ", args[0], args[1])
|
|
}
|
|
return
|
|
}
|
|
if data1 != data2 {
|
|
if _, err := exec.LookPath("diffoscope"); err == nil {
|
|
// We'll error below; ignore the exec error here.
|
|
ts.Exec("diffoscope",
|
|
"--diff-context", "2", // down from 7 by default
|
|
"--max-text-report-size", "4096", // no limit (in bytes) by default; avoid huge output
|
|
ts.MkAbs(args[0]), ts.MkAbs(args[1]))
|
|
} else {
|
|
ts.Logf("diffoscope not found; skipping")
|
|
}
|
|
outDir := "bincmp_output"
|
|
err := os.MkdirAll(outDir, 0o777)
|
|
ts.Check(err)
|
|
|
|
file1, err := os.CreateTemp(outDir, "file1-*")
|
|
ts.Check(err)
|
|
_, err = file1.Write([]byte(data1))
|
|
ts.Check(err)
|
|
err = file1.Close()
|
|
ts.Check(err)
|
|
|
|
file2, err := os.CreateTemp(outDir, "file2-*")
|
|
ts.Check(err)
|
|
_, err = file2.Write([]byte(data2))
|
|
ts.Check(err)
|
|
err = file2.Close()
|
|
ts.Check(err)
|
|
|
|
ts.Logf("wrote files to %s and %s", file1.Name(), file2.Name())
|
|
sizeDiff := len(data2) - len(data1)
|
|
ts.Fatalf("%s and %s differ; diffoscope above, size diff: %+d",
|
|
args[0], args[1], sizeDiff)
|
|
}
|
|
}
|
|
|
|
var testRand = mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
|
|
|
|
func generateStringLit(minSize int) *ast.BasicLit {
|
|
buffer := make([]byte, minSize)
|
|
_, err := testRand.Read(buffer)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return ah.StringLit(string(buffer) + "a_unique_string_that_is_part_of_all_extra_literals")
|
|
}
|
|
|
|
// generateLiterals creates a new source code file with a few random literals inside.
|
|
// All literals contain the string "a_unique_string_that_is_part_of_all_extra_literals"
|
|
// so we can later check if they are all obfuscated by looking for this substring.
|
|
// The code is designed such that the Go compiler does not optimize away the literals,
|
|
// which would destroy the test.
|
|
// This is achieved by defining a global variable `var x = ""` and an `init` function
|
|
// which appends all literals to `x`.
|
|
func generateLiterals(ts *testscript.TestScript, neg bool, args []string) {
|
|
if neg {
|
|
ts.Fatalf("unsupported: ! generate-literals")
|
|
}
|
|
if len(args) != 1 {
|
|
ts.Fatalf("usage: generate-literals file")
|
|
}
|
|
|
|
codePath := args[0]
|
|
|
|
// Global string variable to which which we append string literals: `var x = ""`
|
|
globalVar := &ast.GenDecl{
|
|
Tok: token.VAR,
|
|
Specs: []ast.Spec{
|
|
&ast.ValueSpec{
|
|
Names: []*ast.Ident{ast.NewIdent("x")},
|
|
Values: []ast.Expr{
|
|
&ast.BasicLit{Kind: token.STRING, Value: `""`},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
var statements []ast.Stmt
|
|
|
|
// Assignments which append 100 random small literals to x: `x += "the_small_random_literal"`
|
|
for i := 0; i < 100; i++ {
|
|
statements = append(
|
|
statements,
|
|
&ast.AssignStmt{
|
|
Lhs: []ast.Expr{ast.NewIdent("x")},
|
|
Tok: token.ADD_ASSIGN,
|
|
Rhs: []ast.Expr{generateStringLit(1 + testRand.Intn(255))},
|
|
},
|
|
)
|
|
}
|
|
|
|
// Assignments which append 5 random huge literals to x: `x += "the_huge_random_literal"`
|
|
// We add huge literals to make sure we obfuscate them fast.
|
|
// 5 * 128KiB is large enough that it would take a very, very long time
|
|
// to obfuscate those literals if too complex obfuscators are used.
|
|
for i := 0; i < 5; i++ {
|
|
statements = append(
|
|
statements,
|
|
&ast.AssignStmt{
|
|
Lhs: []ast.Expr{ast.NewIdent("x")},
|
|
Tok: token.ADD_ASSIGN,
|
|
Rhs: []ast.Expr{generateStringLit(128 << 10)},
|
|
},
|
|
)
|
|
}
|
|
|
|
// An `init` function which includes all assignments from above
|
|
initFunc := &ast.FuncDecl{
|
|
Name: &ast.Ident{
|
|
Name: "init",
|
|
},
|
|
Type: &ast.FuncType{},
|
|
Body: ah.BlockStmt(statements...),
|
|
}
|
|
|
|
// A file with the global string variable and init function
|
|
file := &ast.File{
|
|
Name: ast.NewIdent("main"),
|
|
Decls: []ast.Decl{
|
|
globalVar,
|
|
initFunc,
|
|
},
|
|
}
|
|
|
|
codeFile := createFile(ts, codePath)
|
|
defer codeFile.Close()
|
|
|
|
if err := printer.Fprint(codeFile, token.NewFileSet(), file); err != nil {
|
|
ts.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func setenvfile(ts *testscript.TestScript, neg bool, args []string) {
|
|
if neg {
|
|
ts.Fatalf("unsupported: ! setenvfile")
|
|
}
|
|
if len(args) != 2 {
|
|
ts.Fatalf("usage: setenvfile name file")
|
|
}
|
|
|
|
ts.Setenv(args[0], ts.ReadFile(args[1]))
|
|
}
|
|
|
|
func grepfiles(ts *testscript.TestScript, neg bool, args []string) {
|
|
if len(args) != 2 {
|
|
ts.Fatalf("usage: grepfiles path pattern")
|
|
}
|
|
anyFound := false
|
|
path, pattern := args[0], args[1]
|
|
rx := regexp.MustCompile(pattern)
|
|
if err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rx.MatchString(path) {
|
|
if neg {
|
|
return fmt.Errorf("%q matches %q", path, pattern)
|
|
} else {
|
|
anyFound = true
|
|
return fs.SkipAll
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
ts.Fatalf("%s", err)
|
|
}
|
|
if !neg && !anyFound {
|
|
ts.Fatalf("no matches for %q", pattern)
|
|
}
|
|
}
|
|
|
|
func TestSplitFlagsFromArgs(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
want [2][]string
|
|
}{
|
|
{"Empty", []string{}, [2][]string{{}, nil}},
|
|
{
|
|
"JustFlags",
|
|
[]string{"-foo", "bar", "-baz"},
|
|
[2][]string{{"-foo", "bar", "-baz"}, nil},
|
|
},
|
|
{
|
|
"JustArgs",
|
|
[]string{"some", "pkgs"},
|
|
[2][]string{{}, {"some", "pkgs"}},
|
|
},
|
|
{
|
|
"FlagsAndArgs",
|
|
[]string{"-foo=bar", "baz"},
|
|
[2][]string{{"-foo=bar"}, {"baz"}},
|
|
},
|
|
{
|
|
"BoolFlagsAndArgs",
|
|
[]string{"-race", "pkg"},
|
|
[2][]string{{"-race"}, {"pkg"}},
|
|
},
|
|
{
|
|
"ExplicitBoolFlag",
|
|
[]string{"-race=true", "pkg"},
|
|
[2][]string{{"-race=true"}, {"pkg"}},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
flags, args := splitFlagsFromArgs(test.args)
|
|
got := [2][]string{flags, args}
|
|
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Fatalf("splitFlagsFromArgs(%q) mismatch (-want +got):\n%s", test.args, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterForwardBuildFlags(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
flags []string
|
|
want []string
|
|
}{
|
|
{"Empty", []string{}, nil},
|
|
{
|
|
"NoBuild",
|
|
[]string{"-short", "-json"},
|
|
nil,
|
|
},
|
|
{
|
|
"Mixed",
|
|
[]string{"-short", "-tags", "foo", "-mod=readonly", "-json"},
|
|
[]string{"-tags", "foo", "-mod=readonly"},
|
|
},
|
|
{
|
|
"NonBinarySkipped",
|
|
[]string{"-o", "binary", "-tags", "foo"},
|
|
[]string{"-tags", "foo"},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, _ := filterForwardBuildFlags(test.flags)
|
|
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Fatalf("filterForwardBuildFlags(%q) mismatch (-want +got):\n%s", test.flags, diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFlagValue(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
flags []string
|
|
flagName string
|
|
want string
|
|
}{
|
|
{"StrSpace", []string{"-buildid", "bar"}, "-buildid", "bar"},
|
|
{"StrSpaceDash", []string{"-buildid", "-bar"}, "-buildid", "-bar"},
|
|
{"StrEqual", []string{"-buildid=bar"}, "-buildid", "bar"},
|
|
{"StrEqualDash", []string{"-buildid=-bar"}, "-buildid", "-bar"},
|
|
{"StrMissing", []string{"-foo"}, "-buildid", ""},
|
|
{"StrNotFollowed", []string{"-buildid"}, "-buildid", ""},
|
|
{"StrEmpty", []string{"-buildid="}, "-buildid", ""},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := flagValue(test.flags, test.flagName)
|
|
if got != test.want {
|
|
t.Fatalf("flagValue(%q, %q) got %q, want %q",
|
|
test.flags, test.flagName, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|