mirror of
https://github.com/lukaszraczylo/go-obfuscation.git
synced 2026-10-14 05:48:22 +00:00
.
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
// Basic block reordering transformation.
|
||||
//
|
||||
// Randomizes the order of basic blocks within functions to defeat
|
||||
// linear disassembly and static analysis. Handles if/else body swapping
|
||||
// (with condition negation), switch case reordering, and sequential
|
||||
// block permutation with jump-label indirection.
|
||||
package transform
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
// Matches if (...) { ... } else { ... } blocks at the line level
|
||||
bbrIfPattern = regexp.MustCompile(`^(\s*)if\s+(.+?)\s*\{`)
|
||||
bbrElsePattern = regexp.MustCompile(`^(\s*)\}\s*else\s*\{`)
|
||||
bbrElseIfPattern = regexp.MustCompile(`^(\s*)\}\s*else\s+if\s+(.+?)\s*\{`)
|
||||
|
||||
// Matches switch statements
|
||||
bbrSwitchPattern = regexp.MustCompile(`^(\s*)switch\s+(.+?)\s*\{`)
|
||||
)
|
||||
|
||||
// ReorderBasicBlocks randomizes control flow block ordering within functions.
|
||||
func ReorderBasicBlocks(src string) string {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
|
||||
lines := strings.Split(src, "\n")
|
||||
var result []string
|
||||
i := 0
|
||||
|
||||
for i < len(lines) {
|
||||
trimmed := strings.TrimSpace(lines[i])
|
||||
|
||||
if isFuncDecl(trimmed) {
|
||||
funcLines := collectFuncBody(lines, i)
|
||||
if funcLines != nil && len(funcLines) >= 12 {
|
||||
reordered := reorderFuncBlocks(funcLines, rng)
|
||||
result = append(result, reordered...)
|
||||
i += len(funcLines)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// reorderFuncBlocks applies all reordering strategies to a single function.
|
||||
func reorderFuncBlocks(funcLines []string, rng *rand.Rand) []string {
|
||||
funcSig := funcLines[0]
|
||||
closingBrace := funcLines[len(funcLines)-1]
|
||||
bodyLines := funcLines[1 : len(funcLines)-1]
|
||||
|
||||
var result []string
|
||||
result = append(result, funcSig)
|
||||
|
||||
// Strategy 1: Swap if/else bodies with negated conditions
|
||||
bodyLines = swapIfElse(bodyLines, rng)
|
||||
|
||||
// Strategy 2: Reorder switch cases
|
||||
bodyLines = reorderSwitchCases(bodyLines, rng)
|
||||
|
||||
result = append(result, bodyLines...)
|
||||
result = append(result, closingBrace)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// swapIfElse finds if/else blocks and swaps the bodies, negating the condition.
|
||||
func swapIfElse(lines []string, rng *rand.Rand) []string {
|
||||
var result []string
|
||||
i := 0
|
||||
|
||||
for i < len(lines) {
|
||||
// Look for if blocks with else
|
||||
ifMatch := bbrIfPattern.FindStringSubmatch(lines[i])
|
||||
if ifMatch == nil || rng.Float64() > 0.4 {
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect the if body
|
||||
indent := ifMatch[1]
|
||||
condition := ifMatch[2]
|
||||
ifBody := []string{lines[i]}
|
||||
i++
|
||||
depth := 1
|
||||
|
||||
for i < len(lines) && depth > 0 {
|
||||
line := lines[i]
|
||||
for _, c := range line {
|
||||
if c == '{' {
|
||||
depth++
|
||||
}
|
||||
if c == '}' {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
ifBody = append(ifBody, line)
|
||||
i++
|
||||
|
||||
// Check if this line ends the if block and has else
|
||||
if depth == 0 && i < len(lines) {
|
||||
if bbrElsePattern.MatchString(lines[i]) || bbrElseIfPattern.MatchString(lines[i]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for else block
|
||||
if i >= len(lines) || depth != 0 {
|
||||
result = append(result, ifBody...)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle else if — don't swap, too complex
|
||||
if bbrElseIfPattern.MatchString(lines[i]) {
|
||||
result = append(result, ifBody...)
|
||||
continue
|
||||
}
|
||||
|
||||
if !bbrElsePattern.MatchString(lines[i]) {
|
||||
result = append(result, ifBody...)
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect else body
|
||||
elseBody := []string{lines[i]}
|
||||
i++
|
||||
depth = 1
|
||||
for i < len(lines) && depth > 0 {
|
||||
line := lines[i]
|
||||
for _, c := range line {
|
||||
if c == '{' {
|
||||
depth++
|
||||
}
|
||||
if c == '}' {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
elseBody = append(elseBody, line)
|
||||
i++
|
||||
}
|
||||
|
||||
// Swap: emit negated condition with else body first, then else with if body
|
||||
negCond := negateCondition(condition)
|
||||
|
||||
// Build swapped: if !condition { <else body> } else { <if body> }
|
||||
result = append(result, indent+"if "+negCond+" {")
|
||||
// The else body contents (skip the "else {" line and closing "}")
|
||||
elseContents := elseBody[1 : len(elseBody)-1]
|
||||
for _, line := range elseContents {
|
||||
result = append(result, line)
|
||||
}
|
||||
result = append(result, indent+"} else {")
|
||||
// The if body contents (skip the "if ... {" line and closing "}")
|
||||
ifContents := ifBody[1 : len(ifBody)-1]
|
||||
for _, line := range ifContents {
|
||||
result = append(result, line)
|
||||
}
|
||||
result = append(result, indent+"}")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// negateCondition negates a boolean condition.
|
||||
func negateCondition(cond string) string {
|
||||
cond = strings.TrimSpace(cond)
|
||||
|
||||
// Handle simple negation
|
||||
if strings.HasPrefix(cond, "!") {
|
||||
return strings.TrimPrefix(cond, "!")
|
||||
}
|
||||
|
||||
// Handle parenthesized conditions
|
||||
if strings.HasPrefix(cond, "(") && strings.HasSuffix(cond, ")") {
|
||||
return "!(" + cond + ")"
|
||||
}
|
||||
|
||||
// Negate comparison operators
|
||||
negationMap := map[string]string{
|
||||
"==": "!=",
|
||||
"!=": "==",
|
||||
"<": ">=",
|
||||
">": "<=",
|
||||
"<=": ">",
|
||||
">=": "<",
|
||||
}
|
||||
|
||||
for op, negOp := range negationMap {
|
||||
idx := strings.Index(cond, op)
|
||||
if idx > 0 {
|
||||
// Make sure we're not matching a substring of a longer operator
|
||||
afterOp := idx + len(op)
|
||||
if afterOp < len(cond) {
|
||||
nextChar := cond[afterOp]
|
||||
if nextChar == '=' || nextChar == '<' || nextChar == '>' {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return cond[:idx] + negOp + cond[idx+len(op):]
|
||||
}
|
||||
}
|
||||
|
||||
// Default: wrap in negation
|
||||
return "!(" + cond + ")"
|
||||
}
|
||||
|
||||
// reorderSwitchCases finds switch statements and randomly reorders their cases.
|
||||
func reorderSwitchCases(lines []string, rng *rand.Rand) []string {
|
||||
var result []string
|
||||
i := 0
|
||||
|
||||
for i < len(lines) {
|
||||
trimmed := strings.TrimSpace(lines[i])
|
||||
|
||||
// Find switch statements
|
||||
if !strings.HasPrefix(trimmed, "switch ") || !strings.HasSuffix(trimmed, "{") {
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Only reorder with some probability
|
||||
if rng.Float64() > 0.4 {
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
switchHeader := lines[i]
|
||||
i++
|
||||
|
||||
// Collect all cases
|
||||
type caseBlock struct {
|
||||
header string
|
||||
body []string
|
||||
}
|
||||
var cases []caseBlock
|
||||
var currentCase *caseBlock
|
||||
depth := 1
|
||||
|
||||
for i < len(lines) && depth > 0 {
|
||||
line := lines[i]
|
||||
for _, c := range line {
|
||||
if c == '{' {
|
||||
depth++
|
||||
}
|
||||
if c == '}' {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
|
||||
if depth <= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
t := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(t, "case ") || strings.HasPrefix(t, "default:") {
|
||||
if currentCase != nil {
|
||||
cases = append(cases, *currentCase)
|
||||
}
|
||||
currentCase = &caseBlock{header: line}
|
||||
} else if currentCase != nil {
|
||||
currentCase.body = append(currentCase.body, line)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if currentCase != nil {
|
||||
cases = append(cases, *currentCase)
|
||||
}
|
||||
|
||||
// Skip closing brace of switch
|
||||
i++
|
||||
|
||||
// Only reorder if enough cases and no default (reordering default is risky)
|
||||
hasDefault := false
|
||||
for _, c := range cases {
|
||||
if strings.Contains(strings.TrimSpace(c.header), "default:") {
|
||||
hasDefault = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(cases) >= 3 && !hasDefault {
|
||||
// Shuffle non-first cases (keep first case position to maintain entry point)
|
||||
if len(cases) > 2 {
|
||||
shuffleStart := 1
|
||||
perm := rng.Perm(len(cases) - shuffleStart)
|
||||
shuffled := make([]caseBlock, len(cases))
|
||||
copy(shuffled, cases[:shuffleStart])
|
||||
for j, p := range perm {
|
||||
shuffled[shuffleStart+j] = cases[shuffleStart+p]
|
||||
}
|
||||
cases = shuffled
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct switch
|
||||
result = append(result, switchHeader)
|
||||
for _, c := range cases {
|
||||
result = append(result, c.header)
|
||||
result = append(result, c.body...)
|
||||
}
|
||||
result = append(result, "\t}")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// InjectJumpLabels converts sequential blocks into labeled blocks with goto
|
||||
// indirection for functions that are eligible. This is the most aggressive
|
||||
// reordering strategy and requires careful analysis of control dependencies.
|
||||
func InjectJumpLabels(bodyLines []string, rng *rand.Rand) []string {
|
||||
stmts := parseStatements(bodyLines)
|
||||
if len(stmts) < 4 {
|
||||
return bodyLines
|
||||
}
|
||||
|
||||
// Generate labels
|
||||
var labels []string
|
||||
for range stmts {
|
||||
labels = append(labels, stringcrypt.RandomIdent("_lbl", 4))
|
||||
}
|
||||
|
||||
// Create a random permutation of statement order
|
||||
perm := rng.Perm(len(stmts))
|
||||
|
||||
var result []string
|
||||
result = append(result, "\tgoto "+labels[perm[0]])
|
||||
|
||||
for _, idx := range perm {
|
||||
result = append(result, labels[idx]+":")
|
||||
for _, line := range stmts[idx] {
|
||||
trimmed := strings.TrimPrefix(line, "\t")
|
||||
result = append(result, "\t"+trimmed)
|
||||
}
|
||||
// Add goto to next block in permutation order
|
||||
nextIdx := -1
|
||||
for j, p := range perm {
|
||||
if p == idx && j+1 < len(perm) {
|
||||
nextIdx = perm[j+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
if nextIdx >= 0 {
|
||||
result = append(result, "\tgoto "+labels[nextIdx])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Constant blinding transformation.
|
||||
//
|
||||
// Replaces integer literal constants with runtime XOR expressions that
|
||||
// evaluate to the original value, defeating pattern-matching in disassemblers
|
||||
// and static analysis tools. Each constant C is replaced with (K ^ (K ^ C))
|
||||
// where K is a per-constant random key.
|
||||
package transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
cbHexRe = regexp.MustCompile(`0x[0-9a-fA-F]+`)
|
||||
cbStringRe = regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"`)
|
||||
cbBacktickRe = regexp.MustCompile("`[^`]*`")
|
||||
|
||||
cbSkipContexts = []string{
|
||||
"case ", "case\t",
|
||||
"[",
|
||||
"len(", "cap(",
|
||||
"make(",
|
||||
"iota",
|
||||
}
|
||||
)
|
||||
|
||||
func TransformConstants(src string) string {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
blindHelperName := stringcrypt.RandomIdent("_blind", 4)
|
||||
|
||||
lines := strings.Split(src, "\n")
|
||||
var result []string
|
||||
inFuncBody := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") {
|
||||
inFuncBody = true
|
||||
}
|
||||
if inFuncBody && trimmed == "}" {
|
||||
inFuncBody = false
|
||||
}
|
||||
|
||||
if !inFuncBody {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(trimmed, "//") || trimmed == "" || trimmed == "{" || trimmed == "}" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
if shouldCBLineSkip(trimmed) {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
blinded := cbBlindLine(line, rng, blindHelperName)
|
||||
result = append(result, blinded)
|
||||
}
|
||||
|
||||
resultStr := strings.Join(result, "\n")
|
||||
resultStr = injectBlindHelper(resultStr, blindHelperName)
|
||||
return resultStr
|
||||
}
|
||||
|
||||
func shouldCBLineSkip(trimmed string) bool {
|
||||
if strings.HasPrefix(trimmed, "case ") || strings.HasPrefix(trimmed, "case\t") {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(trimmed, "[") && strings.Contains(trimmed, "]") {
|
||||
return true
|
||||
}
|
||||
for _, ctx := range cbSkipContexts {
|
||||
if strings.Contains(trimmed, ctx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cbBlindLine(line string, rng *rand.Rand, helperName string) string {
|
||||
line = cbHexRe.ReplaceAllStringFunc(line, func(match string) string {
|
||||
return cbBlindHex(match, rng, helperName)
|
||||
})
|
||||
|
||||
line = blindDecimalsInLine(line, rng, helperName)
|
||||
return line
|
||||
}
|
||||
|
||||
func blindDecimalsInLine(line string, rng *rand.Rand, helperName string) string {
|
||||
var buf strings.Builder
|
||||
i := 0
|
||||
for i < len(line) {
|
||||
if unicode.IsDigit(rune(line[i])) {
|
||||
start := i
|
||||
for i < len(line) && unicode.IsDigit(rune(line[i])) {
|
||||
i++
|
||||
}
|
||||
numStr := line[start:i]
|
||||
|
||||
if start > 0 && (unicode.IsLetter(rune(line[start-1])) || line[start-1] == '_') {
|
||||
buf.WriteString(numStr)
|
||||
continue
|
||||
}
|
||||
if i < len(line) && (unicode.IsLetter(rune(line[i])) || line[i] == '_' || line[i] == 'x' || line[i] == 'X') {
|
||||
buf.WriteString(numStr)
|
||||
continue
|
||||
}
|
||||
|
||||
replaced := cbBlindDecimal(numStr, rng, helperName)
|
||||
buf.WriteString(replaced)
|
||||
} else {
|
||||
buf.WriteByte(line[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func cbBlindHex(match string, rng *rand.Rand, helperName string) string {
|
||||
val, err := strconv.ParseUint(match, 0, 64)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
if val == 0 || val == 1 {
|
||||
return match
|
||||
}
|
||||
if val > 0x7FFFFFFFFFFFFFFF {
|
||||
return match
|
||||
}
|
||||
|
||||
key := rng.Uint64() | 1
|
||||
blinded := key ^ val
|
||||
return fmt.Sprintf("%s(0x%X, 0x%X)", helperName, key, blinded)
|
||||
}
|
||||
|
||||
func cbBlindDecimal(match string, rng *rand.Rand, helperName string) string {
|
||||
val, err := strconv.ParseUint(match, 10, 64)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
if val <= 2 || val < 10 {
|
||||
return match
|
||||
}
|
||||
|
||||
key := rng.Uint64() | 1
|
||||
blinded := key ^ val
|
||||
return fmt.Sprintf("%s(0x%X, 0x%X)", helperName, key, blinded)
|
||||
}
|
||||
|
||||
func injectBlindHelper(src string, helperName string) string {
|
||||
helper := fmt.Sprintf(`
|
||||
//go:noinline
|
||||
func %s(a, b uint64) uint64 { return a ^ b }
|
||||
`, helperName)
|
||||
|
||||
insertAt := findImportBlockEnd(src)
|
||||
if insertAt == -1 {
|
||||
pkgIdx := strings.Index(src, "package ")
|
||||
if pkgIdx != -1 {
|
||||
lineEnd := strings.Index(src[pkgIdx:], "\n")
|
||||
if lineEnd != -1 {
|
||||
insertAt = pkgIdx + lineEnd + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if insertAt != -1 && insertAt < len(src) {
|
||||
src = src[:insertAt] + helper + src[insertAt:]
|
||||
}
|
||||
return src
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// Fake function signature injection.
|
||||
//
|
||||
// Injects realistic-looking but dead function definitions into the source
|
||||
// file. These functions are never called and serve to confuse disassemblers
|
||||
// by polluting the symbol table with plausible function boundaries, making
|
||||
// it harder to identify real functions and their purposes.
|
||||
package transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
|
||||
)
|
||||
|
||||
type fakeFuncTemplate struct {
|
||||
namePattern string
|
||||
params string
|
||||
returnType string
|
||||
bodyGen func(rng *rand.Rand, argName string) string
|
||||
}
|
||||
|
||||
var fakeFuncTemplates = []fakeFuncTemplate{
|
||||
{
|
||||
namePattern: "_initAuth_obf",
|
||||
params: "token string",
|
||||
returnType: "bool",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_v", 3)
|
||||
v2 := stringcrypt.RandomIdent("_v", 3)
|
||||
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := %s & 0xFF\n\treturn %s > 100 && %s < 0",
|
||||
arg, v1, arg, v2, arg, v1, v2)
|
||||
},
|
||||
},
|
||||
{
|
||||
namePattern: "_parseConfig_obf",
|
||||
params: "data []byte",
|
||||
returnType: "int",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_c", 3)
|
||||
v2 := stringcrypt.RandomIdent("_c", 3)
|
||||
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := %s * 3\n\t_ = %s\n\treturn %s - %s",
|
||||
arg, v1, arg, v2, v1, v2, v1, v2)
|
||||
},
|
||||
},
|
||||
{
|
||||
namePattern: "_validateKey_obf",
|
||||
params: "key string, size int",
|
||||
returnType: "bool",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_k", 3)
|
||||
v2 := stringcrypt.RandomIdent("_k", 3)
|
||||
return fmt.Sprintf("\t%s := len(key)\n\t%s := size ^ 0xDEAD\n\t_ = %s\n\treturn %s == %s && %s != %s",
|
||||
v1, v2, v2, v1, v2, v1, v2)
|
||||
},
|
||||
},
|
||||
{
|
||||
namePattern: "_hashData_obf",
|
||||
params: "input []byte",
|
||||
returnType: "[32]byte",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_h", 3)
|
||||
return fmt.Sprintf("\tvar %s [32]byte\n\t_ = %s\n\tfor _i := range %s {\n\t\t%s[_i] = byte(_i)\n\t}\n\treturn %s",
|
||||
v1, arg, arg, v1, v1)
|
||||
},
|
||||
},
|
||||
{
|
||||
namePattern: "_encodePayload_obf",
|
||||
params: "buf []byte, offset int",
|
||||
returnType: "int",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_e", 3)
|
||||
return fmt.Sprintf("\t_ = buf\n\t_ = offset\n\t%s := len(buf) + offset\n\treturn %s ^ %s",
|
||||
v1, v1, v1)
|
||||
},
|
||||
},
|
||||
{
|
||||
namePattern: "_checkPerms_obf",
|
||||
params: "uid int, gid int",
|
||||
returnType: "bool",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_p", 3)
|
||||
return fmt.Sprintf("\t%s := uid ^ gid\n\treturn %s > 0 && %s < 0 && uid == gid && uid != gid",
|
||||
v1, v1, v1)
|
||||
},
|
||||
},
|
||||
{
|
||||
namePattern: "_readStream_obf",
|
||||
params: "path string",
|
||||
returnType: "[]byte",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_r", 3)
|
||||
v2 := stringcrypt.RandomIdent("_r", 3)
|
||||
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := make([]byte, %s)\n\tfor _i := range %s {\n\t\t%s[_i] = 0\n\t}\n\treturn %s",
|
||||
arg, v1, arg, v2, v1, v2, v2, v2)
|
||||
},
|
||||
},
|
||||
{
|
||||
namePattern: "_formatLog_obf",
|
||||
params: "level int, msg string",
|
||||
returnType: "string",
|
||||
bodyGen: func(rng *rand.Rand, arg string) string {
|
||||
v1 := stringcrypt.RandomIdent("_l", 3)
|
||||
return fmt.Sprintf("\t_ = level\n\t%s := len(msg)\n\tif %s > 1000000 {\n\t\treturn \"\"\n\t}\n\treturn msg[:%s]",
|
||||
v1, v1, v1)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// InjectFakeSignatures inserts 2-5 fake function definitions per file
|
||||
// that look realistic but are never called.
|
||||
func InjectFakeSignatures(src string) string {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
|
||||
numFake := rng.Intn(4) + 2 // 2-5 fake functions
|
||||
var fakeFuncs strings.Builder
|
||||
|
||||
// Select random templates without replacement
|
||||
perm := rng.Perm(len(fakeFuncTemplates))
|
||||
for i := 0; i < numFake && i < len(fakeFuncTemplates); i++ {
|
||||
tmpl := fakeFuncTemplates[perm[i]]
|
||||
suffix := fmt.Sprintf("%04X", rng.Intn(0xFFFF))
|
||||
funcName := tmpl.namePattern + suffix
|
||||
|
||||
argName := extractFirstArgName(tmpl.params)
|
||||
|
||||
fakeFuncs.WriteString("\n//go:noinline\n")
|
||||
fakeFuncs.WriteString(fmt.Sprintf("func %s(%s) %s {\n", funcName, tmpl.params, tmpl.returnType))
|
||||
fakeFuncs.WriteString(tmpl.bodyGen(rng, argName))
|
||||
fakeFuncs.WriteString("\n}\n")
|
||||
}
|
||||
|
||||
// Inject at package level, after imports
|
||||
insertAt := findImportBlockEnd(src)
|
||||
if insertAt == -1 {
|
||||
pkgIdx := strings.Index(src, "package ")
|
||||
if pkgIdx != -1 {
|
||||
lineEnd := strings.Index(src[pkgIdx:], "\n")
|
||||
if lineEnd != -1 {
|
||||
insertAt = pkgIdx + lineEnd + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if insertAt != -1 && insertAt < len(src) {
|
||||
src = src[:insertAt] + fakeFuncs.String() + src[insertAt:]
|
||||
}
|
||||
|
||||
return src
|
||||
}
|
||||
|
||||
func extractFirstArgName(params string) string {
|
||||
params = strings.TrimSpace(params)
|
||||
if params == "" {
|
||||
return "_"
|
||||
}
|
||||
parts := strings.SplitN(params, ",", 2)
|
||||
first := strings.TrimSpace(parts[0])
|
||||
fields := strings.Fields(first)
|
||||
if len(fields) >= 1 {
|
||||
return fields[0]
|
||||
}
|
||||
return "_"
|
||||
}
|
||||
|
||||
// InjectMoreFakeSignatures generates additional fake functions with
|
||||
// random realistic names and bodies for deeper obfuscation.
|
||||
func InjectMoreFakeSignatures(src string, count int) string {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
|
||||
prefixes := []string{
|
||||
"_resolveAddr", "_buildRequest", "_handleResp", "_writeLog",
|
||||
"_parseHeader", "_checkAuth", "_compressData", "_decompressBuf",
|
||||
"_verifySig", "_loadCache", "_flushBuffer", "_retryConn",
|
||||
"_closeSocket", "_allocMem", "_releaseLock", "_acquireLock",
|
||||
"_serializeMsg", "_deserializeMsg", "_encodeURL", "_decodeURL",
|
||||
}
|
||||
|
||||
paramSets := []struct {
|
||||
params string
|
||||
returnType string
|
||||
}{
|
||||
{"addr string, port int", "error"},
|
||||
{"data []byte", "int"},
|
||||
{"key string", "bool"},
|
||||
{"src, dst string", "string"},
|
||||
{"n int", "int64"},
|
||||
{"buf []byte, offset int", "[]byte"},
|
||||
{"flag bool, name string", "string"},
|
||||
{"x, y int64", "int64"},
|
||||
}
|
||||
|
||||
var fakeFuncs strings.Builder
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
prefix := prefixes[rng.Intn(len(prefixes))]
|
||||
suffix := fmt.Sprintf("%04X", rng.Intn(0xFFFF))
|
||||
funcName := prefix + "_obf" + suffix
|
||||
|
||||
pset := paramSets[rng.Intn(len(paramSets))]
|
||||
argName := extractFirstArgName(pset.params)
|
||||
|
||||
body := generateRandomBody(rng, argName, pset.returnType)
|
||||
|
||||
fakeFuncs.WriteString("\n//go:noinline\n")
|
||||
fakeFuncs.WriteString(fmt.Sprintf("func %s(%s) %s {\n", funcName, pset.params, pset.returnType))
|
||||
fakeFuncs.WriteString(body)
|
||||
fakeFuncs.WriteString("\n}\n")
|
||||
}
|
||||
|
||||
insertAt := findImportBlockEnd(src)
|
||||
if insertAt == -1 {
|
||||
pkgIdx := strings.Index(src, "package ")
|
||||
if pkgIdx != -1 {
|
||||
lineEnd := strings.Index(src[pkgIdx:], "\n")
|
||||
if lineEnd != -1 {
|
||||
insertAt = pkgIdx + lineEnd + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if insertAt != -1 && insertAt < len(src) {
|
||||
src = src[:insertAt] + fakeFuncs.String() + src[insertAt:]
|
||||
}
|
||||
|
||||
return src
|
||||
}
|
||||
|
||||
// generateRandomBody creates a plausible-looking function body that computes
|
||||
// something meaningless but looks legitimate.
|
||||
func generateRandomBody(rng *rand.Rand, argName, returnType string) string {
|
||||
var sb strings.Builder
|
||||
numStmts := rng.Intn(6) + 3 // 3-8 statements
|
||||
|
||||
vars := make([]string, numStmts)
|
||||
for i := range vars {
|
||||
vars[i] = stringcrypt.RandomIdent("_x", 3)
|
||||
}
|
||||
|
||||
// Always start with consuming the argument
|
||||
sb.WriteString(fmt.Sprintf("\t_ = %s\n", argName))
|
||||
|
||||
for i := 0; i < numStmts; i++ {
|
||||
switch rng.Intn(5) {
|
||||
case 0:
|
||||
// Assignment from arithmetic
|
||||
val := rng.Intn(0xFFFF) + 1
|
||||
sb.WriteString(fmt.Sprintf("\t%s := %d ^ 0x%X\n", vars[i], val, rng.Intn(0xFFFF)))
|
||||
case 1:
|
||||
// Boolean expression
|
||||
sb.WriteString(fmt.Sprintf("\t%s := %d > %d && %d < %d\n",
|
||||
vars[i], rng.Intn(100), rng.Intn(100)+200, rng.Intn(100), rng.Intn(100)+200))
|
||||
case 2:
|
||||
// Self-referencing computation
|
||||
if i > 0 {
|
||||
sb.WriteString(fmt.Sprintf("\t%s = %s + %d\n", vars[i-1], vars[i-1], rng.Intn(1000)))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("\t%s := %d\n", vars[i], rng.Intn(0xFFFF)))
|
||||
}
|
||||
case 3:
|
||||
// Dead code branch
|
||||
sb.WriteString(fmt.Sprintf("\tif false { %s = %s }\n", vars[i], vars[max(0, i-1)]))
|
||||
case 4:
|
||||
// Bitwise operation
|
||||
sb.WriteString(fmt.Sprintf("\t%s := int64(%d) << %d\n", vars[i], rng.Intn(256), rng.Intn(8)))
|
||||
}
|
||||
}
|
||||
|
||||
// Return something based on the return type
|
||||
switch returnType {
|
||||
case "bool":
|
||||
sb.WriteString(fmt.Sprintf("\treturn %s && !%s\n", vars[0], vars[0]))
|
||||
case "int", "int64":
|
||||
sb.WriteString(fmt.Sprintf("\treturn %s - %s\n", vars[0], vars[0]))
|
||||
case "string":
|
||||
sb.WriteString("\treturn \"\"\n")
|
||||
case "[]byte":
|
||||
sb.WriteString("\treturn nil\n")
|
||||
case "[32]byte":
|
||||
sb.WriteString(fmt.Sprintf("\tvar %s [32]byte\n\treturn %s\n", vars[numStmts-1], vars[numStmts-1]))
|
||||
case "error":
|
||||
sb.WriteString("\treturn nil\n")
|
||||
default:
|
||||
sb.WriteString(fmt.Sprintf("\treturn %s\n", vars[0]))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
// Function splitting transformation.
|
||||
//
|
||||
// Splits function bodies with 8+ statements into fragments called via
|
||||
// indirect dispatch using a state machine pattern. Shared variables are
|
||||
// hoisted and passed between fragments. Each fragment is marked go:noinline
|
||||
// to prevent the compiler from re-inlining the split code.
|
||||
package transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
|
||||
)
|
||||
|
||||
// SplitFunctions splits eligible function bodies into fragments called
|
||||
// through a state-machine dispatcher.
|
||||
func SplitFunctions(src string) string {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
|
||||
lines := strings.Split(src, "\n")
|
||||
var result []string
|
||||
i := 0
|
||||
|
||||
for i < len(lines) {
|
||||
trimmed := strings.TrimSpace(lines[i])
|
||||
|
||||
if isFuncDecl(trimmed) && !isMainOrInit(trimmed) {
|
||||
funcLines := collectFuncBody(lines, i)
|
||||
if funcLines != nil {
|
||||
bodyLines := funcLines[1 : len(funcLines)-1]
|
||||
funcSig := funcLines[0]
|
||||
closingBrace := funcLines[len(funcLines)-1]
|
||||
|
||||
if shouldSplit(bodyLines) && rng.Float64() < 0.5 {
|
||||
split := splitFunctionBody(funcSig, bodyLines, closingBrace, rng)
|
||||
result = append(result, split...)
|
||||
i += len(funcLines)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, lines[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func isFuncDecl(trimmed string) bool {
|
||||
return strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{")
|
||||
}
|
||||
|
||||
func isMainOrInit(trimmed string) bool {
|
||||
return strings.Contains(trimmed, "main") || strings.Contains(trimmed, "init()")
|
||||
}
|
||||
|
||||
// collectFuncBody returns the lines of a function (signature through closing brace)
|
||||
// starting at lineIdx. Returns nil if not a complete function.
|
||||
func collectFuncBody(lines []string, lineIdx int) []string {
|
||||
if lineIdx >= len(lines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
funcLines := []string{lines[lineIdx]}
|
||||
i := lineIdx + 1
|
||||
depth := 1
|
||||
|
||||
for i < len(lines) && depth > 0 {
|
||||
for _, c := range lines[i] {
|
||||
if c == '{' {
|
||||
depth++
|
||||
}
|
||||
if c == '}' {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
funcLines = append(funcLines, lines[i])
|
||||
i++
|
||||
}
|
||||
|
||||
if depth != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return funcLines
|
||||
}
|
||||
|
||||
// shouldSplit determines if a function body is worth splitting.
|
||||
// Requires 8+ statements and no disqualifying patterns.
|
||||
func shouldSplit(bodyLines []string) bool {
|
||||
stmtCount := 0
|
||||
for _, line := range bodyLines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || trimmed == "{" || trimmed == "}" {
|
||||
continue
|
||||
}
|
||||
// Skip functions with early returns, goto, or defer
|
||||
if strings.HasPrefix(trimmed, "return ") || trimmed == "return" {
|
||||
if stmtCount < 3 {
|
||||
return false // Early return — don't split
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "goto ") || strings.HasPrefix(trimmed, "defer ") {
|
||||
return false
|
||||
}
|
||||
// Skip if there are labels
|
||||
if strings.HasSuffix(trimmed, ":") && !strings.HasPrefix(trimmed, "case ") && !strings.HasPrefix(trimmed, "default") {
|
||||
return false
|
||||
}
|
||||
stmtCount++
|
||||
}
|
||||
return stmtCount >= 8
|
||||
}
|
||||
|
||||
// splitFunctionBody splits a function body into fragments using a state machine.
|
||||
func splitFunctionBody(funcSig string, bodyLines []string, closingBrace string, rng *rand.Rand) []string {
|
||||
var result []string
|
||||
result = append(result, funcSig)
|
||||
|
||||
// Parse statements from body
|
||||
stmts := parseStatements(bodyLines)
|
||||
if len(stmts) < 4 {
|
||||
// Not enough statements; return unmodified
|
||||
result = append(result, bodyLines...)
|
||||
result = append(result, closingBrace)
|
||||
return result
|
||||
}
|
||||
|
||||
// Determine number of fragments (2-4)
|
||||
numFragments := 2
|
||||
if len(stmts) >= 12 {
|
||||
numFragments = 3
|
||||
}
|
||||
if len(stmts) >= 16 {
|
||||
numFragments = 4
|
||||
}
|
||||
|
||||
// Collect variables declared with := in the body so we can hoist them
|
||||
var hoistedVars []string
|
||||
declaredVars := map[string]bool{}
|
||||
var cleanedStmts [][]string
|
||||
|
||||
for _, stmt := range stmts {
|
||||
var cleaned []string
|
||||
for _, line := range stmt {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.Contains(trimmed, ":=") {
|
||||
parts := strings.SplitN(trimmed, ":=", 2)
|
||||
varName := strings.TrimSpace(parts[0])
|
||||
rhs := strings.TrimSpace(parts[1])
|
||||
|
||||
names := splitVarNames(varName)
|
||||
allValid := true
|
||||
for _, n := range names {
|
||||
if !isValidIdent(n) {
|
||||
allValid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allValid {
|
||||
for _, n := range names {
|
||||
if !declaredVars[n] {
|
||||
hoistedVars = append(hoistedVars, "\tvar "+n+" "+inferVarType(rhs))
|
||||
declaredVars[n] = true
|
||||
}
|
||||
}
|
||||
cleaned = append(cleaned, "\t"+varName+" = "+rhs)
|
||||
} else {
|
||||
cleaned = append(cleaned, line)
|
||||
}
|
||||
} else {
|
||||
cleaned = append(cleaned, line)
|
||||
}
|
||||
}
|
||||
cleanedStmts = append(cleanedStmts, cleaned)
|
||||
}
|
||||
|
||||
// Emit hoisted variables
|
||||
for _, v := range hoistedVars {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
// Build state machine
|
||||
stateVar := stringcrypt.RandomIdent("_sf", 4)
|
||||
result = append(result, "\t"+stateVar+" := 0")
|
||||
|
||||
// Determine return variable if last statement is a return
|
||||
returnVar := ""
|
||||
lastStmtLines := cleanedStmts[len(cleanedStmts)-1]
|
||||
if len(lastStmtLines) > 0 {
|
||||
lastTrimmed := strings.TrimSpace(lastStmtLines[len(lastStmtLines)-1])
|
||||
if strings.HasPrefix(lastTrimmed, "return ") {
|
||||
returnExpr := strings.TrimPrefix(lastTrimmed, "return ")
|
||||
returnVar = stringcrypt.RandomIdent("_ret", 4)
|
||||
result = append(result, "\tvar "+returnVar+" "+inferVarType(returnExpr))
|
||||
cleanedStmts[len(cleanedStmts)-1][len(lastStmtLines)-1] = "\t" + returnVar + " = " + returnExpr
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, "\tfor {")
|
||||
result = append(result, "\t\tswitch "+stateVar+" {")
|
||||
|
||||
// Distribute statements across fragments
|
||||
fragmentSize := len(cleanedStmts) / numFragments
|
||||
for f := 0; f < numFragments; f++ {
|
||||
result = append(result, fmt.Sprintf("\t\tcase %d:", f))
|
||||
start := f * fragmentSize
|
||||
end := (f + 1) * fragmentSize
|
||||
if f == numFragments-1 {
|
||||
end = len(cleanedStmts)
|
||||
}
|
||||
|
||||
for j := start; j < end; j++ {
|
||||
for _, line := range cleanedStmts[j] {
|
||||
// Re-indent for inside switch case
|
||||
trimmed := strings.TrimPrefix(line, "\t")
|
||||
result = append(result, "\t\t\t"+trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
if f < numFragments-1 {
|
||||
result = append(result, fmt.Sprintf("\t\t\t%s = %d", stateVar, f+1))
|
||||
} else {
|
||||
if returnVar != "" {
|
||||
result = append(result, "\t\t\treturn "+returnVar)
|
||||
} else {
|
||||
result = append(result, "\t\t\treturn")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, "\t\tdefault:")
|
||||
if returnVar != "" {
|
||||
result = append(result, "\t\t\treturn "+returnVar)
|
||||
} else {
|
||||
result = append(result, "\t\t\treturn")
|
||||
}
|
||||
result = append(result, "\t\t}")
|
||||
result = append(result, "\t}")
|
||||
result = append(result, closingBrace)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// parseStatements splits body lines into statement groups.
|
||||
// Each group is a complete statement (may span multiple lines for blocks).
|
||||
func parseStatements(bodyLines []string) [][]string {
|
||||
var statements [][]string
|
||||
var current []string
|
||||
depth := 0
|
||||
|
||||
for _, line := range bodyLines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || trimmed == "{" || trimmed == "}" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range trimmed {
|
||||
if c == '{' {
|
||||
depth++
|
||||
}
|
||||
if c == '}' {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
|
||||
current = append(current, line)
|
||||
|
||||
if depth <= 0 {
|
||||
statements = append(statements, current)
|
||||
current = nil
|
||||
depth = 0
|
||||
}
|
||||
}
|
||||
|
||||
if len(current) > 0 {
|
||||
statements = append(statements, current)
|
||||
}
|
||||
|
||||
return statements
|
||||
}
|
||||
|
||||
func splitVarNames(varPart string) []string {
|
||||
varPart = strings.TrimSpace(varPart)
|
||||
if strings.Contains(varPart, ",") {
|
||||
var names []string
|
||||
for _, n := range strings.Split(varPart, ",") {
|
||||
names = append(names, strings.TrimSpace(n))
|
||||
}
|
||||
return names
|
||||
}
|
||||
return []string{varPart}
|
||||
}
|
||||
|
||||
func inferVarType(rhs string) string {
|
||||
rhs = strings.TrimSpace(rhs)
|
||||
if strings.HasPrefix(rhs, "\"") || strings.HasPrefix(rhs, "`") || strings.HasPrefix(rhs, "fmt.") {
|
||||
return "string"
|
||||
}
|
||||
if strings.HasPrefix(rhs, "[]byte") || strings.HasPrefix(rhs, "make([]byte") {
|
||||
return "[]byte"
|
||||
}
|
||||
if rhs == "true" || rhs == "false" {
|
||||
return "bool"
|
||||
}
|
||||
if strings.HasPrefix(rhs, "[") {
|
||||
return "" // array — skip
|
||||
}
|
||||
// Default to int64 for numeric/unknown types
|
||||
return "int64"
|
||||
}
|
||||
|
||||
// InjectSplitFragments generates standalone fragment helper functions for a
|
||||
// more aggressive splitting strategy where fragments are separate functions
|
||||
// rather than switch cases. This is an alternative to the state-machine approach.
|
||||
func InjectSplitFragments(fragments []string, rng *rand.Rand) string {
|
||||
var sb strings.Builder
|
||||
for i, frag := range fragments {
|
||||
fragName := stringcrypt.RandomIdent(fmt.Sprintf("_frag%d_", i), 4)
|
||||
sb.WriteString("//go:noinline\n")
|
||||
sb.WriteString(fmt.Sprintf("func %s() {\n", fragName))
|
||||
sb.WriteString(frag)
|
||||
sb.WriteString("\n}\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
// MBA (Mixed Boolean-Arithmetic) expression transformation.
|
||||
//
|
||||
// Replaces simple arithmetic and bitwise operations with equivalent
|
||||
// MBA expressions that produce the same result but obscure the
|
||||
// original operation from static analysis and decompilation.
|
||||
package transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
// Binary operations: a OP b where OP is +, -, ^, &, |
|
||||
// Careful to not match comparison operators (==, !=, <=, >=, <<, >>)
|
||||
// and not match unary ^ or -.
|
||||
mbaBinAddRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*\+\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
|
||||
mbaBinSubRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*-\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
|
||||
mbaBinXorRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*\^\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
|
||||
mbaBinAndRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*&\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
|
||||
mbaBinOrRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*\|\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
|
||||
|
||||
// Unary bitwise NOT: ^a — match broadly, filter context in replacement
|
||||
mbaUnNotRe = regexp.MustCompile(`\^([a-zA-Z_][a-zA-Z0-9_.]*)`)
|
||||
|
||||
// Patterns to skip: comparisons, shifts, assignments
|
||||
mbaSkipRe = regexp.MustCompile(`==|!=|<=|>=|<<|>>|<-|\+\+|--|\|\|`)
|
||||
|
||||
// String literal detection to avoid transforming inside strings
|
||||
mbaStringLitRe = regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"`)
|
||||
mbaBacktickRe = regexp.MustCompile("`[^`]*`")
|
||||
|
||||
// Operators in comparisons, type assertions, channel ops that we must skip
|
||||
mbaCtxPatterns = []string{"==", "!=", "<=", ">=", "<<", ">>", "&&", "||", "<-", "++", "--", ":="}
|
||||
)
|
||||
|
||||
// TransformMBA replaces simple arithmetic and bitwise operations with
|
||||
// equivalent Mixed Boolean-Arithmetic expressions.
|
||||
func TransformMBA(src string) string {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
|
||||
// Build a set of string-literal and backtick-literal ranges to skip
|
||||
skipRanges := buildSkipRanges(src)
|
||||
|
||||
lines := strings.Split(src, "\n")
|
||||
var result []string
|
||||
inFuncBody := false
|
||||
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") {
|
||||
inFuncBody = true
|
||||
}
|
||||
if inFuncBody && trimmed == "}" {
|
||||
inFuncBody = false
|
||||
}
|
||||
|
||||
if !inFuncBody || trimmed == "" || strings.HasPrefix(trimmed, "//") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip lines with comparison/shift operators or assignments
|
||||
if containsAny(trimmed, mbaCtxPatterns) {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip lines that are mostly control flow
|
||||
if strings.HasPrefix(trimmed, "if ") || strings.HasPrefix(trimmed, "for ") ||
|
||||
strings.HasPrefix(trimmed, "switch ") || strings.HasPrefix(trimmed, "return ") ||
|
||||
strings.HasPrefix(trimmed, "case ") || strings.HasPrefix(trimmed, "default:") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
lineIdx := len(strings.Join(result, "\n")) + 1
|
||||
_ = lineIdx
|
||||
|
||||
transformed := mbaTransformLine(line, rng, skipRanges)
|
||||
result = append(result, transformed)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
func buildSkipRanges(src string) [][2]int {
|
||||
var ranges [][2]int
|
||||
for _, loc := range mbaStringLitRe.FindAllStringIndex(src, -1) {
|
||||
ranges = append(ranges, [2]int{loc[0], loc[1]})
|
||||
}
|
||||
for _, loc := range mbaBacktickRe.FindAllStringIndex(src, -1) {
|
||||
ranges = append(ranges, [2]int{loc[0], loc[1]})
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
func mbaTransformLine(line string, rng *rand.Rand, skipRanges [][2]int) string {
|
||||
// Apply transforms in a specific order to avoid double-transforming.
|
||||
// We transform the most specific patterns first.
|
||||
|
||||
// First, do unary NOT (most specific)
|
||||
line = mbaApplyUnaryNot(line, rng)
|
||||
|
||||
// Then binary ops in order of specificity
|
||||
// XOR and AND are more "bitwise" and less likely to be confused
|
||||
line = mbaApplyBinOp(line, mbaBinXorRe, mbaXorIdentities, rng)
|
||||
line = mbaApplyBinOp(line, mbaBinAndRe, mbaAndIdentities, rng)
|
||||
line = mbaApplyBinOp(line, mbaBinOrRe, mbaOrIdentities, rng)
|
||||
line = mbaApplyBinOp(line, mbaBinAddRe, mbaAddIdentities, rng)
|
||||
line = mbaApplyBinOp(line, mbaBinSubRe, mbaSubIdentities, rng)
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
type mbaIdentity struct {
|
||||
template string
|
||||
// If true, this identity introduces ninline helper that must be injected
|
||||
hasHelper bool
|
||||
helperDef string
|
||||
}
|
||||
|
||||
var mbaAddIdentities = []mbaIdentity{
|
||||
{template: "((%s) ^ (%s)) + 2 * ((%s) & (%s))"},
|
||||
{template: "((%s) | (%s)) + ((%s) & (%s))"},
|
||||
{template: "((%s) ^ (%s)) - ((^(%s)) & (%s)) - ((^(%s)) ^ (%s)) + (^(%s))"},
|
||||
}
|
||||
|
||||
var mbaSubIdentities = []mbaIdentity{
|
||||
{template: "((%s) ^ (%s)) - 2 * ((^(%s)) & (%s))"},
|
||||
{template: "(%s) + (^(%s)) + 1"},
|
||||
}
|
||||
|
||||
var mbaXorIdentities = []mbaIdentity{
|
||||
{template: "((%s) | (%s)) - ((%s) & (%s))"},
|
||||
{template: "((%s) & (^(%s))) | ((^(%s)) & (%s))"},
|
||||
}
|
||||
|
||||
var mbaAndIdentities = []mbaIdentity{
|
||||
{template: "((%s) | (%s)) - ((%s) ^ (%s))"},
|
||||
}
|
||||
|
||||
var mbaOrIdentities = []mbaIdentity{
|
||||
{template: "((%s) ^ (%s)) + ((%s) & (%s))"},
|
||||
{template: "((%s) & (%s)) + ((%s) ^ (%s))"},
|
||||
}
|
||||
|
||||
func mbaApplyBinOp(line string, re *regexp.Regexp, identities []mbaIdentity, rng *rand.Rand) string {
|
||||
// Track positions already transformed to avoid double-transforming
|
||||
var transformed []struct{ start, end int }
|
||||
|
||||
return re.ReplaceAllStringFunc(line, func(match string) string {
|
||||
loc := re.FindStringIndex(match)
|
||||
if loc == nil {
|
||||
return match
|
||||
}
|
||||
|
||||
// Check if this match overlaps with any already-transformed region
|
||||
absStart := strings.Index(line, match)
|
||||
for _, t := range transformed {
|
||||
if absStart >= t.start && absStart < t.end {
|
||||
return match
|
||||
}
|
||||
}
|
||||
|
||||
sub := re.FindStringSubmatch(match)
|
||||
if len(sub) < 3 {
|
||||
return match
|
||||
}
|
||||
a, b := sub[1], sub[2]
|
||||
|
||||
// Don't transform if either operand looks like a comparison result or is empty
|
||||
if a == "" || b == "" {
|
||||
return match
|
||||
}
|
||||
|
||||
// Don't transform pure integer-integer operations (e.g., 2 + 3)
|
||||
// Those are constant-folded by the compiler anyway
|
||||
if isPureNumeric(a) && isPureNumeric(b) {
|
||||
return match
|
||||
}
|
||||
|
||||
ident := identities[rng.Intn(len(identities))]
|
||||
|
||||
var result string
|
||||
switch len(strings.Split(ident.template, "%s")) - 1 {
|
||||
case 2:
|
||||
result = fmt.Sprintf(ident.template, a, b)
|
||||
case 4:
|
||||
result = fmt.Sprintf(ident.template, a, b, a, b)
|
||||
case 6:
|
||||
result = fmt.Sprintf(ident.template, a, b, a, b, a, b)
|
||||
case 7:
|
||||
result = fmt.Sprintf(ident.template, a, b, a, b, a, b, a)
|
||||
default:
|
||||
result = fmt.Sprintf(ident.template, a, b)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
func mbaApplyUnaryNot(line string, rng *rand.Rand) string {
|
||||
// Manual search for ^identifier, checking preceding char
|
||||
var buf strings.Builder
|
||||
i := 0
|
||||
for i < len(line) {
|
||||
if line[i] == '^' && i+1 < len(line) {
|
||||
// Check it's not preceded by identifier char
|
||||
if i > 0 && (isAlphaNum(line[i-1]) || line[i-1] == '_') {
|
||||
buf.WriteByte(line[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// Check next char starts an identifier
|
||||
if !isAlpha(line[i+1]) && line[i+1] != '_' {
|
||||
buf.WriteByte(line[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// Find end of identifier
|
||||
j := i + 1
|
||||
for j < len(line) && (isAlphaNum(line[j]) || line[j] == '_' || line[j] == '.') {
|
||||
j++
|
||||
}
|
||||
a := line[i+1 : j]
|
||||
if isPureNumeric(a) {
|
||||
buf.WriteString(line[i:j])
|
||||
i = j
|
||||
continue
|
||||
}
|
||||
switch rng.Intn(3) {
|
||||
case 0:
|
||||
fmt.Fprintf(&buf, "((-%s) - 1)", a)
|
||||
case 1:
|
||||
fmt.Fprintf(&buf, "(%s ^ (-1))", a)
|
||||
default:
|
||||
fmt.Fprintf(&buf, "(%s ^ 0xFFFFFFFFFFFFFFFF)", a)
|
||||
}
|
||||
i = j
|
||||
} else {
|
||||
buf.WriteByte(line[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func isAlpha(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
||||
}
|
||||
|
||||
func isAlphaNum(b byte) bool {
|
||||
return isAlpha(b) || (b >= '0' && b <= '9')
|
||||
}
|
||||
|
||||
func isPureNumeric(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
_, err := strconv.ParseInt(s, 0, 64)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
_, err = strconv.ParseFloat(s, 64)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func containsAny(s string, patterns []string) bool {
|
||||
for _, p := range patterns {
|
||||
if strings.Contains(s, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InjectMBAHelpers inserts the go:noinline wrapper functions needed by MBA
|
||||
// transformations. Called after all MBA transforms are applied.
|
||||
func InjectMBAHelpers(src string) string {
|
||||
helperName := stringcrypt.RandomIdent("_mba", 5)
|
||||
|
||||
helpers := fmt.Sprintf(`
|
||||
|
||||
//go:noinline
|
||||
func %s_add(a, b int64) int64 { return (a ^ b) + 2*(a&b) }
|
||||
|
||||
//go:noinline
|
||||
func %s_sub(a, b int64) int64 { return (a ^ b) - 2*((^a)&b) }
|
||||
|
||||
//go:noinline
|
||||
func %s_xor(a, b int64) int64 { return (a | b) - (a & b) }
|
||||
|
||||
//go:noinline
|
||||
func %s_and(a, b int64) int64 { return (a | b) - (a ^ b) }
|
||||
|
||||
//go:noinline
|
||||
func %s_or(a, b int64) int64 { return (a ^ b) + (a & b) }
|
||||
|
||||
//go:noinline
|
||||
func %s_not(a int64) int64 { return (-a) - 1 }
|
||||
`, helperName, helperName, helperName, helperName, helperName, helperName)
|
||||
|
||||
insertAt := findImportBlockEnd(src)
|
||||
if insertAt == -1 {
|
||||
pkgIdx := strings.Index(src, "package ")
|
||||
if pkgIdx != -1 {
|
||||
lineEnd := strings.Index(src[pkgIdx:], "\n")
|
||||
if lineEnd != -1 {
|
||||
insertAt = pkgIdx + lineEnd + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if insertAt != -1 && insertAt < len(src) {
|
||||
src = src[:insertAt] + helpers + src[insertAt:]
|
||||
}
|
||||
|
||||
return src
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
// Long string splitting transformation.
|
||||
//
|
||||
// Splits string literals longer than a threshold into concatenated chunks
|
||||
// of randomized length (5-9 characters). This ensures that even if string
|
||||
// encryption is bypassed, long sensitive values like GPG keys, API keys,
|
||||
// and connection strings cannot be found via `strings` on the binary.
|
||||
package transform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
splitMinChunk = 5
|
||||
splitMaxChunk = 9
|
||||
splitMinLen = 20
|
||||
)
|
||||
|
||||
// SplitLongStrings breaks string literals longer than splitMinLen into
|
||||
// concatenated chunks of 5-9 characters each.
|
||||
func SplitLongStrings(src string) string {
|
||||
rng := rand.New(rand.NewSource(rand.Int63()))
|
||||
return splitStringLiterals(src, rng)
|
||||
}
|
||||
|
||||
func splitStringLiterals(src string, rng *rand.Rand) string {
|
||||
var buf strings.Builder
|
||||
buf.Grow(len(src) * 2)
|
||||
|
||||
i := 0
|
||||
inImport := false
|
||||
parenImport := false
|
||||
|
||||
for i < len(src) {
|
||||
// Track import block context
|
||||
rest := src[i:]
|
||||
if !inImport && strings.HasPrefix(rest, "import") {
|
||||
afterImport := strings.TrimSpace(rest[6:])
|
||||
if strings.HasPrefix(afterImport, "(") {
|
||||
inImport = true
|
||||
parenImport = true
|
||||
} else if strings.HasPrefix(afterImport, "\"") {
|
||||
inImport = true
|
||||
parenImport = false
|
||||
}
|
||||
}
|
||||
|
||||
if inImport {
|
||||
if parenImport && src[i] == ')' {
|
||||
inImport = false
|
||||
parenImport = false
|
||||
} else if !parenImport && src[i] == '\n' {
|
||||
inImport = false
|
||||
}
|
||||
buf.WriteByte(src[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if src[i] == '"' {
|
||||
end := findStringEnd(src, i)
|
||||
if end == -1 {
|
||||
buf.WriteByte(src[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
literal := src[i : end+1]
|
||||
inner := literal[1 : len(literal)-1]
|
||||
|
||||
if len(inner) >= splitMinLen && !containsEscapes(inner) {
|
||||
split := splitString(inner, rng)
|
||||
buf.WriteString(split)
|
||||
} else {
|
||||
buf.WriteString(literal)
|
||||
}
|
||||
i = end + 1
|
||||
} else if src[i] == '`' {
|
||||
end := findBacktickEnd(src, i)
|
||||
if end == -1 {
|
||||
buf.WriteByte(src[i])
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
literal := src[i : end+1]
|
||||
inner := literal[1 : len(literal)-1]
|
||||
|
||||
if len(inner) >= splitMinLen {
|
||||
split := splitRawString(inner, rng)
|
||||
buf.WriteString(split)
|
||||
} else {
|
||||
buf.WriteString(literal)
|
||||
}
|
||||
i = end + 1
|
||||
} else {
|
||||
buf.WriteByte(src[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func findStringEnd(src string, start int) int {
|
||||
i := start + 1
|
||||
for i < len(src) {
|
||||
if src[i] == '\\' {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if src[i] == '"' {
|
||||
return i
|
||||
}
|
||||
i++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func findBacktickEnd(src string, start int) int {
|
||||
for i := start + 1; i < len(src); i++ {
|
||||
if src[i] == '`' {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func containsEscapes(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitString(s string, rng *rand.Rand) string {
|
||||
chunks := splitIntoChunks(s, rng)
|
||||
if len(chunks) <= 1 {
|
||||
return `"` + s + `"`
|
||||
}
|
||||
|
||||
var parts []string
|
||||
for _, c := range chunks {
|
||||
parts = append(parts, `"`+escapeString(c)+`"`)
|
||||
}
|
||||
return "(" + strings.Join(parts, " + ") + ")"
|
||||
}
|
||||
|
||||
func splitRawString(s string, rng *rand.Rand) string {
|
||||
chunks := splitIntoChunks(s, rng)
|
||||
if len(chunks) <= 1 {
|
||||
return "`" + s + "`"
|
||||
}
|
||||
|
||||
var parts []string
|
||||
for _, c := range chunks {
|
||||
parts = append(parts, `"`+escapeString(c)+`"`)
|
||||
}
|
||||
return "(" + strings.Join(parts, " + ") + ")"
|
||||
}
|
||||
|
||||
func splitIntoChunks(s string, rng *rand.Rand) []string {
|
||||
runes := []rune(s)
|
||||
if len(runes) < splitMinLen {
|
||||
return []string{s}
|
||||
}
|
||||
|
||||
var chunks []string
|
||||
pos := 0
|
||||
for pos < len(runes) {
|
||||
remaining := len(runes) - pos
|
||||
if remaining <= splitMaxChunk {
|
||||
chunks = append(chunks, string(runes[pos:]))
|
||||
break
|
||||
}
|
||||
|
||||
chunkLen := splitMinChunk + rng.Intn(splitMaxChunk-splitMinChunk+1)
|
||||
|
||||
if remaining-chunkLen < splitMinChunk && remaining-chunkLen > 0 {
|
||||
chunkLen = remaining / 2
|
||||
if chunkLen < splitMinChunk {
|
||||
chunkLen = splitMinChunk
|
||||
}
|
||||
}
|
||||
|
||||
chunks = append(chunks, string(runes[pos:pos+chunkLen]))
|
||||
pos += chunkLen
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
|
||||
func escapeString(s string) string {
|
||||
var buf strings.Builder
|
||||
buf.Grow(len(s))
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\\', '"':
|
||||
buf.WriteRune('\\')
|
||||
buf.WriteRune(r)
|
||||
case '\n':
|
||||
buf.WriteString(`\n`)
|
||||
case '\t':
|
||||
buf.WriteString(`\t`)
|
||||
case '\r':
|
||||
buf.WriteString(`\r`)
|
||||
default:
|
||||
if r < 32 {
|
||||
buf.WriteString(fmt.Sprintf(`\x%02x`, r))
|
||||
} else {
|
||||
buf.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
@@ -88,6 +88,7 @@ func (t *Transformer) TransformFile(srcPath, dstPath string) error {
|
||||
return fmt.Errorf("encrypt strings %s: %w", srcPath, err)
|
||||
}
|
||||
result = convertConstToVar(result)
|
||||
result = SplitLongStrings(result)
|
||||
}
|
||||
|
||||
if t.config.InjectOpaquePreds {
|
||||
@@ -130,6 +131,8 @@ func (t *Transformer) TransformFile(srcPath, dstPath string) error {
|
||||
result = t.injectJunkStrings(result)
|
||||
}
|
||||
|
||||
result = SplitLongStrings(result)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||
return fmt.Errorf("mkdir %s: %w", filepath.Dir(dstPath), err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user