Files
go-obfuscation/internal/transform/constblind.go
T
2026-05-31 19:48:56 +01:00

182 lines
4.1 KiB
Go

// 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
}