Files
go-obfuscation/internal/transform/mba.go
T
lukaszraczylo b6c3bacd23 Bug fixes, new transform passes, test coverage, and CI tooling
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.
2026-06-01 14:08:50 +01:00

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
}