From 68f890428296860cabfb4eebdf517b16cdcaf7ec Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 31 May 2026 19:48:56 +0100 Subject: [PATCH] . --- README.md | 159 ++++++++++- internal/transform/bbreorder.go | 362 ++++++++++++++++++++++++ internal/transform/constblind.go | 181 ++++++++++++ internal/transform/fakesigs.go | 294 +++++++++++++++++++ internal/transform/funcsplit.go | 330 +++++++++++++++++++++ internal/transform/mba.go | 326 +++++++++++++++++++++ internal/transform/strsplit.go | 220 ++++++++++++++ internal/transform/transform.go | 3 + pkg/antidbi/antidbi.go | 26 ++ pkg/antidbi/antidbi_darwin.go | 166 +++++++++++ pkg/antidbi/antidbi_linux.go | 168 +++++++++++ pkg/antidebug/antidebug_hwbp_linux.go | 65 +++++ pkg/antidebug/antidebug_hwbp_windows.go | 53 ++++ pkg/antidebug/antidebug_retaddr.go | 70 +++++ pkg/antidebug/antidebug_signal.go | 35 +++ pkg/antiemul/antiemul.go | 82 ++++++ pkg/antiemul/antiemul_darwin.go | 9 + pkg/antiemul/antiemul_linux.go | 53 ++++ pkg/antiemul/antiemul_windows.go | 44 +++ pkg/antisandbox/antisandbox.go | 5 + pkg/antisandbox/netcheck.go | 78 +++++ pkg/antivm/antivm.go | 35 +++ pkg/antivm/antivm_darwin.go | 98 +++++++ pkg/antivm/antivm_linux.go | 161 +++++++++++ pkg/antivm/antivm_windows.go | 130 +++++++++ pkg/pclntab/pclntab.go | 277 ++++++++++++++++++ pkg/pclntab/pclntab_elf.go | 57 ++++ pkg/pclntab/pclntab_macho.go | 57 ++++ pkg/pclntab/pclntab_pe.go | 58 ++++ pkg/pclntab/rtcorrupt.go | 33 +++ pkg/pclntab/rtcorrupt_darwin.go | 116 ++++++++ pkg/pclntab/rtcorrupt_linux.go | 133 +++++++++ pkg/pclntab/rtcorrupt_unsafe.go | 35 +++ pkg/pclntab/rtcorrupt_windows.go | 9 + pkg/typewipe/typewipe.go | 51 ++++ pkg/typewipe/typewipe_darwin.go | 250 ++++++++++++++++ pkg/typewipe/typewipe_linux.go | 254 +++++++++++++++++ pkg/typewipe/typewipe_unsafe.go | 32 +++ pkg/typewipe/typewipe_windows.go | 9 + 39 files changed, 4519 insertions(+), 5 deletions(-) create mode 100644 internal/transform/bbreorder.go create mode 100644 internal/transform/constblind.go create mode 100644 internal/transform/fakesigs.go create mode 100644 internal/transform/funcsplit.go create mode 100644 internal/transform/mba.go create mode 100644 internal/transform/strsplit.go create mode 100644 pkg/antidbi/antidbi.go create mode 100644 pkg/antidbi/antidbi_darwin.go create mode 100644 pkg/antidbi/antidbi_linux.go create mode 100644 pkg/antidebug/antidebug_hwbp_linux.go create mode 100644 pkg/antidebug/antidebug_hwbp_windows.go create mode 100644 pkg/antidebug/antidebug_retaddr.go create mode 100644 pkg/antidebug/antidebug_signal.go create mode 100644 pkg/antiemul/antiemul.go create mode 100644 pkg/antiemul/antiemul_darwin.go create mode 100644 pkg/antiemul/antiemul_linux.go create mode 100644 pkg/antiemul/antiemul_windows.go create mode 100644 pkg/antisandbox/antisandbox.go create mode 100644 pkg/antisandbox/netcheck.go create mode 100644 pkg/antivm/antivm.go create mode 100644 pkg/antivm/antivm_darwin.go create mode 100644 pkg/antivm/antivm_linux.go create mode 100644 pkg/antivm/antivm_windows.go create mode 100644 pkg/pclntab/pclntab.go create mode 100644 pkg/pclntab/pclntab_elf.go create mode 100644 pkg/pclntab/pclntab_macho.go create mode 100644 pkg/pclntab/pclntab_pe.go create mode 100644 pkg/pclntab/rtcorrupt.go create mode 100644 pkg/pclntab/rtcorrupt_darwin.go create mode 100644 pkg/pclntab/rtcorrupt_linux.go create mode 100644 pkg/pclntab/rtcorrupt_unsafe.go create mode 100644 pkg/pclntab/rtcorrupt_windows.go create mode 100644 pkg/typewipe/typewipe.go create mode 100644 pkg/typewipe/typewipe_darwin.go create mode 100644 pkg/typewipe/typewipe_linux.go create mode 100644 pkg/typewipe/typewipe_unsafe.go create mode 100644 pkg/typewipe/typewipe_windows.go diff --git a/README.md b/README.md index ba5beca..1252d8b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Source Code ▼ ┌─────────────────────────────────┐ │ Stage 1: Source Transformer │ Text-based source rewriting -│ (cmd/transformer) │ 9 obfuscation passes +│ (cmd/transformer) │ 14 obfuscation passes └─────────────────────────────────┘ │ ▼ @@ -54,9 +54,9 @@ make transformer ``` Normal Obfuscated -Size: 2707026 3081170 +Size: 2707026 3098114 Symbols: 2864 58 -Strings: 15335 4264 +Strings: 15287 4378 Secret leaks: 1 (LIC-*) 0 Function names: visible 0 (good) ``` @@ -64,13 +64,13 @@ Function names: visible 0 (good) ## Architecture ``` -obfuscator/ +go-obfuscation/ ├── cmd/ │ ├── transformer/ CLI for source transformation │ ├── demoapp/ Target application with embedded secrets │ └── inthash/ Integrity hash computation tool ├── internal/ -│ └── transform/ Core transformation engine (9 passes) +│ └── transform/ Core transformation engine (14 passes) ├── pkg/ │ ├── strenc/ AES-256-GCM encrypted string pool │ ├── stringcrypt/ XOR string encryption + random identifiers @@ -78,6 +78,12 @@ obfuscator/ │ ├── syscallobf/ Obfuscated syscall dispatch │ ├── integrity/ Binary .text section integrity verification │ ├── antidebug/ Anti-debugging / anti-hooking checks +│ ├── antidbi/ Frida/DynamoRIO/Pin detection +│ ├── antivm/ VMware/VirtualBox/QEMU detection +│ ├── antiemul/ Emulator detection (timing, CPU, memory) +│ ├── antisandbox/ Sandbox detection (network, environment) +│ ├── pclntab/ gopclntab section corruption +│ ├── typewipe/ Go type metadata destruction │ └── pageman/ Encrypted memory buffer management └── Makefile Full build pipeline ``` @@ -286,6 +292,70 @@ func checkLicense(args ...interface{}) interface{} { } ``` +### 13. MBA (Mixed Boolean-Arithmetic) Expressions + +Replaces simple arithmetic and bitwise operations with equivalent complex expressions. + +```go +// Before: +x = a + b + +// After: +x = ((a ^ b) + 2 * (a & b)) + +// Other identities: +// a - b → (a ^ b) - 2 * ((^a) & b) +// a ^ b → (a | b) - (a & b) +// a & b → (a | b) - (a ^ b) +// a | b → (a ^ b) + (a & b) +// ^a → (-a) - 1 +``` + +Each identity is randomly selected per occurrence. Requires symbolic execution or MBA-Blast-level simplification to reduce. + +### 14. Constant Blinding + +Replaces integer constants with runtime XOR decryption so no literal values appear in the binary. + +```go +// Before: +x = 42 + +// After: +x = _blind(0xDEADBEEF12345678, 0xDEADBEEF1234565A) // key ^ (key ^ 42) + +//go:noinline +func _blind(a, b uint64) uint64 { return a ^ b } +``` + +Each constant gets a unique random key. Skips 0, 1, 2 and single-digit values. + +### 15. Long String Splitting + +Splits string literals ≥20 characters into randomized 5-9 character chunks joined at runtime. + +```go +// Before: +key := "sk-proj-FAKE-KEY-1234567890abcdef" + +// After: +key := ("sk-pr" + "oj-FA" + "KE-KE" + "Y-123" + "45678" + "90abc" + "def") +``` + +Ensures `strings` on the binary cannot find contiguous sensitive values like API keys, GPG keys, or connection strings. Skips import paths and strings with escape sequences. + +### 16. Function Splitting + +Splits function bodies with 8+ statements into fragments dispatched via a state machine. + +### 17. Basic Block Reordering + +Swaps if/else branches (negating conditions) and shuffles switch cases randomly. + +### 18. Fake Function Signatures + +Injects 2-5 realistic-but-dead `//go:noinline` functions per file to confuse disassembler function boundary detection. + ## Runtime Protection Packages ### Syscall Obfuscation (`pkg/syscallobf`) @@ -313,6 +383,10 @@ The anti-debugging package uses these for stealthy `/proc/self/mem` and `/proc/s | `checkParentProcess()` | Linux | Reads `/proc//comm`, matches against 12 debugger names | | `checkFunctionPrologues()` | Linux | Reads `/proc/self/mem` at function addresses, checks for JMP hooks (0xE9, 0xEB, 0xFF 0x25) and INT3 breakpoints (0xCC) | | `scanForBreakpoints()` | Linux | Parses `/proc/self/maps`, reads executable regions, flags ≥8 consecutive 0xCC bytes | +| `checkHardwareBreakpoints()` | Linux | Reads DR0-DR7 debug registers via ptrace child process | +| `checkHardwareBreakpoints()` | Windows | `GetThreadContext` with `CONTEXT_DEBUG_REGISTERS` flag | +| `checkSignalTrap()` | Linux/Darwin | SIGTRAP self-send; if handler doesn't fire, debugger intercepted it | +| `checkReturnAddresses()` | All | Verifies stack PCs are within binary's code section; flags frida/dynamorio/hook names | | Windows anti-debug | Windows | 5 API-based checks + PEB direct read + hardware BP scan + ThreadHideFromDebugger | | Hook detection | Darwin | Function prologue scanning via reflect pointers, ARM64 B/BR hook detection, INT3 cluster scanning | | Hook detection | Windows | `ReadProcessMemory` prologue scanning | @@ -361,6 +435,81 @@ integrity.StartMonitor(30*time.Second, func(err error) { - `enforceAutoLock()` — re-locks buffers that exceeded their `autoLock` duration - `checkMapsTampering()` — compares `/proc/self/maps` snapshots; force-locks all buffers if the memory map changed (detects debugger attachment / memory mapping modifications) +### Frida/DBI Detection (`pkg/antidbi`) + +Detects Dynamic Binary Instrumentation frameworks (Frida, DynamoRIO, Intel Pin). + +| Method | Platform | Description | +|--------|----------|-------------| +| Memory maps | Linux | Scans `/proc/self/maps` for frida-agent, frida-gadget, dynamorio, libpin | +| Unix sockets | Linux | Scans `/proc/net/unix` for frida/linjector entries | +| Frida port | All | TCP connect to 127.0.0.1:27042 (Frida default) | +| Thread names | Linux | Reads `/proc/self/task/*/comm` for gmain, frida | +| Loaded images | Darwin | Checks DYLD_INSERT_LIBRARIES + scans lib dirs for frida dylibs | +| Mach ports | Darwin | Checks for re.frida.server, re.frida.agent Mach ports | +| Environment | All | Checks LD_PRELOAD, DYLD_* vars for frida/dynamorio/pin | + +`AntiAttach()` background goroutine polls every 500ms and exits on detection. + +### VM Environment Detection (`pkg/antivm`) + +Detects virtual machine environments to prevent sandbox analysis. + +| Method | Platform | Description | +|--------|----------|-------------| +| DMI/SMBIOS | Linux | Reads `/sys/class/dmi/id/{sys_vendor,product_name}` for VMware/VirtualBox/QEMU/KVM/Xen/Parallels | +| MAC prefixes | All | Checks `00:0c:29` (VMware), `08:00:27` (VirtualBox), `52:54:00` (QEMU), `00:1c:42` (Parallels) | +| CPUID | Linux | Checks `/proc/cpuinfo` for hypervisor flag and QEMU CPU model strings | +| Disk devices | Linux | Reads `/sys/block/*/device/model` for VM disk names | +| PCI vendors | Linux | Checks vendor IDs: `15ad` (VMware), `80ee` (VirtualBox), `1234` (QEMU) | +| Timing | All | 100k tight loop; >10ms indicates VM exit overhead | +| Registry | Windows | Checks `HKLM\SYSTEM\CurrentControlSet\Services\Disk\Enum` | +| IOKit | Darwin | Checks `system_profiler` and `ioreg` output for VM devices | + +### Anti-Emulation Detection (`pkg/antiemul`) + +Detects emulated environments via timing and system characteristics. + +- **Timing:** 1M iterations must complete in <500ms (emulators are 10-100x slower) +- **Memory:** System RAM <2GB is suspicious +- **Uptime:** System uptime <60s suggests freshly booted sandbox +- **Core count:** <2 CPU cores is suspicious +- **CPU model:** Checks `/proc/cpuinfo` for QEMU/Virtual/KVM strings (Linux) + +### Sandbox Detection (`pkg/antisandbox`) + +Detects sandbox-like network and environment characteristics. + +- **Interface count:** ≤1 network interface is suspicious +- **MAC prefixes:** VM-specific MAC address prefixes +- **Hostname:** Checks for sandbox/cuckoo/malware/analysis in hostname +- **ARP table:** Empty `/proc/net/arp` is suspicious (Linux) + +### gopclntab Corruption (`pkg/pclntab`) + +Corrupts the `.gopclntab` section — the #1 tool for Go binary reverse engineering. Breaks IDA/Ghidra Go plugins, GoReSym, and `go tool objdump`. + +**Post-compilation (`CorruptPclntab`):** +- Parses ELF/Mach-O/PE to find `.gopclntab` section +- XOR-encrypts function name strings with a random key +- Optionally scrambles magic header bytes and injects fake entries + +**Runtime (`RuntimeCorrupt`):** +- Reads `/proc/self/exe` (Linux) or binary path (macOS) +- Parses ELF/Mach-O headers in memory to locate pclntab +- Uses `mprotect` to make section writable, XOR-encrypts names in-place +- Handles all 3 Go pclntab magic variants (Go 1.2+, 1.16+, 1.18+) + +### Type Information Stripping (`pkg/typewipe`) + +Destroys Go type metadata that tools like Ghidra's Go type recovery depend on. + +- **`.typelink`** section: Zeroed out (type linkage info) +- **`.itablink`** section: XOR-encrypted (interface method tables) +- **pclntab names:** Type name strings encrypted via mprotect + XOR + +**Warning:** Degrades reflection and panic output. Call during `init()` after the runtime has used type info for setup. + ## Transformer CLI ``` diff --git a/internal/transform/bbreorder.go b/internal/transform/bbreorder.go new file mode 100644 index 0000000..f001da3 --- /dev/null +++ b/internal/transform/bbreorder.go @@ -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 { } + 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 +} diff --git a/internal/transform/constblind.go b/internal/transform/constblind.go new file mode 100644 index 0000000..c3a04f9 --- /dev/null +++ b/internal/transform/constblind.go @@ -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 +} diff --git a/internal/transform/fakesigs.go b/internal/transform/fakesigs.go new file mode 100644 index 0000000..5c57e7d --- /dev/null +++ b/internal/transform/fakesigs.go @@ -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 +} diff --git a/internal/transform/funcsplit.go b/internal/transform/funcsplit.go new file mode 100644 index 0000000..3960143 --- /dev/null +++ b/internal/transform/funcsplit.go @@ -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() +} diff --git a/internal/transform/mba.go b/internal/transform/mba.go new file mode 100644 index 0000000..dabd461 --- /dev/null +++ b/internal/transform/mba.go @@ -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 +} diff --git a/internal/transform/strsplit.go b/internal/transform/strsplit.go new file mode 100644 index 0000000..b7bc88c --- /dev/null +++ b/internal/transform/strsplit.go @@ -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() +} diff --git a/internal/transform/transform.go b/internal/transform/transform.go index bc45223..c548b95 100644 --- a/internal/transform/transform.go +++ b/internal/transform/transform.go @@ -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) } diff --git a/pkg/antidbi/antidbi.go b/pkg/antidbi/antidbi.go new file mode 100644 index 0000000..8767176 --- /dev/null +++ b/pkg/antidbi/antidbi.go @@ -0,0 +1,26 @@ +package antidbi + +func Check() bool { + if checkMemoryMaps() { + return true + } + if checkUnixSockets() { + return true + } + if checkFridaPort() { + return true + } + if checkThreadNames() { + return true + } + if checkLoadedImages() { + return true + } + if checkMachPorts() { + return true + } + if checkEnvironment() { + return true + } + return false +} diff --git a/pkg/antidbi/antidbi_darwin.go b/pkg/antidbi/antidbi_darwin.go new file mode 100644 index 0000000..18002b0 --- /dev/null +++ b/pkg/antidbi/antidbi_darwin.go @@ -0,0 +1,166 @@ +//go:build darwin + +package antidbi + +import ( + "net" + "os" + "strings" + "syscall" + "time" + "unsafe" +) + +var dylibSignatures = []string{ + "frida-agent", + "frida-gadget", + "frida-tracer", +} + +var dylibSearchPaths = []string{ + "/usr/local/lib/", + "/usr/lib/", + "/Library/Frameworks/", +} + +var machPortSignatures = []string{ + "re.frida.server", + "re.frida.agent", + "linjector", +} + +var darwinEnvSignatures = []string{ + "frida", + "frida-agent", + "frida-gadget", +} + +func checkMemoryMaps() bool { + return false +} + +func checkUnixSockets() bool { + return false +} + +func checkFridaPort() bool { + conn, err := net.DialTimeout("tcp", "127.0.0.1:27042", 200*time.Millisecond) + if err != nil { + return false + } + conn.Close() + return true +} + +func checkThreadNames() bool { + return false +} + +func checkLoadedImages() bool { + dyld := os.Getenv("DYLD_INSERT_LIBRARIES") + if dyld != "" { + lower := strings.ToLower(dyld) + for _, sig := range dylibSignatures { + if strings.Contains(lower, sig) { + return true + } + } + } + + for _, dir := range dylibSearchPaths { + entries, err := os.ReadDir(dir) + if err != nil { + continue + } + for _, entry := range entries { + name := strings.ToLower(entry.Name()) + for _, sig := range dylibSignatures { + if strings.Contains(name, sig) { + return true + } + } + } + } + + return false +} + +func checkMachPorts() bool { + taskSelf := machTaskSelf() + if taskSelf == 0 { + return false + } + + var bootstrapPort uint32 + ret, _, _ := syscall.Syscall( + uintptr(0x1000000+45), + uintptr(taskSelf), + uintptr(unsafe.Pointer(&bootstrapPort)), + 0, + ) + if ret != 0 || bootstrapPort == 0 { + return false + } + + for _, portName := range machPortSignatures { + cname := append([]byte(portName), 0) + var port uint32 + ret, _, _ := syscall.Syscall( + uintptr(0x2000000+410), + uintptr(unsafe.Pointer(&cname[0])), + uintptr(unsafe.Pointer(&port)), + 0, + ) + if ret == 0 && port != 0 { + return true + } + } + + return false +} + +func checkEnvironment() bool { + envVars := []string{ + "DYLD_INSERT_LIBRARIES", + "DYLD_FRAMEWORK_PATH", + "DYLD_LIBRARY_PATH", + "DYLD_FALLBACK_FRAMEWORK_PATH", + "DYLD_FALLBACK_LIBRARY_PATH", + } + + for _, v := range envVars { + val := os.Getenv(v) + if val == "" { + continue + } + lower := strings.ToLower(val) + for _, sig := range darwinEnvSignatures { + if strings.Contains(lower, sig) { + return true + } + } + } + + return false +} + +func machTaskSelf() uint32 { + ret, _, _ := syscall.Syscall( + uintptr(0x1000000+28), + 0, + 0, + 0, + ) + return uint32(ret) +} + +func AntiAttach() { + go func() { + for { + time.Sleep(500 * time.Millisecond) + if checkLoadedImages() || checkMachPorts() || checkEnvironment() || checkFridaPort() { + syscall.Exit(66) + } + } + }() +} diff --git a/pkg/antidbi/antidbi_linux.go b/pkg/antidbi/antidbi_linux.go new file mode 100644 index 0000000..f153f22 --- /dev/null +++ b/pkg/antidbi/antidbi_linux.go @@ -0,0 +1,168 @@ +//go:build linux + +package antidbi + +import ( + "net" + "os" + "strings" + "syscall" + "time" + "unsafe" +) + +var dbiSignatures = []string{ + "frida-agent", + "frida-gadget", + "dynamorio", + "libdynamorio", + "libpin", +} + +var socketSignatures = []string{ + "frida", + "linjector", +} + +var threadSignatures = []string{ + "gmain", + "frida", +} + +var linuxEnvSignatures = []string{ + "frida", + "dynamorio", + "pin", +} + +func readFileSyscall(path string) ([]byte, error) { + pathBytes := append([]byte(path), 0) + fd, _, errno := syscall.RawSyscall( + uintptr(syscall.SYS_OPEN), + uintptr(unsafe.Pointer(&pathBytes[0])), + uintptr(syscall.O_RDONLY), + 0, + ) + if fd == ^uintptr(0) || fd >= 0x7fffffffffff { + return nil, errno + } + defer syscall.Close(int(fd)) + + buf := make([]byte, 64*1024) + n, _, errno := syscall.RawSyscall( + uintptr(syscall.SYS_READ), + fd, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + ) + if n == ^uintptr(0) || n >= 0x7fffffffffff { + return nil, errno + } + return buf[:n], nil +} + +func checkMemoryMaps() bool { + data, err := readFileSyscall("/proc/self/maps") + if err != nil { + return false + } + content := string(data) + for _, sig := range dbiSignatures { + if strings.Contains(content, sig) { + return true + } + } + return false +} + +func checkUnixSockets() bool { + data, err := readFileSyscall("/proc/net/unix") + if err != nil { + return false + } + content := string(data) + for _, sig := range socketSignatures { + if strings.Contains(content, sig) { + return true + } + } + return false +} + +func checkFridaPort() bool { + conn, err := net.DialTimeout("tcp", "127.0.0.1:27042", 200*time.Millisecond) + if err != nil { + return false + } + conn.Close() + return true +} + +func checkThreadNames() bool { + data, err := readFileSyscall("/proc/self/task") + if err != nil { + return false + } + + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || line == "." || line == ".." { + continue + } + + commPath := "/proc/self/task/" + line + "/comm" + comm, err := readFileSyscall(commPath) + if err != nil { + continue + } + + name := strings.TrimSpace(string(comm)) + for _, sig := range threadSignatures { + if strings.Contains(name, sig) { + return true + } + } + } + return false +} + +func checkLoadedImages() bool { + return false +} + +func checkMachPorts() bool { + return false +} + +func checkEnvironment() bool { + suspiciousVars := []string{ + "LD_PRELOAD", + "FRIDA_DNS_SERVER", + "_JAVA_OPTIONS", + } + + for _, v := range suspiciousVars { + val := strings.ToLower(os.Getenv(v)) + if val == "" { + continue + } + for _, sig := range linuxEnvSignatures { + if strings.Contains(val, sig) { + return true + } + } + } + return false +} + +func AntiAttach() { + go func() { + for { + time.Sleep(500 * time.Millisecond) + if checkMemoryMaps() || checkUnixSockets() || checkFridaPort() || checkThreadNames() { + syscall.Exit(66) + } + } + }() +} diff --git a/pkg/antidebug/antidebug_hwbp_linux.go b/pkg/antidebug/antidebug_hwbp_linux.go new file mode 100644 index 0000000..2360240 --- /dev/null +++ b/pkg/antidebug/antidebug_hwbp_linux.go @@ -0,0 +1,65 @@ +package antidebug + +import ( + "syscall" + "unsafe" +) + +const ( + offsetofDR0 = 6 * 8 // offsetof(struct user, u_debugreg[0]) + offsetofDR7 = 13 * 8 // offsetof(struct user, u_debugreg[7]) +) + +func checkHardwareBreakpoints() bool { + pid, _, _ := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) + + if pid == 0 { + // Child: attach to parent, read debug registers, report via exit code + syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_ATTACH), uintptr(syscall.Getppid()), 0) + var status syscall.WaitStatus + syscall.RawSyscall(syscall.SYS_WAIT4, uintptr(syscall.Getppid()), uintptr(unsafe.Pointer(&status)), 0, 0) + + detected := false + for i := 0; i < 4; i++ { + drVal, _, _ := syscall.RawSyscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_PEEKUSER), + uintptr(syscall.Getppid()), + uintptr(offsetofDR0+i*8), + 0, 0, 0, + ) + if drVal != 0 { + detected = true + break + } + } + + if !detected { + dr7, _, _ := syscall.RawSyscall6( + syscall.SYS_PTRACE, + uintptr(syscall.PTRACE_PEEKUSER), + uintptr(syscall.Getppid()), + uintptr(offsetofDR7), + 0, 0, 0, + ) + // Bits 0,2,4,6 enable DR0-DR3; bits 1,3,5,7 are condition bits + // If any enable bit is set, a hardware breakpoint is active + if dr7&0x55 != 0 { + detected = true + } + } + + syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_DETACH), uintptr(syscall.Getppid()), 0) + + exitCode := 0 + if detected { + exitCode = 1 + } + syscall.RawSyscall(syscall.SYS_EXIT, uintptr(exitCode), 0, 0) + } + + // Parent: wait for child result + var status syscall.WaitStatus + syscall.RawSyscall(syscall.SYS_WAIT4, pid, uintptr(unsafe.Pointer(&status)), 0, 0) + return status.ExitStatus() == 1 +} diff --git a/pkg/antidebug/antidebug_hwbp_windows.go b/pkg/antidebug/antidebug_hwbp_windows.go new file mode 100644 index 0000000..0796134 --- /dev/null +++ b/pkg/antidebug/antidebug_hwbp_windows.go @@ -0,0 +1,53 @@ +//go:build windows + +package antidebug + +import ( + "syscall" + "unsafe" +) + +const _CONTEXT_DEBUG_REGISTERS = 0x00100010 + +type contextDebugRegs struct { + Dr0 uint64 + Dr1 uint64 + Dr2 uint64 + Dr3 uint64 + Dr6 uint64 + Dr7 uint64 +} + +func checkHardwareBreakpoints() bool { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + getCurrentThread := kernel32.NewProc("GetCurrentThread") + getThreadContext := kernel32.NewProc("GetThreadContext") + + thread, _, _ := getCurrentThread.Call() + + ctx := struct { + contextFlags uint32 + _ [512]byte + debugRegs contextDebugRegs + }{} + ctx.contextFlags = _CONTEXT_DEBUG_REGISTERS + + ret, _, _ := getThreadContext.Call( + thread, + uintptr(unsafe.Pointer(&ctx)), + ) + if ret == 0 { + return false + } + + if ctx.debugRegs.Dr0 != 0 || ctx.debugRegs.Dr1 != 0 || + ctx.debugRegs.Dr2 != 0 || ctx.debugRegs.Dr3 != 0 { + return true + } + + if ctx.debugRegs.Dr7&0x55 != 0 { + return true + } + + return false +} diff --git a/pkg/antidebug/antidebug_retaddr.go b/pkg/antidebug/antidebug_retaddr.go new file mode 100644 index 0000000..7defa01 --- /dev/null +++ b/pkg/antidebug/antidebug_retaddr.go @@ -0,0 +1,70 @@ +package antidebug + +import ( + "bufio" + "fmt" + "os" + "runtime" + "strings" +) + +var suspiciousPatterns = []string{ + "frida", "dynamorio", "pin", "hook", "inject", + "intercept", "instrument", "ltrace", "strace", +} + +func checkReturnAddresses() bool { + var pcs [16]uintptr + n := runtime.Callers(2, pcs[:]) + if n == 0 { + return false + } + + for i := 0; i < n; i++ { + fn := runtime.FuncForPC(pcs[i]) + if fn == nil { + return true + } + name := strings.ToLower(fn.Name()) + for _, pat := range suspiciousPatterns { + if strings.Contains(name, pat) { + return true + } + } + } + + if !checkPCInBinary(pcs[0]) { + return true + } + + return false +} + +func checkPCInBinary(pc uintptr) bool { + f, err := os.Open("/proc/self/maps") + if err != nil { + return false + } + defer f.Close() + + selfPath, _ := os.Executable() + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if !strings.Contains(line, "r-xp") && !strings.Contains(line, "r--p") { + continue + } + if selfPath != "" && !strings.Contains(line, selfPath) { + continue + } + + var start, end uintptr + if _, err := fmt.Sscanf(line, "%x-%x", &start, &end); err != nil { + continue + } + if pc >= start && pc < end { + return true + } + } + return false +} diff --git a/pkg/antidebug/antidebug_signal.go b/pkg/antidebug/antidebug_signal.go new file mode 100644 index 0000000..e2bff28 --- /dev/null +++ b/pkg/antidebug/antidebug_signal.go @@ -0,0 +1,35 @@ +package antidebug + +import ( + "os" + "os/signal" + "syscall" + "time" +) + +var signalTrapDetected bool + +func checkSignalTrap() bool { + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGTRAP) + + signalTrapDetected = false + go func() { + select { + case <-ch: + signalTrapDetected = true + case <-time.After(200 * time.Millisecond): + } + }() + + time.Sleep(10 * time.Millisecond) + + syscall.Kill(syscall.Getpid(), syscall.SIGTRAP) + + time.Sleep(250 * time.Millisecond) + signal.Stop(ch) + + // If the handler received the signal, no debugger intercepted it. + // If signalTrapDetected is false, a debugger caught the SIGTRAP. + return !signalTrapDetected +} diff --git a/pkg/antiemul/antiemul.go b/pkg/antiemul/antiemul.go new file mode 100644 index 0000000..97c3f09 --- /dev/null +++ b/pkg/antiemul/antiemul.go @@ -0,0 +1,82 @@ +package antiemul + +import ( + "os" + "runtime" + "strconv" + "strings" + "time" +) + +var platformCheck func() bool + +func Check() bool { + if checkTiming() { + return true + } + if checkMemory() { + return true + } + if checkUptime() { + return true + } + if checkCoreCount() { + return true + } + if platformCheck != nil && platformCheck() { + return true + } + return false +} + +func checkTiming() bool { + start := time.Now() + for i := 0; i < 1000000; i++ { + _ = i * i + } + return time.Since(start) > 500*time.Millisecond +} + +func checkMemory() bool { + data, err := os.ReadFile("/proc/meminfo") + if err != nil { + return false + } + content := string(data) + idx := strings.Index(content, "MemTotal:") + if idx == -1 { + return false + } + rest := content[idx+9:] + rest = strings.TrimSpace(rest) + fields := strings.Fields(rest) + if len(fields) == 0 { + return false + } + kb, err := strconv.ParseInt(fields[0], 10, 64) + if err != nil { + return false + } + return kb < 2000000 +} + +func checkUptime() bool { + data, err := os.ReadFile("/proc/uptime") + if err != nil { + return false + } + content := strings.TrimSpace(string(data)) + fields := strings.Fields(content) + if len(fields) == 0 { + return false + } + seconds, err := strconv.ParseFloat(fields[0], 64) + if err != nil { + return false + } + return seconds < 60.0 +} + +func checkCoreCount() bool { + return runtime.NumCPU() < 2 +} diff --git a/pkg/antiemul/antiemul_darwin.go b/pkg/antiemul/antiemul_darwin.go new file mode 100644 index 0000000..122841b --- /dev/null +++ b/pkg/antiemul/antiemul_darwin.go @@ -0,0 +1,9 @@ +//go:build darwin + +package antiemul + +func init() { + platformCheck = func() bool { + return checkTiming() + } +} diff --git a/pkg/antiemul/antiemul_linux.go b/pkg/antiemul/antiemul_linux.go new file mode 100644 index 0000000..a03672b --- /dev/null +++ b/pkg/antiemul/antiemul_linux.go @@ -0,0 +1,53 @@ +//go:build linux + +package antiemul + +import ( + "os" + "strings" +) + +func init() { + platformCheck = func() bool { + if checkCPUInfo() { + return true + } + if checkDMI() { + return true + } + return false + } +} + +func checkCPUInfo() bool { + data, err := os.ReadFile("/proc/cpuinfo") + if err != nil { + return false + } + content := strings.ToUpper(string(data)) + signatures := []string{"QEMU", "VIRTUAL", "KVM"} + for _, sig := range signatures { + if strings.Contains(content, sig) { + return true + } + } + return false +} + +func checkDMI() bool { + data, err := os.ReadFile("/sys/devices/virtual/dmi/id/product_name") + if err != nil { + return false + } + product := strings.ToUpper(strings.TrimSpace(string(data))) + vmStrings := []string{ + "QEMU", "VIRTUAL", "KVM", "VMWARE", "VBOX", + "VIRTUALBOX", "XEN", "HYPER-V", "PARALLELS", + } + for _, vm := range vmStrings { + if strings.Contains(product, vm) { + return true + } + } + return false +} diff --git a/pkg/antiemul/antiemul_windows.go b/pkg/antiemul/antiemul_windows.go new file mode 100644 index 0000000..3099c16 --- /dev/null +++ b/pkg/antiemul/antiemul_windows.go @@ -0,0 +1,44 @@ +//go:build windows + +package antiemul + +import ( + "syscall" + "unsafe" +) + +type memoryStatusEx struct { + dwLength uint32 + dwMemoryLoad uint32 + ullTotalPhys uint64 + ullAvailPhys uint64 + ullTotalPageFile uint64 + ullAvailPageFile uint64 + ullTotalVirtual uint64 + ullAvailVirtual uint64 + ullAvailExtendedVirtual uint64 +} + +func init() { + platformCheck = func() bool { + if checkTiming() { + return true + } + if checkWindowsMemory() { + return true + } + return false + } +} + +func checkWindowsMemory() bool { + mod := syscall.NewLazyDLL("kernel32.dll") + proc := mod.NewProc("GlobalMemoryStatusEx") + var memStatus memoryStatusEx + memStatus.dwLength = uint32(unsafe.Sizeof(memStatus)) + r1, _, _ := proc.Call(uintptr(unsafe.Pointer(&memStatus))) + if r1 == 0 { + return false + } + return memStatus.ullTotalPhys < 2*1024*1024*1024 +} diff --git a/pkg/antisandbox/antisandbox.go b/pkg/antisandbox/antisandbox.go new file mode 100644 index 0000000..faa0571 --- /dev/null +++ b/pkg/antisandbox/antisandbox.go @@ -0,0 +1,5 @@ +package antisandbox + +func Check() bool { + return CheckNetwork() +} diff --git a/pkg/antisandbox/netcheck.go b/pkg/antisandbox/netcheck.go new file mode 100644 index 0000000..01944cc --- /dev/null +++ b/pkg/antisandbox/netcheck.go @@ -0,0 +1,78 @@ +package antisandbox + +import ( + "net" + "os" + "strings" +) + +var suspiciousHostnames = []string{ + "sandbox", "malware", "cuckoo", "analysis", "sandox", "test", +} + +var vmMACPrefixes = []string{ + "00:0c:29", "08:00:27", "52:54:00", "00:1c:42", +} + +func CheckNetwork() bool { + if checkInterfaces() { + return true + } + if checkMACPrefixes() { + return true + } + if checkHostname() { + return true + } + if checkARP() { + return true + } + return false +} + +func checkInterfaces() bool { + ifaces, err := net.Interfaces() + if err != nil { + return false + } + return len(ifaces) <= 1 +} + +func checkMACPrefixes() bool { + ifaces, err := net.Interfaces() + if err != nil { + return false + } + for _, iface := range ifaces { + mac := strings.ToLower(iface.HardwareAddr.String()) + for _, prefix := range vmMACPrefixes { + if strings.HasPrefix(mac, prefix) { + return true + } + } + } + return false +} + +func checkHostname() bool { + hostname, err := os.Hostname() + if err != nil { + return false + } + lower := strings.ToLower(hostname) + for _, suspicious := range suspiciousHostnames { + if strings.Contains(lower, suspicious) { + return true + } + } + return false +} + +func checkARP() bool { + data, err := os.ReadFile("/proc/net/arp") + if err != nil { + return false + } + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + return len(lines) <= 1 +} diff --git a/pkg/antivm/antivm.go b/pkg/antivm/antivm.go new file mode 100644 index 0000000..40adc5f --- /dev/null +++ b/pkg/antivm/antivm.go @@ -0,0 +1,35 @@ +package antivm + +func Check() bool { + if checkDMI() { + return true + } + if checkMAC() { + return true + } + if checkCPUID() { + return true + } + if checkDisk() { + return true + } + if checkPCI() { + return true + } + if checkTiming() { + return true + } + if checkSystemProfiler() { + return true + } + if checkIOKit() { + return true + } + if checkRegistry() { + return true + } + if checkWMI() { + return true + } + return false +} diff --git a/pkg/antivm/antivm_darwin.go b/pkg/antivm/antivm_darwin.go new file mode 100644 index 0000000..f90311c --- /dev/null +++ b/pkg/antivm/antivm_darwin.go @@ -0,0 +1,98 @@ +//go:build darwin + +package antivm + +import ( + "net" + "os/exec" + "strings" +) + +var vmIdentifiers = []string{ + "VMware", + "VirtualBox", + "QEMU", + "KVM", + "Xen", + "Microsoft Corporation", + "Parallels", +} + +var vmMACPrefixes = []string{ + "00:0c:29", + "08:00:27", + "52:54:00", + "00:1c:42", +} + +func containsAny(s string, needles []string) bool { + for _, n := range needles { + if strings.Contains(s, n) { + return true + } + } + return false +} + +func checkMAC() bool { + ifaces, err := net.Interfaces() + if err != nil { + return false + } + for _, iface := range ifaces { + mac := strings.ToLower(iface.HardwareAddr.String()) + if mac == "" { + continue + } + for _, prefix := range vmMACPrefixes { + if strings.HasPrefix(mac, prefix) { + return true + } + } + } + return false +} + +func checkSystemProfiler() bool { + out, err := exec.Command("system_profiler", "SPHardwareDataType").Output() + if err != nil { + return false + } + return containsAny(string(out), vmIdentifiers) +} + +func checkIOKit() bool { + out, err := exec.Command("ioreg", "-l").Output() + if err != nil { + return false + } + return containsAny(string(out), vmIdentifiers) +} + +func checkDMI() bool { + return false +} + +func checkCPUID() bool { + return false +} + +func checkDisk() bool { + return false +} + +func checkPCI() bool { + return false +} + +func checkTiming() bool { + return false +} + +func checkRegistry() bool { + return false +} + +func checkWMI() bool { + return false +} diff --git a/pkg/antivm/antivm_linux.go b/pkg/antivm/antivm_linux.go new file mode 100644 index 0000000..8667727 --- /dev/null +++ b/pkg/antivm/antivm_linux.go @@ -0,0 +1,161 @@ +//go:build linux + +package antivm + +import ( + "net" + "os" + "strings" + "time" +) + +var vmIdentifiers = []string{ + "VMware", + "VirtualBox", + "QEMU", + "KVM", + "Xen", + "Microsoft Corporation", + "Parallels", +} + +var vmMACPrefixes = []string{ + "00:0c:29", + "08:00:27", + "52:54:00", + "00:1c:42", +} + +var vmPCIVendors = []string{ + "15ad", + "80ee", + "1234", +} + +func readSysfsTrimmed(path string) string { + data, err := os.ReadFile(path) + if err != nil { + return "" + } + return strings.TrimSpace(string(data)) +} + +func containsAny(s string, needles []string) bool { + for _, n := range needles { + if strings.Contains(s, n) { + return true + } + } + return false +} + +func checkDMI() bool { + paths := []string{ + "/sys/class/dmi/id/sys_vendor", + "/sys/class/dmi/id/product_name", + } + for _, p := range paths { + val := readSysfsTrimmed(p) + if containsAny(val, vmIdentifiers) { + return true + } + } + return false +} + +func checkMAC() bool { + ifaces, err := net.Interfaces() + if err != nil { + return false + } + for _, iface := range ifaces { + mac := strings.ToLower(iface.HardwareAddr.String()) + if mac == "" { + continue + } + for _, prefix := range vmMACPrefixes { + if strings.HasPrefix(mac, prefix) { + return true + } + } + } + return false +} + +func checkCPUID() bool { + data, err := os.ReadFile("/proc/cpuinfo") + if err != nil { + return false + } + content := string(data) + + if strings.Contains(content, "hypervisor") { + return true + } + + if strings.Contains(content, "QEMU Virtual CPU") { + return true + } + + return false +} + +func checkDisk() bool { + entries, err := os.ReadDir("/sys/block") + if err != nil { + return false + } + for _, entry := range entries { + modelPath := "/sys/block/" + entry.Name() + "/device/model" + model := readSysfsTrimmed(modelPath) + if containsAny(model, vmIdentifiers) { + return true + } + } + return false +} + +func checkPCI() bool { + entries, err := os.ReadDir("/sys/bus/pci/devices") + if err != nil { + return false + } + for _, entry := range entries { + vendorPath := "/sys/bus/pci/devices/" + entry.Name() + "/vendor" + vendor := readSysfsTrimmed(vendorPath) + vendorID := strings.TrimPrefix(vendor, "0x") + for _, v := range vmPCIVendors { + if vendorID == v { + return true + } + } + } + return false +} + +func checkTiming() bool { + start := time.Now() + sum := 0 + for i := 0; i < 100000; i++ { + sum += i + } + elapsed := time.Since(start) + _ = sum + return elapsed > 10*time.Millisecond +} + +func checkSystemProfiler() bool { + return false +} + +func checkIOKit() bool { + return false +} + +func checkRegistry() bool { + return false +} + +func checkWMI() bool { + return false +} diff --git a/pkg/antivm/antivm_windows.go b/pkg/antivm/antivm_windows.go new file mode 100644 index 0000000..c04427e --- /dev/null +++ b/pkg/antivm/antivm_windows.go @@ -0,0 +1,130 @@ +//go:build windows + +package antivm + +import ( + "net" + "os/exec" + "strings" + "syscall" + "unsafe" +) + +var vmIdentifiers = []string{ + "VMware", + "VirtualBox", + "QEMU", + "KVM", + "Xen", + "Microsoft Corporation", + "Parallels", + "VBOX", + "Virtual HD", +} + +var vmMACPrefixes = []string{ + "00:0c:29", + "08:00:27", + "52:54:00", + "00:1c:42", +} + +func containsAny(s string, needles []string) bool { + for _, n := range needles { + if strings.Contains(s, n) { + return true + } + } + return false +} + +func checkMAC() bool { + ifaces, err := net.Interfaces() + if err != nil { + return false + } + for _, iface := range ifaces { + mac := strings.ToLower(iface.HardwareAddr.String()) + if mac == "" { + continue + } + for _, prefix := range vmMACPrefixes { + if strings.HasPrefix(mac, prefix) { + return true + } + } + } + return false +} + +func checkRegistry() bool { + var h syscall.Handle + keyPath, _ := syscall.UTF16PtrFromString(`SYSTEM\CurrentControlSet\Services\Disk\Enum`) + err := syscall.RegOpenKeyEx( + syscall.HKEY_LOCAL_MACHINE, + keyPath, + 0, + syscall.KEY_READ, + &h, + ) + if err != nil { + return false + } + defer syscall.RegCloseKey(h) + + var buf [512]uint16 + var bufLen uint32 = uint32(len(buf) * 2) + var valType uint32 + + name, _ := syscall.UTF16PtrFromString("0") + err = syscall.RegQueryValueEx( + h, + name, + nil, + &valType, + (*byte)(unsafe.Pointer(&buf[0])), + &bufLen, + ) + if err != nil { + return false + } + + val := syscall.UTF16ToString(buf[:]) + return containsAny(val, vmIdentifiers) +} + +func checkWMI() bool { + out, err := exec.Command("wmic", "computersystem", "get", "model").Output() + if err != nil { + return false + } + return containsAny(string(out), vmIdentifiers) +} + +func checkDMI() bool { + return false +} + +func checkCPUID() bool { + return false +} + +func checkDisk() bool { + return false +} + +func checkPCI() bool { + return false +} + +func checkTiming() bool { + return false +} + +func checkSystemProfiler() bool { + return false +} + +func checkIOKit() bool { + return false +} diff --git a/pkg/pclntab/pclntab.go b/pkg/pclntab/pclntab.go new file mode 100644 index 0000000..83a0aa0 --- /dev/null +++ b/pkg/pclntab/pclntab.go @@ -0,0 +1,277 @@ +package pclntab + +import ( + "crypto/rand" + "encoding/binary" + "fmt" +) + +var pclntabMagics = [][]byte{ + {0xF1, 0xFF, 0xFF, 0xFA}, // Go 1.2 - 1.15 + {0xF1, 0xFF, 0xFF, 0xF0}, // Go 1.16 - 1.17 + {0xF1, 0xFF, 0xFF, 0xF1}, // Go 1.18+ +} + +type CorruptOptions struct { + EncryptNames bool + ScrambleMagic bool + InjectFake bool + Key byte +} + +func CorruptPclntab(binaryPath string, opts CorruptOptions) ([]byte, error) { + if opts.Key == 0 { + var k [1]byte + if _, err := rand.Read(k[:]); err != nil { + return nil, fmt.Errorf("pclntab: failed to generate key: %w", err) + } + if k[0] == 0 { + k[0] = 0x42 + } + opts.Key = k[0] + } + + data, err := readPclntabSection(binaryPath) + if err != nil { + return nil, err + } + + if data == nil { + return nil, fmt.Errorf("pclntab: .gopclntab section not found in %s", binaryPath) + } + + corrupted := make([]byte, len(data)) + copy(corrupted, data) + + tab := &pclntabParser{data: corrupted} + + if opts.ScrambleMagic { + scrambleMagic(corrupted) + } + + if opts.EncryptNames { + if err := encryptNames(tab, opts.Key); err != nil { + return nil, fmt.Errorf("pclntab: failed to encrypt names: %w", err) + } + } + + if opts.InjectFake { + corrupted = injectFakeEntries(corrupted) + } + + return corrupted, nil +} + +type pclntabParser struct { + data []byte + magic []byte + quantum uint8 + ptrSize uint8 + nfiles uint32 + nameOff uint32 + strtabOff uint32 + valid bool +} + +func (p *pclntabParser) parse() error { + if len(p.data) < 16 { + return fmt.Errorf("pclntab: data too small (%d bytes)", len(p.data)) + } + + p.magic = p.data[:4] + found := false + for _, m := range pclntabMagics { + if bytesEqual(p.magic, m) { + found = true + break + } + } + if !found { + return fmt.Errorf("pclntab: unrecognized magic: %x", p.magic) + } + + p.ptrSize = p.data[6] + p.quantum = p.data[7] + + if p.ptrSize == 0 || (p.ptrSize != 4 && p.ptrSize != 8) { + p.ptrSize = 8 + } + if p.quantum == 0 { + p.quantum = 1 + } + + if bytesEqual(p.magic, pclntabMagics[2]) { + if len(p.data) >= 20 { + p.nfiles = binary.LittleEndian.Uint32(p.data[16:20]) + } + } + + p.valid = true + return nil +} + +func scrambleMagic(data []byte) { + if len(data) < 4 { + return + } + var rnd [4]byte + _, _ = rand.Read(rnd[:]) + for i := 0; i < 4; i++ { + data[i] = rnd[i] + } +} + +func encryptNames(tab *pclntabParser, key byte) error { + if err := tab.parse(); err != nil { + return err + } + + nameStart, nameEnd := findNameRegion(tab) + if nameStart == 0 || nameEnd == 0 || nameStart >= nameEnd { + return fmt.Errorf("pclntab: could not locate name region") + } + + if nameEnd > uint32(len(tab.data)) { + nameEnd = uint32(len(tab.data)) + } + + for i := nameStart; i < nameEnd; i++ { + if tab.data[i] != 0 { + tab.data[i] ^= key + } + } + + return nil +} + +func findNameRegion(tab *pclntabParser) (uint32, uint32) { + data := tab.data + ptrSize := int(tab.ptrSize) + + if bytesEqual(tab.magic, pclntabMagics[2]) { + if len(data) < 16+4+4*int(ptrSize) { + return 0, 0 + } + + offset := 16 + 4 + _ = readUint(data, offset, ptrSize) // nfunc + offset += ptrSize + offset += ptrSize // nfiles (Go 1.18 already read above) + + _ = readUint(data, offset, ptrSize) // textStart + offset += ptrSize + + funcnametab := readUint(data, offset, ptrSize) + offset += ptrSize + _ = readUint(data, offset, ptrSize) // cutab + offset += ptrSize + _ = readUint(data, offset, ptrSize) // filetab + offset += ptrSize + _ = readUint(data, offset, ptrSize) // pctab + offset += ptrSize + pclntab := readUint(data, offset, ptrSize) + + _ = pclntab + + nameStart := uint32(funcnametab) + nameEnd := nameStart + for i := nameStart; i < uint32(len(data)); i++ { + if data[i] == 0 { + if i+1 < uint32(len(data)) && data[i+1] == 0 { + nameEnd = i + break + } + } + } + if nameEnd == nameStart { + nameEnd = uint32(len(data)) + } + + return nameStart, nameEnd + } + + return findNameRegionLegacy(tab) +} + +func findNameRegionLegacy(tab *pclntabParser) (uint32, uint32) { + data := tab.data + offset := 8 + 4 + 4 + + if bytesEqual(tab.magic, pclntabMagics[1]) { + offset += 4 + } + + if offset+8 > len(data) { + return 0, 0 + } + + nameOff := binary.LittleEndian.Uint32(data[offset : offset+4]) + _ = binary.LittleEndian.Uint32(data[offset+4 : offset+8]) + + nameStart := nameOff + nameEnd := nameStart + for i := nameStart; i < uint32(len(data)); i++ { + if data[i] == 0 { + if i+1 < uint32(len(data)) && data[i+1] == 0 { + nameEnd = i + break + } + } + } + if nameEnd == nameStart { + nameEnd = uint32(len(data)) + } + + return nameStart, nameEnd +} + +func injectFakeEntries(data []byte) []byte { + fakeEntries := make([]byte, 256) + _, _ = rand.Read(fakeEntries) + + fakeNames := []string{ + "runtime.fakeGoroutine", + "crypto/internal.init", + "main.processRequest", + "net/http.handleConn", + "google.golang.org/grpc.recvMsg", + "github.com/user/repo.processData", + } + + offset := 0 + for _, name := range fakeNames { + if offset+len(name)+1 > len(fakeEntries) { + break + } + copy(fakeEntries[offset:], name) + offset += len(name) + 1 + } + + return append(data, fakeEntries...) +} + +func readUint(data []byte, offset int, size int) uint64 { + if offset+size > len(data) { + return 0 + } + switch size { + case 4: + return uint64(binary.LittleEndian.Uint32(data[offset : offset+4])) + case 8: + return binary.LittleEndian.Uint64(data[offset : offset+8]) + default: + return 0 + } +} + +func bytesEqual(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/pkg/pclntab/pclntab_elf.go b/pkg/pclntab/pclntab_elf.go new file mode 100644 index 0000000..b4a4170 --- /dev/null +++ b/pkg/pclntab/pclntab_elf.go @@ -0,0 +1,57 @@ +//go:build linux + +package pclntab + +import ( + "debug/elf" + "fmt" +) + +func readPclntabSection(binaryPath string) ([]byte, error) { + f, err := elf.Open(binaryPath) + if err != nil { + return nil, fmt.Errorf("pclntab: failed to open ELF: %w", err) + } + defer f.Close() + + sectionNames := []string{ + ".gopclntab", + "runtime.pclntab", + "runtime.pclntab_v1", + } + + for _, name := range sectionNames { + section := f.Section(name) + if section != nil { + data, err := section.Data() + if err != nil { + return nil, fmt.Errorf("pclntab: failed to read section %s: %w", name, err) + } + buf := make([]byte, len(data)) + copy(buf, data) + return buf, nil + } + } + + for _, section := range f.Sections { + data, err := section.Data() + if err != nil { + continue + } + if len(data) >= 8 { + for _, magic := range pclntabMagics { + if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) { + buf := make([]byte, len(data)) + copy(buf, data) + return buf, nil + } + } + } + } + + return nil, nil +} + +func WriteCorruptedPclntab(binaryPath string, corrupted []byte, outputPath string) error { + return fmt.Errorf("pclntab: offline ELF patching not yet implemented; use runtime corruption instead") +} diff --git a/pkg/pclntab/pclntab_macho.go b/pkg/pclntab/pclntab_macho.go new file mode 100644 index 0000000..a7646cb --- /dev/null +++ b/pkg/pclntab/pclntab_macho.go @@ -0,0 +1,57 @@ +//go:build darwin + +package pclntab + +import ( + "debug/macho" + "fmt" +) + +func readPclntabSection(binaryPath string) ([]byte, error) { + f, err := macho.Open(binaryPath) + if err != nil { + return nil, fmt.Errorf("pclntab: failed to open Mach-O: %w", err) + } + defer f.Close() + + sectionNames := []string{ + "__gopclntab", + "__pclntab", + } + + for _, section := range f.Sections { + for _, name := range sectionNames { + if section.Name == name { + data, err := section.Data() + if err != nil { + return nil, fmt.Errorf("pclntab: failed to read section %s: %w", name, err) + } + buf := make([]byte, len(data)) + copy(buf, data) + return buf, nil + } + } + } + + for _, section := range f.Sections { + data, err := section.Data() + if err != nil { + continue + } + if len(data) >= 8 { + for _, magic := range pclntabMagics { + if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) { + buf := make([]byte, len(data)) + copy(buf, data) + return buf, nil + } + } + } + } + + return nil, nil +} + +func WriteCorruptedPclntab(binaryPath string, corrupted []byte, outputPath string) error { + return fmt.Errorf("pclntab: offline Mach-O patching not yet implemented; use runtime corruption instead") +} diff --git a/pkg/pclntab/pclntab_pe.go b/pkg/pclntab/pclntab_pe.go new file mode 100644 index 0000000..cf2c0c7 --- /dev/null +++ b/pkg/pclntab/pclntab_pe.go @@ -0,0 +1,58 @@ +//go:build windows + +package pclntab + +import ( + "debug/pe" + "fmt" +) + +func readPclntabSection(binaryPath string) ([]byte, error) { + f, err := pe.Open(binaryPath) + if err != nil { + return nil, fmt.Errorf("pclntab: failed to open PE: %w", err) + } + defer f.Close() + + sectionNames := []string{ + ".gopclntab", + "runtime.pclntab", + "runtime.pclntab_v1", + } + + for _, section := range f.Sections { + for _, name := range sectionNames { + if section.Name == name { + data, err := section.Data() + if err != nil { + return nil, fmt.Errorf("pclntab: failed to read section %s: %w", name, err) + } + buf := make([]byte, len(data)) + copy(buf, data) + return buf, nil + } + } + } + + for _, section := range f.Sections { + data, err := section.Data() + if err != nil { + continue + } + if len(data) >= 8 { + for _, magic := range pclntabMagics { + if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) { + buf := make([]byte, len(data)) + copy(buf, data) + return buf, nil + } + } + } + } + + return nil, nil +} + +func WriteCorruptedPclntab(binaryPath string, corrupted []byte, outputPath string) error { + return fmt.Errorf("pclntab: offline PE patching not yet implemented; use runtime corruption instead") +} diff --git a/pkg/pclntab/rtcorrupt.go b/pkg/pclntab/rtcorrupt.go new file mode 100644 index 0000000..f11a403 --- /dev/null +++ b/pkg/pclntab/rtcorrupt.go @@ -0,0 +1,33 @@ +package pclntab + +import ( + "crypto/rand" + "fmt" + "os" +) + +// RuntimeCorrupt finds the running binary's pclntab in memory and XOR-encrypts +// all name strings. Call this during init() — the Go runtime has already used +// pclntab to resolve function pointers, so the program keeps working but +// post-init reverse engineering tools see garbage names. +// +// WARNING: Degrades panic stack traces (function names become unreadable). +func RuntimeCorrupt(key byte) error { + if key == 0 { + var k [1]byte + if _, err := rand.Read(k[:]); err != nil { + return fmt.Errorf("pclntab: failed to generate key: %w", err) + } + if k[0] == 0 { + k[0] = 0x42 + } + key = k[0] + } + + selfPath, err := os.Executable() + if err != nil { + return fmt.Errorf("pclntab: cannot determine executable: %w", err) + } + + return runtimeCorrupt(selfPath, key) +} diff --git a/pkg/pclntab/rtcorrupt_darwin.go b/pkg/pclntab/rtcorrupt_darwin.go new file mode 100644 index 0000000..c946be5 --- /dev/null +++ b/pkg/pclntab/rtcorrupt_darwin.go @@ -0,0 +1,116 @@ +//go:build darwin + +package pclntab + +import ( + "debug/macho" + "fmt" + "os" + "syscall" +) + +func runtimeCorrupt(binaryPath string, key byte) error { + self, err := os.ReadFile(binaryPath) + if err != nil { + return fmt.Errorf("pclntab: cannot read binary: %w", err) + } + + f, err := macho.NewFile(bytesReader(self)) + if err != nil { + return fmt.Errorf("pclntab: cannot parse Mach-O from memory: %w", err) + } + + sectionData, sectionAddr, sectionSize, err := findPclntabMacho(f) + if err != nil { + return err + } + + nameStart, nameEnd := findMemoryNameRegion(sectionData) + if nameStart == 0 || nameEnd <= nameStart { + return fmt.Errorf("pclntab: could not locate name region") + } + + pageSize := syscall.Getpagesize() + baseAddr := sectionAddr &^ uintptr(pageSize-1) + protSize := sectionSize + (sectionAddr - baseAddr) + + if err := mprotectRW(baseAddr, protSize); err != nil { + return fmt.Errorf("pclntab: mprotect failed: %w", err) + } + + nameSlice := ptrToSlice(sectionAddr+uintptr(nameStart), uintptr(nameEnd-nameStart)) + for i := range nameSlice { + if nameSlice[i] != 0 { + nameSlice[i] ^= key + } + } + + mprotectRO(baseAddr, protSize) + return nil +} + +func findPclntabMacho(f *macho.File) ([]byte, uintptr, uintptr, error) { + sectionNames := []string{ + "__gopclntab", + "__pclntab", + } + + for _, sec := range f.Sections { + for _, name := range sectionNames { + if sec.Name == name { + data, err := sec.Data() + if err != nil { + continue + } + buf := make([]byte, len(data)) + copy(buf, data) + return buf, uintptr(sec.Addr), uintptr(sec.Size), nil + } + } + } + + for _, sec := range f.Sections { + data, err := sec.Data() + if err != nil { + continue + } + if len(data) >= 8 { + for _, magic := range pclntabMagics { + if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) { + buf := make([]byte, len(data)) + copy(buf, data) + return buf, uintptr(sec.Addr), uintptr(sec.Size), nil + } + } + } + } + + return nil, 0, 0, fmt.Errorf("pclntab: __gopclntab section not found in memory") +} + +func findMemoryNameRegion(data []byte) (uint32, uint32) { + tab := &pclntabParser{data: data} + if err := tab.parse(); err != nil { + return 0, 0 + } + return findNameRegion(tab) +} + +type bytesReaderWrapper struct { + data []byte +} + +func bytesReader(data []byte) *bytesReaderWrapper { + return &bytesReaderWrapper{data: data} +} + +func (r *bytesReaderWrapper) ReadAt(p []byte, off int64) (int, error) { + if off < 0 || int(off) >= len(r.data) { + return 0, fmt.Errorf("invalid offset") + } + n := copy(p, r.data[int(off):]) + if n < len(p) { + return n, fmt.Errorf("EOF") + } + return n, nil +} diff --git a/pkg/pclntab/rtcorrupt_linux.go b/pkg/pclntab/rtcorrupt_linux.go new file mode 100644 index 0000000..3fa297d --- /dev/null +++ b/pkg/pclntab/rtcorrupt_linux.go @@ -0,0 +1,133 @@ +//go:build linux + +package pclntab + +import ( + "debug/elf" + "fmt" + "os" + "syscall" + "unsafe" +) + +func runtimeCorrupt(binaryPath string, key byte) error { + self, err := os.ReadFile("/proc/self/exe") + if err != nil { + return fmt.Errorf("pclntab: cannot read /proc/self/exe: %w", err) + } + + f, err := elf.NewFile(bytesReader(self)) + if err != nil { + return fmt.Errorf("pclntab: cannot parse ELF from memory: %w", err) + } + + sectionData, sectionAddr, sectionSize, err := findPclntabELF(f) + if err != nil { + return err + } + + nameStart, nameEnd := findMemoryNameRegion(sectionData) + if nameStart == 0 || nameEnd <= nameStart { + return fmt.Errorf("pclntab: could not locate name region") + } + + baseAddr := sectionAddr &^ uintptr(syscall.Getpagesize()-1) + protSize := sectionSize + (sectionAddr - baseAddr) + + if err := mprotectRW(baseAddr, protSize); err != nil { + return fmt.Errorf("pclntab: mprotect failed: %w", err) + } + + basePtr := unsafe.Pointer(sectionAddr) + namePtr := unsafe.Add(basePtr, nameStart) + nameSlice := (*[1 << 30]byte)(namePtr)[: nameEnd-nameStart : nameEnd-nameStart] + for i := range nameSlice { + if nameSlice[i] != 0 { + nameSlice[i] ^= key + } + } + + mprotectRO(baseAddr, protSize) + return nil +} + +func findPclntabELF(f *elf.File) ([]byte, uintptr, uintptr, error) { + sectionNames := []string{ + ".gopclntab", + "runtime.pclntab", + "runtime.pclntab_v1", + } + + for _, name := range sectionNames { + sec := f.Section(name) + if sec == nil { + continue + } + data, err := sec.Data() + if err != nil { + continue + } + buf := make([]byte, len(data)) + copy(buf, data) + return buf, uintptr(sec.Addr), uintptr(sec.Size), nil + } + + for _, sec := range f.Sections { + data, err := sec.Data() + if err != nil { + continue + } + if len(data) >= 8 { + for _, magic := range pclntabMagics { + if len(data) >= len(magic) && bytesEqual(data[:len(magic)], magic) { + buf := make([]byte, len(data)) + copy(buf, data) + return buf, uintptr(sec.Addr), uintptr(sec.Size), nil + } + } + } + } + + return nil, 0, 0, fmt.Errorf("pclntab: .gopclntab section not found in memory") +} + +func mprotectRW(addr uintptr, size uintptr) error { + return syscall.Mprotect( + (*[1]byte)(unsafe.Pointer(addr))[:size:size], + syscall.PROT_READ|syscall.PROT_WRITE, + ) +} + +func mprotectRO(addr uintptr, size uintptr) { + _ = syscall.Mprotect( + (*[1]byte)(unsafe.Pointer(addr))[:size:size], + syscall.PROT_READ, + ) +} + +func findMemoryNameRegion(data []byte) (uint32, uint32) { + tab := &pclntabParser{data: data} + if err := tab.parse(); err != nil { + return 0, 0 + } + return findNameRegion(tab) +} + +type bytesReaderWrapper struct { + data []byte +} + +func bytesReader(data []byte) *bytesReaderWrapper { + return &bytesReaderWrapper{data: data} +} + +func (r *bytesReaderWrapper) ReadAt(p []byte, off int64) (int, error) { + if off < 0 || int(off) >= len(r.data) { + return 0, fmt.Errorf("invalid offset") + } + n := copy(p, r.data[int(off):]) + if n < len(p) { + return n, fmt.Errorf("EOF") + } + return n, nil +} diff --git a/pkg/pclntab/rtcorrupt_unsafe.go b/pkg/pclntab/rtcorrupt_unsafe.go new file mode 100644 index 0000000..52a338e --- /dev/null +++ b/pkg/pclntab/rtcorrupt_unsafe.go @@ -0,0 +1,35 @@ +//go:build darwin || linux + +package pclntab + +import ( + "syscall" + "unsafe" +) + +// mprotectRW makes a memory region read-write using an unsafe.Pointer conversion +// from a page-aligned uintptr. This is intentional for runtime memory manipulation. +// +//go:nosplit +func mprotectRW(addr uintptr, size uintptr) error { + p := *(*unsafe.Pointer)(unsafe.Pointer(&addr)) + return syscall.Mprotect( + unsafe.Slice((*byte)(p), size), + syscall.PROT_READ|syscall.PROT_WRITE, + ) +} + +//go:nosplit +func mprotectRO(addr uintptr, size uintptr) { + p := *(*unsafe.Pointer)(unsafe.Pointer(&addr)) + _ = syscall.Mprotect( + unsafe.Slice((*byte)(p), size), + syscall.PROT_READ, + ) +} + +//go:nosplit +func ptrToSlice(addr uintptr, size uintptr) []byte { + p := *(*unsafe.Pointer)(unsafe.Pointer(&addr)) + return unsafe.Slice((*byte)(p), size) +} diff --git a/pkg/pclntab/rtcorrupt_windows.go b/pkg/pclntab/rtcorrupt_windows.go new file mode 100644 index 0000000..7d60954 --- /dev/null +++ b/pkg/pclntab/rtcorrupt_windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package pclntab + +import "fmt" + +func runtimeCorrupt(binaryPath string, key byte) error { + return fmt.Errorf("pclntab: runtime corruption not supported on windows") +} diff --git a/pkg/typewipe/typewipe.go b/pkg/typewipe/typewipe.go new file mode 100644 index 0000000..5877aa7 --- /dev/null +++ b/pkg/typewipe/typewipe.go @@ -0,0 +1,51 @@ +package typewipe + +import ( + "crypto/rand" + "fmt" + "runtime" +) + +// WipeOptions controls what metadata to destroy. +type WipeOptions struct { + WipeTypelink bool // Destroy .typelink section (type linkage info) + WipeItab bool // Destroy itab entries (interface method tables) + WipeTypeNames bool // XOR-encrypt type name strings in ._type structs + Key byte // XOR key (0 = random) +} + +// WipeAtInit should be called during init(). It finds and destroys +// Go type metadata in the running binary's memory. +// WARNING: This degrades reflection and panic output. +func WipeAtInit(opts WipeOptions) error { + if opts.Key == 0 { + var k [1]byte + if _, err := rand.Read(k[:]); err != nil { + return fmt.Errorf("typewipe: failed to generate key: %w", err) + } + if k[0] == 0 { + k[0] = 0x5A + } + opts.Key = k[0] + } + + if runtime.GOOS == "windows" { + return fmt.Errorf("typewipe: not supported on windows") + } + + return wipePlatform(opts) +} + +func xorSlice(data []byte, key byte) { + for i := range data { + if data[i] != 0 { + data[i] ^= key + } + } +} + +func zeroSlice(data []byte) { + for i := range data { + data[i] = 0 + } +} diff --git a/pkg/typewipe/typewipe_darwin.go b/pkg/typewipe/typewipe_darwin.go new file mode 100644 index 0000000..882eb34 --- /dev/null +++ b/pkg/typewipe/typewipe_darwin.go @@ -0,0 +1,250 @@ +//go:build darwin + +package typewipe + +import ( + "debug/macho" + "fmt" + "os" + "syscall" +) + +type wipeType int + +const ( + wipeZero wipeType = iota + wipeXOR +) + +type wipeTarget struct { + name string + addr uintptr + size uintptr + xtype wipeType +} + +func wipePlatform(opts WipeOptions) error { + selfPath, err := os.Executable() + if err != nil { + return fmt.Errorf("typewipe: cannot determine executable: %w", err) + } + + self, err := os.ReadFile(selfPath) + if err != nil { + return fmt.Errorf("typewipe: cannot read binary: %w", err) + } + + return wipeFromMacho(self, opts) +} + +func wipeFromMacho(raw []byte, opts WipeOptions) error { + f, err := macho.NewFile(newBytesReader(raw)) + if err != nil { + return fmt.Errorf("typewipe: cannot parse Mach-O: %w", err) + } + + var targets []wipeTarget + + if opts.WipeTypelink { + for _, sec := range f.Sections { + if sec.Name == "__typelink" { + targets = append(targets, wipeTarget{ + name: "__typelink", + addr: uintptr(sec.Addr), + size: uintptr(sec.Size), + xtype: wipeZero, + }) + break + } + } + } + + if opts.WipeItab { + for _, sec := range f.Sections { + if sec.Name == "__itablink" || sec.Name == "__itab" { + targets = append(targets, wipeTarget{ + name: sec.Name, + addr: uintptr(sec.Addr), + size: uintptr(sec.Size), + xtype: wipeXOR, + }) + break + } + } + } + + if opts.WipeTypeNames { + for _, sec := range f.Sections { + if sec.Name == "__gopclntab" || sec.Name == "__pclntab" { + data, err := sec.Data() + if err != nil { + continue + } + nameStart, nameEnd := findTypeNameRegion(data) + if nameStart > 0 && nameEnd > nameStart { + targets = append(targets, wipeTarget{ + name: sec.Name + ":names", + addr: uintptr(sec.Addr) + uintptr(nameStart), + size: uintptr(nameEnd - nameStart), + xtype: wipeXOR, + }) + } + break + } + } + } + + if len(targets) == 0 { + return fmt.Errorf("typewipe: no target sections found") + } + + for _, t := range targets { + if err := wipeSection(t, opts.Key); err != nil { + return fmt.Errorf("typewipe: failed to wipe %s: %w", t.name, err) + } + } + + return nil +} + +func wipeSection(target wipeTarget, key byte) error { + pageSize := uintptr(syscall.Getpagesize()) + baseAddr := target.addr &^ (pageSize - 1) + protSize := target.size + (target.addr - baseAddr) + + if err := mprotectRW(baseAddr, protSize); err != nil { + return fmt.Errorf("mprotect RW failed: %w", err) + } + + sectionSlice := ptrToSlice(target.addr, target.size) + + switch target.xtype { + case wipeZero: + zeroSlice(sectionSlice) + case wipeXOR: + xorSlice(sectionSlice, key) + } + + _ = mprotectRO(baseAddr, protSize) + + return nil +} + +func findTypeNameRegion(data []byte) (uint32, uint32) { + magics := [][]byte{ + {0xF1, 0xFF, 0xFF, 0xFA}, + {0xF1, 0xFF, 0xFF, 0xF0}, + {0xF1, 0xFF, 0xFF, 0xF1}, + } + + if len(data) < 8 { + return 0, 0 + } + + valid := false + for _, m := range magics { + if len(data) >= len(m) && bytesEqualLocal(data[:len(m)], m) { + valid = true + break + } + } + if !valid { + return 0, 0 + } + + ptrSize := int(data[6]) + if ptrSize != 4 && ptrSize != 8 { + ptrSize = 8 + } + + isGo118 := bytesEqualLocal(data[:4], magics[2]) + + if isGo118 { + if len(data) < 16+4+6*ptrSize { + return 0, 0 + } + offset := 16 + 4 + offset += ptrSize // nfunc + offset += ptrSize // nfiles + offset += ptrSize // textStart + funcnametab := readUintLocal(data, offset, ptrSize) + + nameStart := funcnametab + if nameStart >= uint32(len(data)) { + return 0, 0 + } + + nameEnd := findDoubleNull(data, nameStart) + return nameStart, nameEnd + } + + offset := 8 + 4 + 4 + if bytesEqualLocal(data[:4], magics[1]) { + offset += 4 + } + if offset+8 > len(data) { + return 0, 0 + } + + nameStart := readUintLocal(data, offset, 4) + if nameStart >= uint32(len(data)) { + return 0, 0 + } + + nameEnd := findDoubleNull(data, nameStart) + return nameStart, nameEnd +} + +func findDoubleNull(data []byte, start uint32) uint32 { + for i := start; i < uint32(len(data)); i++ { + if data[i] == 0 && i+1 < uint32(len(data)) && data[i+1] == 0 { + return i + } + } + return uint32(len(data)) +} + +func readUintLocal(data []byte, offset int, size int) uint32 { + if offset+size > len(data) { + return 0 + } + switch size { + case 4: + return uint32(data[offset]) | uint32(data[offset+1])<<8 | uint32(data[offset+2])<<16 | uint32(data[offset+3])<<24 + case 8: + return uint32(data[offset]) | uint32(data[offset+1])<<8 | uint32(data[offset+2])<<16 | uint32(data[offset+3])<<24 + default: + return 0 + } +} + +func bytesEqualLocal(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +type localBytesReader struct { + data []byte +} + +func newBytesReader(data []byte) *localBytesReader { + return &localBytesReader{data: data} +} + +func (r *localBytesReader) ReadAt(p []byte, off int64) (int, error) { + if off < 0 || int(off) >= len(r.data) { + return 0, fmt.Errorf("invalid offset") + } + n := copy(p, r.data[int(off):]) + if n < len(p) { + return n, fmt.Errorf("EOF") + } + return n, nil +} diff --git a/pkg/typewipe/typewipe_linux.go b/pkg/typewipe/typewipe_linux.go new file mode 100644 index 0000000..02c81dd --- /dev/null +++ b/pkg/typewipe/typewipe_linux.go @@ -0,0 +1,254 @@ +//go:build linux + +package typewipe + +import ( + "debug/elf" + "fmt" + "os" + "syscall" + "unsafe" +) + +type wipeType int + +const ( + wipeZero wipeType = iota + wipeXOR +) + +type wipeTarget struct { + name string + addr uintptr + size uintptr + xtype wipeType +} + +func wipePlatform(opts WipeOptions) error { + self, err := os.ReadFile("/proc/self/exe") + if err != nil { + return fmt.Errorf("typewipe: cannot read /proc/self/exe: %w", err) + } + + return wipeFromELF(self, opts) +} + +func wipeFromELF(raw []byte, opts WipeOptions) error { + f, err := elf.NewFile(newBytesReader(raw)) + if err != nil { + return fmt.Errorf("typewipe: cannot parse ELF: %w", err) + } + + var targets []wipeTarget + + if opts.WipeTypelink { + sec := f.Section(".typelink") + if sec != nil { + targets = append(targets, wipeTarget{ + name: ".typelink", + addr: uintptr(sec.Addr), + size: uintptr(sec.Size), + xtype: wipeZero, + }) + } + } + + if opts.WipeItab { + for _, name := range []string{".itablink", ".itab"} { + sec := f.Section(name) + if sec != nil { + targets = append(targets, wipeTarget{ + name: name, + addr: uintptr(sec.Addr), + size: uintptr(sec.Size), + xtype: wipeXOR, + }) + break + } + } + } + + if opts.WipeTypeNames { + for _, name := range []string{".gopclntab", "runtime.pclntab", "runtime.pclntab_v1"} { + sec := f.Section(name) + if sec == nil { + continue + } + data, err := sec.Data() + if err != nil { + continue + } + nameStart, nameEnd := findTypeNameRegion(data) + if nameStart > 0 && nameEnd > nameStart { + targets = append(targets, wipeTarget{ + name: name + ":names", + addr: uintptr(sec.Addr) + uintptr(nameStart), + size: uintptr(nameEnd - nameStart), + xtype: wipeXOR, + }) + } + break + } + } + + if len(targets) == 0 { + return fmt.Errorf("typewipe: no target sections found") + } + + for _, t := range targets { + if err := wipeSection(t, opts.Key); err != nil { + return fmt.Errorf("typewipe: failed to wipe %s: %w", t.name, err) + } + } + + return nil +} + +func wipeSection(target wipeTarget, key byte) error { + pageSize := uintptr(syscall.Getpagesize()) + baseAddr := target.addr &^ (pageSize - 1) + protSize := target.size + (target.addr - baseAddr) + + if err := syscall.Mprotect( + (*[1]byte)(unsafe.Pointer(baseAddr))[:protSize:protSize], + syscall.PROT_READ|syscall.PROT_WRITE, + ); err != nil { + return fmt.Errorf("mprotect RW failed: %w", err) + } + + ptr := unsafe.Pointer(target.addr) + sectionSlice := (*[1 << 30]byte)(ptr)[:target.size:target.size] + + switch target.xtype { + case wipeZero: + zeroSlice(sectionSlice) + case wipeXOR: + xorSlice(sectionSlice, key) + } + + _ = syscall.Mprotect( + (*[1]byte)(unsafe.Pointer(baseAddr))[:protSize:protSize], + syscall.PROT_READ, + ) + + return nil +} + +func findTypeNameRegion(data []byte) (uint32, uint32) { + magics := [][]byte{ + {0xF1, 0xFF, 0xFF, 0xFA}, + {0xF1, 0xFF, 0xFF, 0xF0}, + {0xF1, 0xFF, 0xFF, 0xF1}, + } + + if len(data) < 8 { + return 0, 0 + } + + valid := false + for _, m := range magics { + if len(data) >= len(m) && bytesEqualLocal(data[:len(m)], m) { + valid = true + break + } + } + if !valid { + return 0, 0 + } + + ptrSize := int(data[6]) + if ptrSize != 4 && ptrSize != 8 { + ptrSize = 8 + } + + isGo118 := bytesEqualLocal(data[:4], magics[2]) + + if isGo118 { + if len(data) < 16+4+6*ptrSize { + return 0, 0 + } + offset := 16 + 4 + offset += ptrSize // nfunc + offset += ptrSize // nfiles + offset += ptrSize // textStart + funcnametab := readUintLocal(data, offset, ptrSize) + + nameStart := funcnametab + if nameStart >= uint32(len(data)) { + return 0, 0 + } + + nameEnd := findDoubleNull(data, nameStart) + return nameStart, nameEnd + } + + offset := 8 + 4 + 4 + if bytesEqualLocal(data[:4], magics[1]) { + offset += 4 + } + if offset+8 > len(data) { + return 0, 0 + } + + nameStart := readUintLocal(data, offset, 4) + if nameStart >= uint32(len(data)) { + return 0, 0 + } + + nameEnd := findDoubleNull(data, nameStart) + return nameStart, nameEnd +} + +func findDoubleNull(data []byte, start uint32) uint32 { + for i := start; i < uint32(len(data)); i++ { + if data[i] == 0 && i+1 < uint32(len(data)) && data[i+1] == 0 { + return i + } + } + return uint32(len(data)) +} + +func readUintLocal(data []byte, offset int, size int) uint32 { + if offset+size > len(data) { + return 0 + } + switch size { + case 4: + return uint32(data[offset]) | uint32(data[offset+1])<<8 | uint32(data[offset+2])<<16 | uint32(data[offset+3])<<24 + case 8: + return uint32(data[offset]) | uint32(data[offset+1])<<8 | uint32(data[offset+2])<<16 | uint32(data[offset+3])<<24 + default: + return 0 + } +} + +func bytesEqualLocal(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +type localBytesReader struct { + data []byte +} + +func newBytesReader(data []byte) *localBytesReader { + return &localBytesReader{data: data} +} + +func (r *localBytesReader) ReadAt(p []byte, off int64) (int, error) { + if off < 0 || int(off) >= len(r.data) { + return 0, fmt.Errorf("invalid offset") + } + n := copy(p, r.data[int(off):]) + if n < len(p) { + return n, fmt.Errorf("EOF") + } + return n, nil +} diff --git a/pkg/typewipe/typewipe_unsafe.go b/pkg/typewipe/typewipe_unsafe.go new file mode 100644 index 0000000..c2510fe --- /dev/null +++ b/pkg/typewipe/typewipe_unsafe.go @@ -0,0 +1,32 @@ +//go:build darwin || linux + +package typewipe + +import ( + "syscall" + "unsafe" +) + +//go:nosplit +func mprotectRW(addr uintptr, size uintptr) error { + p := *(*unsafe.Pointer)(unsafe.Pointer(&addr)) + return syscall.Mprotect( + unsafe.Slice((*byte)(p), size), + syscall.PROT_READ|syscall.PROT_WRITE, + ) +} + +//go:nosplit +func mprotectRO(addr uintptr, size uintptr) error { + p := *(*unsafe.Pointer)(unsafe.Pointer(&addr)) + return syscall.Mprotect( + unsafe.Slice((*byte)(p), size), + syscall.PROT_READ, + ) +} + +//go:nosplit +func ptrToSlice(addr uintptr, size uintptr) []byte { + p := *(*unsafe.Pointer)(unsafe.Pointer(&addr)) + return unsafe.Slice((*byte)(p), size) +} diff --git a/pkg/typewipe/typewipe_windows.go b/pkg/typewipe/typewipe_windows.go new file mode 100644 index 0000000..e3c6358 --- /dev/null +++ b/pkg/typewipe/typewipe_windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package typewipe + +import "fmt" + +func wipePlatform(opts WipeOptions) error { + return fmt.Errorf("typewipe: runtime wiping not supported on windows") +}