mirror of
https://github.com/lukaszraczylo/go-obfuscation.git
synced 2026-10-12 05:32:33 +00:00
Bug fixes: - pclntab/rtcorrupt_linux.go: remove duplicate mprotectRW/mprotectRO that broke GOOS=linux builds (they're defined in rtcorrupt_unsafe.go for darwin+linux). - pclntab/pclntab.go: readUint now guards against negative offset (previously panicked via slice out-of-range). - integrity/integrity.go: SetExpectedHashHex now validates length 64 up-front and reports byte index on parse error; added ClearExpectedHash for runtime toggling and test isolation. - transform/mba.go: mbaApplyBinOp used strings.Index on first match instead of the current match; replaced with FindAllStringSubmatchIndex + two-pass replacement. - transform/transform.go: removed duplicate unconditional SplitLongStrings call. New transform passes (CLI flags + Config fields + pipeline wiring): - splice, constblind, mba, fakesigs, funcsplit, bbreorder — all default true. Cleanup: - strenc/keyderive.go: removed unused DeriveFromBinary + buildSeed. - transform/transform.go: removed unused trimFirstParen. - antidebug_hook_darwin.go: modernized readAddr to unsafe.Slice with noescape pattern (vet-safe). Tests (10 packages, 116 tests, all green): - internal/transform: 28 tests covering every pipeline stage. - pkg/strenc: 7 tests for key derivation + XOR stream. - pkg/stringcrypt: 7 tests for CFB round-trip + tamper detection. - pkg/pageman: 14 tests for alignment + mprotect guards. - pkg/integrity: 14 tests including ClearExpectedHash teardown. - pkg/pclntab: 12 tests including readUint negative-offset guard. - pkg/antisandbox: 6 tests for hostname/MAC lists + Check delegation. - pkg/antiemul: 6 tests (1 skipped on non-Linux) for no-panic + timing. Tooling: - Makefile: added test, verify, lint, fmt, vet targets. vet uses -unsafeptr=false (only legitimate uintptr→pointer is the darwin antidebug hook). fmt skips build/ (generated). - README.md: test coverage table + Makefile target reference.
361 lines
10 KiB
Go
361 lines
10 KiB
Go
// 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 {
|
|
// Use FindAllStringSubmatchIndex so we know the exact position of each match
|
|
// in the line. FindStringIndex on the match alone only gives a position relative
|
|
// to the match, not the line, which previously caused us to mis-attribute
|
|
// already-transformed ranges (the bug: strings.Index(line, match) returns the
|
|
// first occurrence, not the current one).
|
|
type range_ struct{ start, end int }
|
|
var transformed []range_
|
|
|
|
// Mutate the line by recording each replacement and applying in a second pass.
|
|
// We can't use ReplaceAllStringFunc alone because we need the absolute position.
|
|
matches := re.FindAllStringSubmatchIndex(line, -1)
|
|
if len(matches) == 0 {
|
|
return line
|
|
}
|
|
|
|
type repl struct {
|
|
start, end int
|
|
text string
|
|
}
|
|
var repls []repl
|
|
|
|
for _, loc := range matches {
|
|
fullStart, fullEnd := loc[0], loc[1]
|
|
if fullEnd > len(line) {
|
|
continue
|
|
}
|
|
match := line[fullStart:fullEnd]
|
|
|
|
// Overlap with already-transformed region?
|
|
overlap := false
|
|
for _, t := range transformed {
|
|
if fullStart < t.end && fullEnd > t.start {
|
|
overlap = true
|
|
break
|
|
}
|
|
}
|
|
if overlap {
|
|
continue
|
|
}
|
|
|
|
sub := re.FindStringSubmatch(match)
|
|
if len(sub) < 3 {
|
|
continue
|
|
}
|
|
a, b := sub[1], sub[2]
|
|
if a == "" || b == "" {
|
|
continue
|
|
}
|
|
// Constant-folded by the compiler, no point in transforming.
|
|
if isPureNumeric(a) && isPureNumeric(b) {
|
|
continue
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
repls = append(repls, repl{start: fullStart, end: fullEnd, text: result})
|
|
transformed = append(transformed, range_{fullStart, fullEnd})
|
|
}
|
|
|
|
if len(repls) == 0 {
|
|
return line
|
|
}
|
|
|
|
var buf strings.Builder
|
|
buf.Grow(len(line))
|
|
cursor := 0
|
|
for _, r := range repls {
|
|
buf.WriteString(line[cursor:r.start])
|
|
buf.WriteString(r.text)
|
|
cursor = r.end
|
|
}
|
|
buf.WriteString(line[cursor:])
|
|
return buf.String()
|
|
}
|
|
|
|
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
|
|
}
|