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,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()
|
||||
}
|
||||
Reference in New Issue
Block a user