This commit is contained in:
2026-05-31 19:48:56 +01:00
parent 7f32417736
commit 68f8904282
39 changed files with 4519 additions and 5 deletions
+154 -5
View File
@@ -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/<ppid>/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
```
+362
View File
@@ -0,0 +1,362 @@
// Basic block reordering transformation.
//
// Randomizes the order of basic blocks within functions to defeat
// linear disassembly and static analysis. Handles if/else body swapping
// (with condition negation), switch case reordering, and sequential
// block permutation with jump-label indirection.
package transform
import (
"math/rand"
"regexp"
"strings"
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
)
var (
// Matches if (...) { ... } else { ... } blocks at the line level
bbrIfPattern = regexp.MustCompile(`^(\s*)if\s+(.+?)\s*\{`)
bbrElsePattern = regexp.MustCompile(`^(\s*)\}\s*else\s*\{`)
bbrElseIfPattern = regexp.MustCompile(`^(\s*)\}\s*else\s+if\s+(.+?)\s*\{`)
// Matches switch statements
bbrSwitchPattern = regexp.MustCompile(`^(\s*)switch\s+(.+?)\s*\{`)
)
// ReorderBasicBlocks randomizes control flow block ordering within functions.
func ReorderBasicBlocks(src string) string {
rng := rand.New(rand.NewSource(rand.Int63()))
lines := strings.Split(src, "\n")
var result []string
i := 0
for i < len(lines) {
trimmed := strings.TrimSpace(lines[i])
if isFuncDecl(trimmed) {
funcLines := collectFuncBody(lines, i)
if funcLines != nil && len(funcLines) >= 12 {
reordered := reorderFuncBlocks(funcLines, rng)
result = append(result, reordered...)
i += len(funcLines)
continue
}
}
result = append(result, lines[i])
i++
}
return strings.Join(result, "\n")
}
// reorderFuncBlocks applies all reordering strategies to a single function.
func reorderFuncBlocks(funcLines []string, rng *rand.Rand) []string {
funcSig := funcLines[0]
closingBrace := funcLines[len(funcLines)-1]
bodyLines := funcLines[1 : len(funcLines)-1]
var result []string
result = append(result, funcSig)
// Strategy 1: Swap if/else bodies with negated conditions
bodyLines = swapIfElse(bodyLines, rng)
// Strategy 2: Reorder switch cases
bodyLines = reorderSwitchCases(bodyLines, rng)
result = append(result, bodyLines...)
result = append(result, closingBrace)
return result
}
// swapIfElse finds if/else blocks and swaps the bodies, negating the condition.
func swapIfElse(lines []string, rng *rand.Rand) []string {
var result []string
i := 0
for i < len(lines) {
// Look for if blocks with else
ifMatch := bbrIfPattern.FindStringSubmatch(lines[i])
if ifMatch == nil || rng.Float64() > 0.4 {
result = append(result, lines[i])
i++
continue
}
// Collect the if body
indent := ifMatch[1]
condition := ifMatch[2]
ifBody := []string{lines[i]}
i++
depth := 1
for i < len(lines) && depth > 0 {
line := lines[i]
for _, c := range line {
if c == '{' {
depth++
}
if c == '}' {
depth--
}
}
ifBody = append(ifBody, line)
i++
// Check if this line ends the if block and has else
if depth == 0 && i < len(lines) {
if bbrElsePattern.MatchString(lines[i]) || bbrElseIfPattern.MatchString(lines[i]) {
break
}
}
}
// Look for else block
if i >= len(lines) || depth != 0 {
result = append(result, ifBody...)
continue
}
// Handle else if — don't swap, too complex
if bbrElseIfPattern.MatchString(lines[i]) {
result = append(result, ifBody...)
continue
}
if !bbrElsePattern.MatchString(lines[i]) {
result = append(result, ifBody...)
continue
}
// Collect else body
elseBody := []string{lines[i]}
i++
depth = 1
for i < len(lines) && depth > 0 {
line := lines[i]
for _, c := range line {
if c == '{' {
depth++
}
if c == '}' {
depth--
}
}
elseBody = append(elseBody, line)
i++
}
// Swap: emit negated condition with else body first, then else with if body
negCond := negateCondition(condition)
// Build swapped: if !condition { <else body> } else { <if body> }
result = append(result, indent+"if "+negCond+" {")
// The else body contents (skip the "else {" line and closing "}")
elseContents := elseBody[1 : len(elseBody)-1]
for _, line := range elseContents {
result = append(result, line)
}
result = append(result, indent+"} else {")
// The if body contents (skip the "if ... {" line and closing "}")
ifContents := ifBody[1 : len(ifBody)-1]
for _, line := range ifContents {
result = append(result, line)
}
result = append(result, indent+"}")
}
return result
}
// negateCondition negates a boolean condition.
func negateCondition(cond string) string {
cond = strings.TrimSpace(cond)
// Handle simple negation
if strings.HasPrefix(cond, "!") {
return strings.TrimPrefix(cond, "!")
}
// Handle parenthesized conditions
if strings.HasPrefix(cond, "(") && strings.HasSuffix(cond, ")") {
return "!(" + cond + ")"
}
// Negate comparison operators
negationMap := map[string]string{
"==": "!=",
"!=": "==",
"<": ">=",
">": "<=",
"<=": ">",
">=": "<",
}
for op, negOp := range negationMap {
idx := strings.Index(cond, op)
if idx > 0 {
// Make sure we're not matching a substring of a longer operator
afterOp := idx + len(op)
if afterOp < len(cond) {
nextChar := cond[afterOp]
if nextChar == '=' || nextChar == '<' || nextChar == '>' {
continue
}
}
return cond[:idx] + negOp + cond[idx+len(op):]
}
}
// Default: wrap in negation
return "!(" + cond + ")"
}
// reorderSwitchCases finds switch statements and randomly reorders their cases.
func reorderSwitchCases(lines []string, rng *rand.Rand) []string {
var result []string
i := 0
for i < len(lines) {
trimmed := strings.TrimSpace(lines[i])
// Find switch statements
if !strings.HasPrefix(trimmed, "switch ") || !strings.HasSuffix(trimmed, "{") {
result = append(result, lines[i])
i++
continue
}
// Only reorder with some probability
if rng.Float64() > 0.4 {
result = append(result, lines[i])
i++
continue
}
switchHeader := lines[i]
i++
// Collect all cases
type caseBlock struct {
header string
body []string
}
var cases []caseBlock
var currentCase *caseBlock
depth := 1
for i < len(lines) && depth > 0 {
line := lines[i]
for _, c := range line {
if c == '{' {
depth++
}
if c == '}' {
depth--
}
}
if depth <= 0 {
break
}
t := strings.TrimSpace(line)
if strings.HasPrefix(t, "case ") || strings.HasPrefix(t, "default:") {
if currentCase != nil {
cases = append(cases, *currentCase)
}
currentCase = &caseBlock{header: line}
} else if currentCase != nil {
currentCase.body = append(currentCase.body, line)
}
i++
}
if currentCase != nil {
cases = append(cases, *currentCase)
}
// Skip closing brace of switch
i++
// Only reorder if enough cases and no default (reordering default is risky)
hasDefault := false
for _, c := range cases {
if strings.Contains(strings.TrimSpace(c.header), "default:") {
hasDefault = true
break
}
}
if len(cases) >= 3 && !hasDefault {
// Shuffle non-first cases (keep first case position to maintain entry point)
if len(cases) > 2 {
shuffleStart := 1
perm := rng.Perm(len(cases) - shuffleStart)
shuffled := make([]caseBlock, len(cases))
copy(shuffled, cases[:shuffleStart])
for j, p := range perm {
shuffled[shuffleStart+j] = cases[shuffleStart+p]
}
cases = shuffled
}
}
// Reconstruct switch
result = append(result, switchHeader)
for _, c := range cases {
result = append(result, c.header)
result = append(result, c.body...)
}
result = append(result, "\t}")
}
return result
}
// InjectJumpLabels converts sequential blocks into labeled blocks with goto
// indirection for functions that are eligible. This is the most aggressive
// reordering strategy and requires careful analysis of control dependencies.
func InjectJumpLabels(bodyLines []string, rng *rand.Rand) []string {
stmts := parseStatements(bodyLines)
if len(stmts) < 4 {
return bodyLines
}
// Generate labels
var labels []string
for range stmts {
labels = append(labels, stringcrypt.RandomIdent("_lbl", 4))
}
// Create a random permutation of statement order
perm := rng.Perm(len(stmts))
var result []string
result = append(result, "\tgoto "+labels[perm[0]])
for _, idx := range perm {
result = append(result, labels[idx]+":")
for _, line := range stmts[idx] {
trimmed := strings.TrimPrefix(line, "\t")
result = append(result, "\t"+trimmed)
}
// Add goto to next block in permutation order
nextIdx := -1
for j, p := range perm {
if p == idx && j+1 < len(perm) {
nextIdx = perm[j+1]
break
}
}
if nextIdx >= 0 {
result = append(result, "\tgoto "+labels[nextIdx])
}
}
return result
}
+181
View File
@@ -0,0 +1,181 @@
// Constant blinding transformation.
//
// Replaces integer literal constants with runtime XOR expressions that
// evaluate to the original value, defeating pattern-matching in disassemblers
// and static analysis tools. Each constant C is replaced with (K ^ (K ^ C))
// where K is a per-constant random key.
package transform
import (
"fmt"
"math/rand"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
)
var (
cbHexRe = regexp.MustCompile(`0x[0-9a-fA-F]+`)
cbStringRe = regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"`)
cbBacktickRe = regexp.MustCompile("`[^`]*`")
cbSkipContexts = []string{
"case ", "case\t",
"[",
"len(", "cap(",
"make(",
"iota",
}
)
func TransformConstants(src string) string {
rng := rand.New(rand.NewSource(rand.Int63()))
blindHelperName := stringcrypt.RandomIdent("_blind", 4)
lines := strings.Split(src, "\n")
var result []string
inFuncBody := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") {
inFuncBody = true
}
if inFuncBody && trimmed == "}" {
inFuncBody = false
}
if !inFuncBody {
result = append(result, line)
continue
}
if strings.HasPrefix(trimmed, "//") || trimmed == "" || trimmed == "{" || trimmed == "}" {
result = append(result, line)
continue
}
if shouldCBLineSkip(trimmed) {
result = append(result, line)
continue
}
blinded := cbBlindLine(line, rng, blindHelperName)
result = append(result, blinded)
}
resultStr := strings.Join(result, "\n")
resultStr = injectBlindHelper(resultStr, blindHelperName)
return resultStr
}
func shouldCBLineSkip(trimmed string) bool {
if strings.HasPrefix(trimmed, "case ") || strings.HasPrefix(trimmed, "case\t") {
return true
}
if strings.Contains(trimmed, "[") && strings.Contains(trimmed, "]") {
return true
}
for _, ctx := range cbSkipContexts {
if strings.Contains(trimmed, ctx) {
return true
}
}
return false
}
func cbBlindLine(line string, rng *rand.Rand, helperName string) string {
line = cbHexRe.ReplaceAllStringFunc(line, func(match string) string {
return cbBlindHex(match, rng, helperName)
})
line = blindDecimalsInLine(line, rng, helperName)
return line
}
func blindDecimalsInLine(line string, rng *rand.Rand, helperName string) string {
var buf strings.Builder
i := 0
for i < len(line) {
if unicode.IsDigit(rune(line[i])) {
start := i
for i < len(line) && unicode.IsDigit(rune(line[i])) {
i++
}
numStr := line[start:i]
if start > 0 && (unicode.IsLetter(rune(line[start-1])) || line[start-1] == '_') {
buf.WriteString(numStr)
continue
}
if i < len(line) && (unicode.IsLetter(rune(line[i])) || line[i] == '_' || line[i] == 'x' || line[i] == 'X') {
buf.WriteString(numStr)
continue
}
replaced := cbBlindDecimal(numStr, rng, helperName)
buf.WriteString(replaced)
} else {
buf.WriteByte(line[i])
i++
}
}
return buf.String()
}
func cbBlindHex(match string, rng *rand.Rand, helperName string) string {
val, err := strconv.ParseUint(match, 0, 64)
if err != nil {
return match
}
if val == 0 || val == 1 {
return match
}
if val > 0x7FFFFFFFFFFFFFFF {
return match
}
key := rng.Uint64() | 1
blinded := key ^ val
return fmt.Sprintf("%s(0x%X, 0x%X)", helperName, key, blinded)
}
func cbBlindDecimal(match string, rng *rand.Rand, helperName string) string {
val, err := strconv.ParseUint(match, 10, 64)
if err != nil {
return match
}
if val <= 2 || val < 10 {
return match
}
key := rng.Uint64() | 1
blinded := key ^ val
return fmt.Sprintf("%s(0x%X, 0x%X)", helperName, key, blinded)
}
func injectBlindHelper(src string, helperName string) string {
helper := fmt.Sprintf(`
//go:noinline
func %s(a, b uint64) uint64 { return a ^ b }
`, helperName)
insertAt := findImportBlockEnd(src)
if insertAt == -1 {
pkgIdx := strings.Index(src, "package ")
if pkgIdx != -1 {
lineEnd := strings.Index(src[pkgIdx:], "\n")
if lineEnd != -1 {
insertAt = pkgIdx + lineEnd + 1
}
}
}
if insertAt != -1 && insertAt < len(src) {
src = src[:insertAt] + helper + src[insertAt:]
}
return src
}
+294
View File
@@ -0,0 +1,294 @@
// Fake function signature injection.
//
// Injects realistic-looking but dead function definitions into the source
// file. These functions are never called and serve to confuse disassemblers
// by polluting the symbol table with plausible function boundaries, making
// it harder to identify real functions and their purposes.
package transform
import (
"fmt"
"math/rand"
"strings"
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
)
type fakeFuncTemplate struct {
namePattern string
params string
returnType string
bodyGen func(rng *rand.Rand, argName string) string
}
var fakeFuncTemplates = []fakeFuncTemplate{
{
namePattern: "_initAuth_obf",
params: "token string",
returnType: "bool",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_v", 3)
v2 := stringcrypt.RandomIdent("_v", 3)
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := %s & 0xFF\n\treturn %s > 100 && %s < 0",
arg, v1, arg, v2, arg, v1, v2)
},
},
{
namePattern: "_parseConfig_obf",
params: "data []byte",
returnType: "int",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_c", 3)
v2 := stringcrypt.RandomIdent("_c", 3)
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := %s * 3\n\t_ = %s\n\treturn %s - %s",
arg, v1, arg, v2, v1, v2, v1, v2)
},
},
{
namePattern: "_validateKey_obf",
params: "key string, size int",
returnType: "bool",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_k", 3)
v2 := stringcrypt.RandomIdent("_k", 3)
return fmt.Sprintf("\t%s := len(key)\n\t%s := size ^ 0xDEAD\n\t_ = %s\n\treturn %s == %s && %s != %s",
v1, v2, v2, v1, v2, v1, v2)
},
},
{
namePattern: "_hashData_obf",
params: "input []byte",
returnType: "[32]byte",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_h", 3)
return fmt.Sprintf("\tvar %s [32]byte\n\t_ = %s\n\tfor _i := range %s {\n\t\t%s[_i] = byte(_i)\n\t}\n\treturn %s",
v1, arg, arg, v1, v1)
},
},
{
namePattern: "_encodePayload_obf",
params: "buf []byte, offset int",
returnType: "int",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_e", 3)
return fmt.Sprintf("\t_ = buf\n\t_ = offset\n\t%s := len(buf) + offset\n\treturn %s ^ %s",
v1, v1, v1)
},
},
{
namePattern: "_checkPerms_obf",
params: "uid int, gid int",
returnType: "bool",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_p", 3)
return fmt.Sprintf("\t%s := uid ^ gid\n\treturn %s > 0 && %s < 0 && uid == gid && uid != gid",
v1, v1, v1)
},
},
{
namePattern: "_readStream_obf",
params: "path string",
returnType: "[]byte",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_r", 3)
v2 := stringcrypt.RandomIdent("_r", 3)
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := make([]byte, %s)\n\tfor _i := range %s {\n\t\t%s[_i] = 0\n\t}\n\treturn %s",
arg, v1, arg, v2, v1, v2, v2, v2)
},
},
{
namePattern: "_formatLog_obf",
params: "level int, msg string",
returnType: "string",
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_l", 3)
return fmt.Sprintf("\t_ = level\n\t%s := len(msg)\n\tif %s > 1000000 {\n\t\treturn \"\"\n\t}\n\treturn msg[:%s]",
v1, v1, v1)
},
},
}
// InjectFakeSignatures inserts 2-5 fake function definitions per file
// that look realistic but are never called.
func InjectFakeSignatures(src string) string {
rng := rand.New(rand.NewSource(rand.Int63()))
numFake := rng.Intn(4) + 2 // 2-5 fake functions
var fakeFuncs strings.Builder
// Select random templates without replacement
perm := rng.Perm(len(fakeFuncTemplates))
for i := 0; i < numFake && i < len(fakeFuncTemplates); i++ {
tmpl := fakeFuncTemplates[perm[i]]
suffix := fmt.Sprintf("%04X", rng.Intn(0xFFFF))
funcName := tmpl.namePattern + suffix
argName := extractFirstArgName(tmpl.params)
fakeFuncs.WriteString("\n//go:noinline\n")
fakeFuncs.WriteString(fmt.Sprintf("func %s(%s) %s {\n", funcName, tmpl.params, tmpl.returnType))
fakeFuncs.WriteString(tmpl.bodyGen(rng, argName))
fakeFuncs.WriteString("\n}\n")
}
// Inject at package level, after imports
insertAt := findImportBlockEnd(src)
if insertAt == -1 {
pkgIdx := strings.Index(src, "package ")
if pkgIdx != -1 {
lineEnd := strings.Index(src[pkgIdx:], "\n")
if lineEnd != -1 {
insertAt = pkgIdx + lineEnd + 1
}
}
}
if insertAt != -1 && insertAt < len(src) {
src = src[:insertAt] + fakeFuncs.String() + src[insertAt:]
}
return src
}
func extractFirstArgName(params string) string {
params = strings.TrimSpace(params)
if params == "" {
return "_"
}
parts := strings.SplitN(params, ",", 2)
first := strings.TrimSpace(parts[0])
fields := strings.Fields(first)
if len(fields) >= 1 {
return fields[0]
}
return "_"
}
// InjectMoreFakeSignatures generates additional fake functions with
// random realistic names and bodies for deeper obfuscation.
func InjectMoreFakeSignatures(src string, count int) string {
rng := rand.New(rand.NewSource(rand.Int63()))
prefixes := []string{
"_resolveAddr", "_buildRequest", "_handleResp", "_writeLog",
"_parseHeader", "_checkAuth", "_compressData", "_decompressBuf",
"_verifySig", "_loadCache", "_flushBuffer", "_retryConn",
"_closeSocket", "_allocMem", "_releaseLock", "_acquireLock",
"_serializeMsg", "_deserializeMsg", "_encodeURL", "_decodeURL",
}
paramSets := []struct {
params string
returnType string
}{
{"addr string, port int", "error"},
{"data []byte", "int"},
{"key string", "bool"},
{"src, dst string", "string"},
{"n int", "int64"},
{"buf []byte, offset int", "[]byte"},
{"flag bool, name string", "string"},
{"x, y int64", "int64"},
}
var fakeFuncs strings.Builder
for i := 0; i < count; i++ {
prefix := prefixes[rng.Intn(len(prefixes))]
suffix := fmt.Sprintf("%04X", rng.Intn(0xFFFF))
funcName := prefix + "_obf" + suffix
pset := paramSets[rng.Intn(len(paramSets))]
argName := extractFirstArgName(pset.params)
body := generateRandomBody(rng, argName, pset.returnType)
fakeFuncs.WriteString("\n//go:noinline\n")
fakeFuncs.WriteString(fmt.Sprintf("func %s(%s) %s {\n", funcName, pset.params, pset.returnType))
fakeFuncs.WriteString(body)
fakeFuncs.WriteString("\n}\n")
}
insertAt := findImportBlockEnd(src)
if insertAt == -1 {
pkgIdx := strings.Index(src, "package ")
if pkgIdx != -1 {
lineEnd := strings.Index(src[pkgIdx:], "\n")
if lineEnd != -1 {
insertAt = pkgIdx + lineEnd + 1
}
}
}
if insertAt != -1 && insertAt < len(src) {
src = src[:insertAt] + fakeFuncs.String() + src[insertAt:]
}
return src
}
// generateRandomBody creates a plausible-looking function body that computes
// something meaningless but looks legitimate.
func generateRandomBody(rng *rand.Rand, argName, returnType string) string {
var sb strings.Builder
numStmts := rng.Intn(6) + 3 // 3-8 statements
vars := make([]string, numStmts)
for i := range vars {
vars[i] = stringcrypt.RandomIdent("_x", 3)
}
// Always start with consuming the argument
sb.WriteString(fmt.Sprintf("\t_ = %s\n", argName))
for i := 0; i < numStmts; i++ {
switch rng.Intn(5) {
case 0:
// Assignment from arithmetic
val := rng.Intn(0xFFFF) + 1
sb.WriteString(fmt.Sprintf("\t%s := %d ^ 0x%X\n", vars[i], val, rng.Intn(0xFFFF)))
case 1:
// Boolean expression
sb.WriteString(fmt.Sprintf("\t%s := %d > %d && %d < %d\n",
vars[i], rng.Intn(100), rng.Intn(100)+200, rng.Intn(100), rng.Intn(100)+200))
case 2:
// Self-referencing computation
if i > 0 {
sb.WriteString(fmt.Sprintf("\t%s = %s + %d\n", vars[i-1], vars[i-1], rng.Intn(1000)))
} else {
sb.WriteString(fmt.Sprintf("\t%s := %d\n", vars[i], rng.Intn(0xFFFF)))
}
case 3:
// Dead code branch
sb.WriteString(fmt.Sprintf("\tif false { %s = %s }\n", vars[i], vars[max(0, i-1)]))
case 4:
// Bitwise operation
sb.WriteString(fmt.Sprintf("\t%s := int64(%d) << %d\n", vars[i], rng.Intn(256), rng.Intn(8)))
}
}
// Return something based on the return type
switch returnType {
case "bool":
sb.WriteString(fmt.Sprintf("\treturn %s && !%s\n", vars[0], vars[0]))
case "int", "int64":
sb.WriteString(fmt.Sprintf("\treturn %s - %s\n", vars[0], vars[0]))
case "string":
sb.WriteString("\treturn \"\"\n")
case "[]byte":
sb.WriteString("\treturn nil\n")
case "[32]byte":
sb.WriteString(fmt.Sprintf("\tvar %s [32]byte\n\treturn %s\n", vars[numStmts-1], vars[numStmts-1]))
case "error":
sb.WriteString("\treturn nil\n")
default:
sb.WriteString(fmt.Sprintf("\treturn %s\n", vars[0]))
}
return sb.String()
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
+330
View File
@@ -0,0 +1,330 @@
// Function splitting transformation.
//
// Splits function bodies with 8+ statements into fragments called via
// indirect dispatch using a state machine pattern. Shared variables are
// hoisted and passed between fragments. Each fragment is marked go:noinline
// to prevent the compiler from re-inlining the split code.
package transform
import (
"fmt"
"math/rand"
"strings"
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
)
// SplitFunctions splits eligible function bodies into fragments called
// through a state-machine dispatcher.
func SplitFunctions(src string) string {
rng := rand.New(rand.NewSource(rand.Int63()))
lines := strings.Split(src, "\n")
var result []string
i := 0
for i < len(lines) {
trimmed := strings.TrimSpace(lines[i])
if isFuncDecl(trimmed) && !isMainOrInit(trimmed) {
funcLines := collectFuncBody(lines, i)
if funcLines != nil {
bodyLines := funcLines[1 : len(funcLines)-1]
funcSig := funcLines[0]
closingBrace := funcLines[len(funcLines)-1]
if shouldSplit(bodyLines) && rng.Float64() < 0.5 {
split := splitFunctionBody(funcSig, bodyLines, closingBrace, rng)
result = append(result, split...)
i += len(funcLines)
continue
}
}
}
result = append(result, lines[i])
i++
}
return strings.Join(result, "\n")
}
func isFuncDecl(trimmed string) bool {
return strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{")
}
func isMainOrInit(trimmed string) bool {
return strings.Contains(trimmed, "main") || strings.Contains(trimmed, "init()")
}
// collectFuncBody returns the lines of a function (signature through closing brace)
// starting at lineIdx. Returns nil if not a complete function.
func collectFuncBody(lines []string, lineIdx int) []string {
if lineIdx >= len(lines) {
return nil
}
funcLines := []string{lines[lineIdx]}
i := lineIdx + 1
depth := 1
for i < len(lines) && depth > 0 {
for _, c := range lines[i] {
if c == '{' {
depth++
}
if c == '}' {
depth--
}
}
funcLines = append(funcLines, lines[i])
i++
}
if depth != 0 {
return nil
}
return funcLines
}
// shouldSplit determines if a function body is worth splitting.
// Requires 8+ statements and no disqualifying patterns.
func shouldSplit(bodyLines []string) bool {
stmtCount := 0
for _, line := range bodyLines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || trimmed == "{" || trimmed == "}" {
continue
}
// Skip functions with early returns, goto, or defer
if strings.HasPrefix(trimmed, "return ") || trimmed == "return" {
if stmtCount < 3 {
return false // Early return — don't split
}
}
if strings.HasPrefix(trimmed, "goto ") || strings.HasPrefix(trimmed, "defer ") {
return false
}
// Skip if there are labels
if strings.HasSuffix(trimmed, ":") && !strings.HasPrefix(trimmed, "case ") && !strings.HasPrefix(trimmed, "default") {
return false
}
stmtCount++
}
return stmtCount >= 8
}
// splitFunctionBody splits a function body into fragments using a state machine.
func splitFunctionBody(funcSig string, bodyLines []string, closingBrace string, rng *rand.Rand) []string {
var result []string
result = append(result, funcSig)
// Parse statements from body
stmts := parseStatements(bodyLines)
if len(stmts) < 4 {
// Not enough statements; return unmodified
result = append(result, bodyLines...)
result = append(result, closingBrace)
return result
}
// Determine number of fragments (2-4)
numFragments := 2
if len(stmts) >= 12 {
numFragments = 3
}
if len(stmts) >= 16 {
numFragments = 4
}
// Collect variables declared with := in the body so we can hoist them
var hoistedVars []string
declaredVars := map[string]bool{}
var cleanedStmts [][]string
for _, stmt := range stmts {
var cleaned []string
for _, line := range stmt {
trimmed := strings.TrimSpace(line)
if strings.Contains(trimmed, ":=") {
parts := strings.SplitN(trimmed, ":=", 2)
varName := strings.TrimSpace(parts[0])
rhs := strings.TrimSpace(parts[1])
names := splitVarNames(varName)
allValid := true
for _, n := range names {
if !isValidIdent(n) {
allValid = false
break
}
}
if allValid {
for _, n := range names {
if !declaredVars[n] {
hoistedVars = append(hoistedVars, "\tvar "+n+" "+inferVarType(rhs))
declaredVars[n] = true
}
}
cleaned = append(cleaned, "\t"+varName+" = "+rhs)
} else {
cleaned = append(cleaned, line)
}
} else {
cleaned = append(cleaned, line)
}
}
cleanedStmts = append(cleanedStmts, cleaned)
}
// Emit hoisted variables
for _, v := range hoistedVars {
result = append(result, v)
}
// Build state machine
stateVar := stringcrypt.RandomIdent("_sf", 4)
result = append(result, "\t"+stateVar+" := 0")
// Determine return variable if last statement is a return
returnVar := ""
lastStmtLines := cleanedStmts[len(cleanedStmts)-1]
if len(lastStmtLines) > 0 {
lastTrimmed := strings.TrimSpace(lastStmtLines[len(lastStmtLines)-1])
if strings.HasPrefix(lastTrimmed, "return ") {
returnExpr := strings.TrimPrefix(lastTrimmed, "return ")
returnVar = stringcrypt.RandomIdent("_ret", 4)
result = append(result, "\tvar "+returnVar+" "+inferVarType(returnExpr))
cleanedStmts[len(cleanedStmts)-1][len(lastStmtLines)-1] = "\t" + returnVar + " = " + returnExpr
}
}
result = append(result, "\tfor {")
result = append(result, "\t\tswitch "+stateVar+" {")
// Distribute statements across fragments
fragmentSize := len(cleanedStmts) / numFragments
for f := 0; f < numFragments; f++ {
result = append(result, fmt.Sprintf("\t\tcase %d:", f))
start := f * fragmentSize
end := (f + 1) * fragmentSize
if f == numFragments-1 {
end = len(cleanedStmts)
}
for j := start; j < end; j++ {
for _, line := range cleanedStmts[j] {
// Re-indent for inside switch case
trimmed := strings.TrimPrefix(line, "\t")
result = append(result, "\t\t\t"+trimmed)
}
}
if f < numFragments-1 {
result = append(result, fmt.Sprintf("\t\t\t%s = %d", stateVar, f+1))
} else {
if returnVar != "" {
result = append(result, "\t\t\treturn "+returnVar)
} else {
result = append(result, "\t\t\treturn")
}
}
}
result = append(result, "\t\tdefault:")
if returnVar != "" {
result = append(result, "\t\t\treturn "+returnVar)
} else {
result = append(result, "\t\t\treturn")
}
result = append(result, "\t\t}")
result = append(result, "\t}")
result = append(result, closingBrace)
return result
}
// parseStatements splits body lines into statement groups.
// Each group is a complete statement (may span multiple lines for blocks).
func parseStatements(bodyLines []string) [][]string {
var statements [][]string
var current []string
depth := 0
for _, line := range bodyLines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || trimmed == "{" || trimmed == "}" {
continue
}
for _, c := range trimmed {
if c == '{' {
depth++
}
if c == '}' {
depth--
}
}
current = append(current, line)
if depth <= 0 {
statements = append(statements, current)
current = nil
depth = 0
}
}
if len(current) > 0 {
statements = append(statements, current)
}
return statements
}
func splitVarNames(varPart string) []string {
varPart = strings.TrimSpace(varPart)
if strings.Contains(varPart, ",") {
var names []string
for _, n := range strings.Split(varPart, ",") {
names = append(names, strings.TrimSpace(n))
}
return names
}
return []string{varPart}
}
func inferVarType(rhs string) string {
rhs = strings.TrimSpace(rhs)
if strings.HasPrefix(rhs, "\"") || strings.HasPrefix(rhs, "`") || strings.HasPrefix(rhs, "fmt.") {
return "string"
}
if strings.HasPrefix(rhs, "[]byte") || strings.HasPrefix(rhs, "make([]byte") {
return "[]byte"
}
if rhs == "true" || rhs == "false" {
return "bool"
}
if strings.HasPrefix(rhs, "[") {
return "" // array — skip
}
// Default to int64 for numeric/unknown types
return "int64"
}
// InjectSplitFragments generates standalone fragment helper functions for a
// more aggressive splitting strategy where fragments are separate functions
// rather than switch cases. This is an alternative to the state-machine approach.
func InjectSplitFragments(fragments []string, rng *rand.Rand) string {
var sb strings.Builder
for i, frag := range fragments {
fragName := stringcrypt.RandomIdent(fmt.Sprintf("_frag%d_", i), 4)
sb.WriteString("//go:noinline\n")
sb.WriteString(fmt.Sprintf("func %s() {\n", fragName))
sb.WriteString(frag)
sb.WriteString("\n}\n")
}
return sb.String()
}
+326
View File
@@ -0,0 +1,326 @@
// MBA (Mixed Boolean-Arithmetic) expression transformation.
//
// Replaces simple arithmetic and bitwise operations with equivalent
// MBA expressions that produce the same result but obscure the
// original operation from static analysis and decompilation.
package transform
import (
"fmt"
"math/rand"
"regexp"
"strconv"
"strings"
"github.com/lukaszraczylo/go-obfuscation/pkg/stringcrypt"
)
var (
// Binary operations: a OP b where OP is +, -, ^, &, |
// Careful to not match comparison operators (==, !=, <=, >=, <<, >>)
// and not match unary ^ or -.
mbaBinAddRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*\+\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
mbaBinSubRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*-\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
mbaBinXorRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*\^\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
mbaBinAndRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*&\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
mbaBinOrRe = regexp.MustCompile(`([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)\s*\|\s*([a-zA-Z_][a-zA-Z0-9_.]*|\d+(?:\.\d+)?(?:<<\d+)?)`)
// Unary bitwise NOT: ^a — match broadly, filter context in replacement
mbaUnNotRe = regexp.MustCompile(`\^([a-zA-Z_][a-zA-Z0-9_.]*)`)
// Patterns to skip: comparisons, shifts, assignments
mbaSkipRe = regexp.MustCompile(`==|!=|<=|>=|<<|>>|<-|\+\+|--|\|\|`)
// String literal detection to avoid transforming inside strings
mbaStringLitRe = regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"`)
mbaBacktickRe = regexp.MustCompile("`[^`]*`")
// Operators in comparisons, type assertions, channel ops that we must skip
mbaCtxPatterns = []string{"==", "!=", "<=", ">=", "<<", ">>", "&&", "||", "<-", "++", "--", ":="}
)
// TransformMBA replaces simple arithmetic and bitwise operations with
// equivalent Mixed Boolean-Arithmetic expressions.
func TransformMBA(src string) string {
rng := rand.New(rand.NewSource(rand.Int63()))
// Build a set of string-literal and backtick-literal ranges to skip
skipRanges := buildSkipRanges(src)
lines := strings.Split(src, "\n")
var result []string
inFuncBody := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") {
inFuncBody = true
}
if inFuncBody && trimmed == "}" {
inFuncBody = false
}
if !inFuncBody || trimmed == "" || strings.HasPrefix(trimmed, "//") {
result = append(result, line)
continue
}
// Skip lines with comparison/shift operators or assignments
if containsAny(trimmed, mbaCtxPatterns) {
result = append(result, line)
continue
}
// Skip lines that are mostly control flow
if strings.HasPrefix(trimmed, "if ") || strings.HasPrefix(trimmed, "for ") ||
strings.HasPrefix(trimmed, "switch ") || strings.HasPrefix(trimmed, "return ") ||
strings.HasPrefix(trimmed, "case ") || strings.HasPrefix(trimmed, "default:") {
result = append(result, line)
continue
}
lineIdx := len(strings.Join(result, "\n")) + 1
_ = lineIdx
transformed := mbaTransformLine(line, rng, skipRanges)
result = append(result, transformed)
}
return strings.Join(result, "\n")
}
func buildSkipRanges(src string) [][2]int {
var ranges [][2]int
for _, loc := range mbaStringLitRe.FindAllStringIndex(src, -1) {
ranges = append(ranges, [2]int{loc[0], loc[1]})
}
for _, loc := range mbaBacktickRe.FindAllStringIndex(src, -1) {
ranges = append(ranges, [2]int{loc[0], loc[1]})
}
return ranges
}
func mbaTransformLine(line string, rng *rand.Rand, skipRanges [][2]int) string {
// Apply transforms in a specific order to avoid double-transforming.
// We transform the most specific patterns first.
// First, do unary NOT (most specific)
line = mbaApplyUnaryNot(line, rng)
// Then binary ops in order of specificity
// XOR and AND are more "bitwise" and less likely to be confused
line = mbaApplyBinOp(line, mbaBinXorRe, mbaXorIdentities, rng)
line = mbaApplyBinOp(line, mbaBinAndRe, mbaAndIdentities, rng)
line = mbaApplyBinOp(line, mbaBinOrRe, mbaOrIdentities, rng)
line = mbaApplyBinOp(line, mbaBinAddRe, mbaAddIdentities, rng)
line = mbaApplyBinOp(line, mbaBinSubRe, mbaSubIdentities, rng)
return line
}
type mbaIdentity struct {
template string
// If true, this identity introduces ninline helper that must be injected
hasHelper bool
helperDef string
}
var mbaAddIdentities = []mbaIdentity{
{template: "((%s) ^ (%s)) + 2 * ((%s) & (%s))"},
{template: "((%s) | (%s)) + ((%s) & (%s))"},
{template: "((%s) ^ (%s)) - ((^(%s)) & (%s)) - ((^(%s)) ^ (%s)) + (^(%s))"},
}
var mbaSubIdentities = []mbaIdentity{
{template: "((%s) ^ (%s)) - 2 * ((^(%s)) & (%s))"},
{template: "(%s) + (^(%s)) + 1"},
}
var mbaXorIdentities = []mbaIdentity{
{template: "((%s) | (%s)) - ((%s) & (%s))"},
{template: "((%s) & (^(%s))) | ((^(%s)) & (%s))"},
}
var mbaAndIdentities = []mbaIdentity{
{template: "((%s) | (%s)) - ((%s) ^ (%s))"},
}
var mbaOrIdentities = []mbaIdentity{
{template: "((%s) ^ (%s)) + ((%s) & (%s))"},
{template: "((%s) & (%s)) + ((%s) ^ (%s))"},
}
func mbaApplyBinOp(line string, re *regexp.Regexp, identities []mbaIdentity, rng *rand.Rand) string {
// Track positions already transformed to avoid double-transforming
var transformed []struct{ start, end int }
return re.ReplaceAllStringFunc(line, func(match string) string {
loc := re.FindStringIndex(match)
if loc == nil {
return match
}
// Check if this match overlaps with any already-transformed region
absStart := strings.Index(line, match)
for _, t := range transformed {
if absStart >= t.start && absStart < t.end {
return match
}
}
sub := re.FindStringSubmatch(match)
if len(sub) < 3 {
return match
}
a, b := sub[1], sub[2]
// Don't transform if either operand looks like a comparison result or is empty
if a == "" || b == "" {
return match
}
// Don't transform pure integer-integer operations (e.g., 2 + 3)
// Those are constant-folded by the compiler anyway
if isPureNumeric(a) && isPureNumeric(b) {
return match
}
ident := identities[rng.Intn(len(identities))]
var result string
switch len(strings.Split(ident.template, "%s")) - 1 {
case 2:
result = fmt.Sprintf(ident.template, a, b)
case 4:
result = fmt.Sprintf(ident.template, a, b, a, b)
case 6:
result = fmt.Sprintf(ident.template, a, b, a, b, a, b)
case 7:
result = fmt.Sprintf(ident.template, a, b, a, b, a, b, a)
default:
result = fmt.Sprintf(ident.template, a, b)
}
return result
})
}
func mbaApplyUnaryNot(line string, rng *rand.Rand) string {
// Manual search for ^identifier, checking preceding char
var buf strings.Builder
i := 0
for i < len(line) {
if line[i] == '^' && i+1 < len(line) {
// Check it's not preceded by identifier char
if i > 0 && (isAlphaNum(line[i-1]) || line[i-1] == '_') {
buf.WriteByte(line[i])
i++
continue
}
// Check next char starts an identifier
if !isAlpha(line[i+1]) && line[i+1] != '_' {
buf.WriteByte(line[i])
i++
continue
}
// Find end of identifier
j := i + 1
for j < len(line) && (isAlphaNum(line[j]) || line[j] == '_' || line[j] == '.') {
j++
}
a := line[i+1 : j]
if isPureNumeric(a) {
buf.WriteString(line[i:j])
i = j
continue
}
switch rng.Intn(3) {
case 0:
fmt.Fprintf(&buf, "((-%s) - 1)", a)
case 1:
fmt.Fprintf(&buf, "(%s ^ (-1))", a)
default:
fmt.Fprintf(&buf, "(%s ^ 0xFFFFFFFFFFFFFFFF)", a)
}
i = j
} else {
buf.WriteByte(line[i])
i++
}
}
return buf.String()
}
func isAlpha(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
func isAlphaNum(b byte) bool {
return isAlpha(b) || (b >= '0' && b <= '9')
}
func isPureNumeric(s string) bool {
s = strings.TrimSpace(s)
if s == "" {
return false
}
_, err := strconv.ParseInt(s, 0, 64)
if err == nil {
return true
}
_, err = strconv.ParseFloat(s, 64)
return err == nil
}
func containsAny(s string, patterns []string) bool {
for _, p := range patterns {
if strings.Contains(s, p) {
return true
}
}
return false
}
// InjectMBAHelpers inserts the go:noinline wrapper functions needed by MBA
// transformations. Called after all MBA transforms are applied.
func InjectMBAHelpers(src string) string {
helperName := stringcrypt.RandomIdent("_mba", 5)
helpers := fmt.Sprintf(`
//go:noinline
func %s_add(a, b int64) int64 { return (a ^ b) + 2*(a&b) }
//go:noinline
func %s_sub(a, b int64) int64 { return (a ^ b) - 2*((^a)&b) }
//go:noinline
func %s_xor(a, b int64) int64 { return (a | b) - (a & b) }
//go:noinline
func %s_and(a, b int64) int64 { return (a | b) - (a ^ b) }
//go:noinline
func %s_or(a, b int64) int64 { return (a ^ b) + (a & b) }
//go:noinline
func %s_not(a int64) int64 { return (-a) - 1 }
`, helperName, helperName, helperName, helperName, helperName, helperName)
insertAt := findImportBlockEnd(src)
if insertAt == -1 {
pkgIdx := strings.Index(src, "package ")
if pkgIdx != -1 {
lineEnd := strings.Index(src[pkgIdx:], "\n")
if lineEnd != -1 {
insertAt = pkgIdx + lineEnd + 1
}
}
}
if insertAt != -1 && insertAt < len(src) {
src = src[:insertAt] + helpers + src[insertAt:]
}
return src
}
+220
View File
@@ -0,0 +1,220 @@
// Long string splitting transformation.
//
// Splits string literals longer than a threshold into concatenated chunks
// of randomized length (5-9 characters). This ensures that even if string
// encryption is bypassed, long sensitive values like GPG keys, API keys,
// and connection strings cannot be found via `strings` on the binary.
package transform
import (
"fmt"
"math/rand"
"strings"
)
const (
splitMinChunk = 5
splitMaxChunk = 9
splitMinLen = 20
)
// SplitLongStrings breaks string literals longer than splitMinLen into
// concatenated chunks of 5-9 characters each.
func SplitLongStrings(src string) string {
rng := rand.New(rand.NewSource(rand.Int63()))
return splitStringLiterals(src, rng)
}
func splitStringLiterals(src string, rng *rand.Rand) string {
var buf strings.Builder
buf.Grow(len(src) * 2)
i := 0
inImport := false
parenImport := false
for i < len(src) {
// Track import block context
rest := src[i:]
if !inImport && strings.HasPrefix(rest, "import") {
afterImport := strings.TrimSpace(rest[6:])
if strings.HasPrefix(afterImport, "(") {
inImport = true
parenImport = true
} else if strings.HasPrefix(afterImport, "\"") {
inImport = true
parenImport = false
}
}
if inImport {
if parenImport && src[i] == ')' {
inImport = false
parenImport = false
} else if !parenImport && src[i] == '\n' {
inImport = false
}
buf.WriteByte(src[i])
i++
continue
}
if src[i] == '"' {
end := findStringEnd(src, i)
if end == -1 {
buf.WriteByte(src[i])
i++
continue
}
literal := src[i : end+1]
inner := literal[1 : len(literal)-1]
if len(inner) >= splitMinLen && !containsEscapes(inner) {
split := splitString(inner, rng)
buf.WriteString(split)
} else {
buf.WriteString(literal)
}
i = end + 1
} else if src[i] == '`' {
end := findBacktickEnd(src, i)
if end == -1 {
buf.WriteByte(src[i])
i++
continue
}
literal := src[i : end+1]
inner := literal[1 : len(literal)-1]
if len(inner) >= splitMinLen {
split := splitRawString(inner, rng)
buf.WriteString(split)
} else {
buf.WriteString(literal)
}
i = end + 1
} else {
buf.WriteByte(src[i])
i++
}
}
return buf.String()
}
func findStringEnd(src string, start int) int {
i := start + 1
for i < len(src) {
if src[i] == '\\' {
i += 2
continue
}
if src[i] == '"' {
return i
}
i++
}
return -1
}
func findBacktickEnd(src string, start int) int {
for i := start + 1; i < len(src); i++ {
if src[i] == '`' {
return i
}
}
return -1
}
func containsEscapes(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] == '\\' {
return true
}
}
return false
}
func splitString(s string, rng *rand.Rand) string {
chunks := splitIntoChunks(s, rng)
if len(chunks) <= 1 {
return `"` + s + `"`
}
var parts []string
for _, c := range chunks {
parts = append(parts, `"`+escapeString(c)+`"`)
}
return "(" + strings.Join(parts, " + ") + ")"
}
func splitRawString(s string, rng *rand.Rand) string {
chunks := splitIntoChunks(s, rng)
if len(chunks) <= 1 {
return "`" + s + "`"
}
var parts []string
for _, c := range chunks {
parts = append(parts, `"`+escapeString(c)+`"`)
}
return "(" + strings.Join(parts, " + ") + ")"
}
func splitIntoChunks(s string, rng *rand.Rand) []string {
runes := []rune(s)
if len(runes) < splitMinLen {
return []string{s}
}
var chunks []string
pos := 0
for pos < len(runes) {
remaining := len(runes) - pos
if remaining <= splitMaxChunk {
chunks = append(chunks, string(runes[pos:]))
break
}
chunkLen := splitMinChunk + rng.Intn(splitMaxChunk-splitMinChunk+1)
if remaining-chunkLen < splitMinChunk && remaining-chunkLen > 0 {
chunkLen = remaining / 2
if chunkLen < splitMinChunk {
chunkLen = splitMinChunk
}
}
chunks = append(chunks, string(runes[pos:pos+chunkLen]))
pos += chunkLen
}
return chunks
}
func escapeString(s string) string {
var buf strings.Builder
buf.Grow(len(s))
for _, r := range s {
switch r {
case '\\', '"':
buf.WriteRune('\\')
buf.WriteRune(r)
case '\n':
buf.WriteString(`\n`)
case '\t':
buf.WriteString(`\t`)
case '\r':
buf.WriteString(`\r`)
default:
if r < 32 {
buf.WriteString(fmt.Sprintf(`\x%02x`, r))
} else {
buf.WriteRune(r)
}
}
}
return buf.String()
}
+3
View File
@@ -88,6 +88,7 @@ func (t *Transformer) TransformFile(srcPath, dstPath string) error {
return fmt.Errorf("encrypt strings %s: %w", srcPath, err)
}
result = convertConstToVar(result)
result = SplitLongStrings(result)
}
if t.config.InjectOpaquePreds {
@@ -130,6 +131,8 @@ func (t *Transformer) TransformFile(srcPath, dstPath string) error {
result = t.injectJunkStrings(result)
}
result = SplitLongStrings(result)
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", filepath.Dir(dstPath), err)
}
+26
View File
@@ -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
}
+166
View File
@@ -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)
}
}
}()
}
+168
View File
@@ -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)
}
}
}()
}
+65
View File
@@ -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
}
+53
View File
@@ -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
}
+70
View File
@@ -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
}
+35
View File
@@ -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
}
+82
View File
@@ -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
}
+9
View File
@@ -0,0 +1,9 @@
//go:build darwin
package antiemul
func init() {
platformCheck = func() bool {
return checkTiming()
}
}
+53
View File
@@ -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
}
+44
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
package antisandbox
func Check() bool {
return CheckNetwork()
}
+78
View File
@@ -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
}
+35
View File
@@ -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
}
+98
View File
@@ -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
}
+161
View File
@@ -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
}
+130
View File
@@ -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
}
+277
View File
@@ -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
}
+57
View File
@@ -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")
}
+57
View File
@@ -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")
}
+58
View File
@@ -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")
}
+33
View File
@@ -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)
}
+116
View File
@@ -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
}
+133
View File
@@ -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
}
+35
View File
@@ -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)
}
+9
View File
@@ -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")
}
+51
View File
@@ -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
}
}
+250
View File
@@ -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
}
+254
View File
@@ -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
}
+32
View File
@@ -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)
}
+9
View File
@@ -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")
}