This commit is contained in:
2026-05-31 19:48:56 +01:00
parent 7f32417736
commit 68f8904282
39 changed files with 4519 additions and 5 deletions
+362
View File
@@ -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
}
+181
View File
@@ -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
}
+294
View File
@@ -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
}
+330
View File
@@ -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()
}
+326
View File
@@ -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
}
+220
View File
@@ -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()
}
+3
View File
@@ -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)
}