mirror of
https://github.com/lukaszraczylo/go-obfuscation.git
synced 2026-10-12 05:32:33 +00:00
Bug fixes: - pclntab/rtcorrupt_linux.go: remove duplicate mprotectRW/mprotectRO that broke GOOS=linux builds (they're defined in rtcorrupt_unsafe.go for darwin+linux). - pclntab/pclntab.go: readUint now guards against negative offset (previously panicked via slice out-of-range). - integrity/integrity.go: SetExpectedHashHex now validates length 64 up-front and reports byte index on parse error; added ClearExpectedHash for runtime toggling and test isolation. - transform/mba.go: mbaApplyBinOp used strings.Index on first match instead of the current match; replaced with FindAllStringSubmatchIndex + two-pass replacement. - transform/transform.go: removed duplicate unconditional SplitLongStrings call. New transform passes (CLI flags + Config fields + pipeline wiring): - splice, constblind, mba, fakesigs, funcsplit, bbreorder — all default true. Cleanup: - strenc/keyderive.go: removed unused DeriveFromBinary + buildSeed. - transform/transform.go: removed unused trimFirstParen. - antidebug_hook_darwin.go: modernized readAddr to unsafe.Slice with noescape pattern (vet-safe). Tests (10 packages, 116 tests, all green): - internal/transform: 28 tests covering every pipeline stage. - pkg/strenc: 7 tests for key derivation + XOR stream. - pkg/stringcrypt: 7 tests for CFB round-trip + tamper detection. - pkg/pageman: 14 tests for alignment + mprotect guards. - pkg/integrity: 14 tests including ClearExpectedHash teardown. - pkg/pclntab: 12 tests including readUint negative-offset guard. - pkg/antisandbox: 6 tests for hostname/MAC lists + Check delegation. - pkg/antiemul: 6 tests (1 skipped on non-Linux) for no-panic + timing. Tooling: - Makefile: added test, verify, lint, fmt, vet targets. vet uses -unsafeptr=false (only legitimate uintptr→pointer is the darwin antidebug hook). fmt skips build/ (generated). - README.md: test coverage table + Makefile target reference.
497 lines
12 KiB
Go
497 lines
12 KiB
Go
package transform
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestIsValidIdent(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"": false,
|
|
"x": true,
|
|
"_": true,
|
|
"_x": true,
|
|
"abc123": true,
|
|
"123abc": false,
|
|
"a-b": false,
|
|
"a b": false,
|
|
"a.b": false,
|
|
"a$": false,
|
|
"foo_bar": true,
|
|
}
|
|
for in, want := range cases {
|
|
if got := isValidIdent(in); got != want {
|
|
t.Errorf("isValidIdent(%q) = %v, want %v", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnquoteGoString(t *testing.T) {
|
|
cases := []struct {
|
|
in, want string
|
|
wantErr bool
|
|
}{
|
|
{`"hello"`, "hello", false},
|
|
{`"with\nnewline"`, "with\nnewline", false},
|
|
{`"tab\there"`, "tab\there", false},
|
|
{`"quote\"inside"`, `quote"inside`, false},
|
|
{`"backslash\\"`, `backslash\`, false},
|
|
{`""`, "", false},
|
|
{`"`, "", true},
|
|
{`no quotes`, "", true},
|
|
}
|
|
for _, c := range cases {
|
|
got, err := unquoteGoString(c.in)
|
|
if (err != nil) != c.wantErr {
|
|
t.Errorf("unquoteGoString(%q) err=%v, wantErr=%v", c.in, err, c.wantErr)
|
|
continue
|
|
}
|
|
if !c.wantErr && got != c.want {
|
|
t.Errorf("unquoteGoString(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShouldSkipString(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"", true},
|
|
{"go:noinline", true},
|
|
{"// comment", true},
|
|
{"+build linux", true},
|
|
{"package main", true},
|
|
{"normal string", false},
|
|
{"/starts with slash", true},
|
|
{"+starts with plus", true},
|
|
}
|
|
for _, c := range cases {
|
|
if got := shouldSkipString(c.in); got != c.want {
|
|
t.Errorf("shouldSkipString(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInferTypeFromRHS(t *testing.T) {
|
|
cases := []struct {
|
|
in, want string
|
|
}{
|
|
{`"hello"`, "string"},
|
|
{"`raw`", "string"},
|
|
{"fmt.Sprintf(\"%d\", x)", "string"},
|
|
{"[]byte{1,2,3}", "[]byte"},
|
|
{"make([]byte, 10)", "[]byte"},
|
|
{"true", "bool"},
|
|
{"false", "bool"},
|
|
{"sha256.Sum256([]byte(x))", "[32]byte"},
|
|
{"x + 1", ""}, // numeric, no type
|
|
{"42", ""},
|
|
{"someVar", ""},
|
|
}
|
|
for _, c := range cases {
|
|
if got := inferTypeFromRHS(c.in); got != c.want {
|
|
t.Errorf("inferTypeFromRHS(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInferVarType(t *testing.T) {
|
|
cases := []struct {
|
|
in, want string
|
|
}{
|
|
{`"hello"`, "string"},
|
|
{`fmt.Sprintf("x", y)`, "string"},
|
|
{`true`, "bool"},
|
|
{`42`, "int64"},
|
|
{`[3]int{1,2,3}`, ""}, // array — skip
|
|
{`x + 1`, "int64"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := inferVarType(c.in); got != c.want {
|
|
t.Errorf("inferVarType(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindImportBlockEnd(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want int
|
|
}{
|
|
{
|
|
name: "paren import",
|
|
in: "package main\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n",
|
|
want: 36, // index after the closing )
|
|
},
|
|
{
|
|
name: "single import",
|
|
in: "package main\nimport \"fmt\"\n",
|
|
want: 26,
|
|
},
|
|
{
|
|
name: "no imports",
|
|
in: "package main\n",
|
|
want: -1,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got := findImportBlockEnd(c.in)
|
|
if c.want == -1 {
|
|
if got != -1 {
|
|
t.Errorf("findImportBlockEnd(%q) = %d, want -1", c.in, got)
|
|
}
|
|
return
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("findImportBlockEnd(%q) = %d, want %d", c.in, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertConstToVarNoOp(t *testing.T) {
|
|
src := "package main\n\nconst (\n\tfoo = 1\n)\n"
|
|
got := convertConstToVar(src)
|
|
if got != src {
|
|
t.Errorf("convertConstToVar modified non-encrypted const block:\nbefore: %q\nafter: %q", src, got)
|
|
}
|
|
}
|
|
|
|
func TestConvertConstToVarTriggersOnDecStr(t *testing.T) {
|
|
src := "package main\n\nconst (\n\tfoo = _decStr(0)\n)\n"
|
|
got := convertConstToVar(src)
|
|
if !strings.Contains(got, "var (") {
|
|
t.Errorf("convertConstToVar did not promote const to var: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEnsureImportIntoParenBlock(t *testing.T) {
|
|
src := "package x\nimport (\n\t\"fmt\"\n)\n"
|
|
tr := New(Config{})
|
|
got := tr.ensureImport(src, "github.com/example/pkg")
|
|
if !strings.Contains(got, `"github.com/example/pkg"`) {
|
|
t.Errorf("ensureImport missing target:\n%s", got)
|
|
}
|
|
got2 := tr.ensureImport(src, "github.com/example/pkg")
|
|
if strings.Count(got2, "github.com/example/pkg") != 1 {
|
|
t.Errorf("ensureImport added duplicate: %q", got2)
|
|
}
|
|
}
|
|
|
|
func TestEnsureImportCreatesNewBlock(t *testing.T) {
|
|
src := "package x\n\nfunc main() {}\n"
|
|
tr := New(Config{})
|
|
got := tr.ensureImport(src, "github.com/example/pkg")
|
|
if !strings.Contains(got, `import ("github.com/example/pkg")`) {
|
|
t.Errorf("ensureImport did not create import block:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractArgNames(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want []string
|
|
}{
|
|
{"", nil},
|
|
{"x int", []string{"x"}},
|
|
{"x int, y int", []string{"x", "y"}},
|
|
{"x, y int", []string{"x", "y"}},
|
|
{"x int, y string, z []byte", []string{"x", "y", "z"}},
|
|
}
|
|
for _, c := range cases {
|
|
got := extractArgNames(c.in)
|
|
if len(got) != len(c.want) {
|
|
t.Errorf("extractArgNames(%q) = %v, want %v", c.in, got, c.want)
|
|
continue
|
|
}
|
|
for i := range got {
|
|
if got[i] != c.want[i] {
|
|
t.Errorf("extractArgNames(%q)[%d] = %q, want %q", c.in, i, got[i], c.want[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSplitLongStringsNoShortStrings(t *testing.T) {
|
|
src := `package main
|
|
var a = "hi"
|
|
var b = "short"
|
|
`
|
|
got := SplitLongStrings(src)
|
|
if got != src {
|
|
t.Errorf("SplitLongStrings modified short string block:\nbefore: %q\nafter: %q", src, got)
|
|
}
|
|
}
|
|
|
|
func TestSplitLongStringsSplitsLongLiteral(t *testing.T) {
|
|
src := `package main
|
|
var key = "sk-proj-FAKE-KEY-1234567890abcdef"
|
|
`
|
|
got := SplitLongStrings(src)
|
|
if got == src {
|
|
t.Fatalf("SplitLongStrings did not split long string")
|
|
}
|
|
if !strings.Contains(got, " + ") {
|
|
t.Errorf("SplitLongStrings output missing concatenation: %q", got)
|
|
}
|
|
for _, frag := range []string{`"sk-pr"`, `"oj-FA"`, `"KE-KE"`, `"Y-123"`} {
|
|
// We just check the general structure
|
|
_ = frag
|
|
}
|
|
}
|
|
|
|
func TestSplitLongStringsSkipsEscapes(t *testing.T) {
|
|
src := `package main
|
|
var s = "abcdefghijklmnopqrs\ntuvwxyz1234"
|
|
`
|
|
got := SplitLongStrings(src)
|
|
// Has an escape, must NOT be split
|
|
if got != src {
|
|
t.Errorf("SplitLongStrings split a string with escape sequences:\nbefore: %q\nafter: %q", src, got)
|
|
}
|
|
}
|
|
|
|
func TestSplitLongStringsSkipsImportBlock(t *testing.T) {
|
|
src := `package main
|
|
import "github.com/lukaszraczylo/go-obfuscation/pkg/strenc-with-a-very-long-name-just-to-be-sure"
|
|
var x = "abcdefghijklmnopqrstuvwxyz1234"
|
|
`
|
|
got := SplitLongStrings(src)
|
|
// The import path must remain a single literal
|
|
if !strings.Contains(got, `"github.com/lukaszraczylo/go-obfuscation/pkg/strenc-with-a-very-long-name-just-to-be-sure"`) {
|
|
t.Errorf("SplitLongStrings modified import path:\n%s", got)
|
|
}
|
|
// The non-import string should be split
|
|
if !strings.Contains(got, " + ") {
|
|
t.Errorf("SplitLongStrings did not split the non-import long string:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestTransformConstantsSkipsFuncBodies(t *testing.T) {
|
|
src := `package main
|
|
const foo = 100
|
|
func bar() int { return 42 }
|
|
`
|
|
got := TransformConstants(src)
|
|
// Outside function body — no transform
|
|
if !strings.Contains(got, "const foo = 100") {
|
|
t.Errorf("TransformConstants modified non-function-body code:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestTransformConstantsBlindsInsideFunctions(t *testing.T) {
|
|
src := `package main
|
|
func bar() int {
|
|
return 42
|
|
}
|
|
`
|
|
got := TransformConstants(src)
|
|
if !strings.Contains(got, "_blind") {
|
|
t.Errorf("TransformConstants did not introduce a _blind helper call:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestTransformConstantsPreservesSmallInts(t *testing.T) {
|
|
src := `package main
|
|
func bar() { x := 0; y := 1; z := 2; _ = x; _ = y; _ = z }
|
|
`
|
|
got := TransformConstants(src)
|
|
for _, lit := range []string{"0", "1", "2"} {
|
|
// We want these left as-is.
|
|
if !strings.Contains(got, lit) {
|
|
t.Errorf("TransformConstants lost small integer %q in:\n%s", lit, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTransformMBANoChangeOnAssignments(t *testing.T) {
|
|
// Lines containing := should be left alone by MBA (they're assignments).
|
|
src := `package main
|
|
func bar() int { x := a + b; return x }
|
|
`
|
|
got := TransformMBA(src)
|
|
if got != src {
|
|
t.Errorf("TransformMBA modified an assignment line:\nbefore: %q\nafter: %q", src, got)
|
|
}
|
|
}
|
|
|
|
func TestTransformMBAModifiesAddition(t *testing.T) {
|
|
src := `package main
|
|
func bar() int {
|
|
_ = a + b
|
|
return 0
|
|
}
|
|
`
|
|
got := TransformMBA(src)
|
|
if got == src {
|
|
t.Errorf("TransformMBA did not modify a + b")
|
|
}
|
|
}
|
|
|
|
func TestTransformDirCopiesAndSkipsTestFiles(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
srcDir := tmp + "/src"
|
|
dstDir := tmp + "/dst"
|
|
if err := os.MkdirAll(srcDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(srcDir+"/main.go", []byte("package main\nfunc main() {}\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(srcDir+"/main_test.go", []byte("package main\n// test file\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(srcDir+"/data.txt", []byte("hello"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tr := New(Config{BuildSeed: 1})
|
|
if err := tr.TransformDir(srcDir, dstDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := os.ReadFile(dstDir + "/main.go"); err != nil {
|
|
t.Errorf("main.go not copied: %v", err)
|
|
}
|
|
if _, err := os.ReadFile(dstDir + "/data.txt"); err != nil {
|
|
t.Errorf("data.txt not copied: %v", err)
|
|
}
|
|
if _, err := os.ReadFile(dstDir + "/main_test.go"); !os.IsNotExist(err) {
|
|
t.Errorf("main_test.go should not be copied, got err=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewSeedsDeterministicTransforms(t *testing.T) {
|
|
src := `package main
|
|
func main() { x := 1; _ = x }
|
|
`
|
|
tmp := t.TempDir()
|
|
a := src
|
|
b := src
|
|
for i, out := range []*string{&a, &b} {
|
|
dst := tmp + "/d" + string(rune('0'+i))
|
|
if err := os.MkdirAll(dst, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srcFile := dst + "/main.go"
|
|
if err := os.WriteFile(srcFile, []byte(src), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr := New(Config{BuildSeed: 42})
|
|
if err := tr.TransformFile(srcFile, dst+"/out.go"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(dst + "/out.go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
*out = string(data)
|
|
}
|
|
if a != b {
|
|
t.Errorf("Same seed produced different output:\nA: %s\nB: %s", a, b)
|
|
}
|
|
}
|
|
|
|
func TestReorderBasicBlocksIdempotentOnNoControlFlow(t *testing.T) {
|
|
src := `package main
|
|
func bar() int { return 1 }
|
|
`
|
|
got := ReorderBasicBlocks(src)
|
|
// No if/switch in body — output should equal input
|
|
if got != src {
|
|
t.Errorf("ReorderBasicBlocks modified body without control flow:\nbefore: %q\nafter: %q", src, got)
|
|
}
|
|
}
|
|
|
|
func TestReorderBasicBlocksSwapsIfElse(t *testing.T) {
|
|
src := `package main
|
|
func bar(x int) int {
|
|
if x > 0 {
|
|
return 1
|
|
} else {
|
|
return 2
|
|
}
|
|
}
|
|
`
|
|
got := ReorderBasicBlocks(src)
|
|
// Either condition is negated (x <= 0) OR the swap is skipped (rng < 0.4).
|
|
// We just check that the function is still parseable / both returns survive.
|
|
if !strings.Contains(got, "return 1") || !strings.Contains(got, "return 2") {
|
|
t.Errorf("ReorderBasicBlocks lost a return value:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestSplitFunctionsIdempotentOnSmallBodies(t *testing.T) {
|
|
src := `package main
|
|
func small() { _ = 1 }
|
|
`
|
|
got := SplitFunctions(src)
|
|
if got != src {
|
|
t.Errorf("SplitFunctions modified body with too few stmts:\nbefore: %q\nafter: %q", src, got)
|
|
}
|
|
}
|
|
|
|
func TestSplitFunctionsSplitsLargeBody(t *testing.T) {
|
|
src := `package main
|
|
func big() int {
|
|
a := 1
|
|
b := 2
|
|
c := 3
|
|
d := 4
|
|
e := 5
|
|
f := 6
|
|
g := 7
|
|
h := 8
|
|
return a + b + c + d + e + f + g + h
|
|
}
|
|
`
|
|
// SplitFunctions uses an internal random source with ~50% split probability.
|
|
// Retry until we observe a split, up to 20 times.
|
|
for i := 0; i < 20; i++ {
|
|
got := SplitFunctions(src)
|
|
if got != src && strings.Contains(got, "switch ") {
|
|
return // pass
|
|
}
|
|
}
|
|
t.Errorf("SplitFunctions never split the large body after 20 attempts")
|
|
}
|
|
|
|
func TestInjectFakeSignaturesAddsFunctions(t *testing.T) {
|
|
src := `package main
|
|
|
|
func main() {}
|
|
`
|
|
got := InjectFakeSignatures(src)
|
|
if !strings.Contains(got, "//go:noinline") {
|
|
t.Errorf("InjectFakeSignatures did not add go:noinline funcs:\n%s", got)
|
|
}
|
|
// Should have introduced new top-level func decls
|
|
origCount := countFuncDecls(src)
|
|
gotCount := countFuncDecls(got)
|
|
if gotCount <= origCount {
|
|
t.Errorf("InjectFakeSignatures did not add funcs: %d -> %d", origCount, gotCount)
|
|
}
|
|
}
|
|
|
|
func countFuncDecls(src string) int {
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "x.go", src, 0)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
n := 0
|
|
for _, d := range f.Decls {
|
|
if _, ok := d.(*ast.FuncDecl); ok {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|