mirror of
https://github.com/lukaszraczylo/go-obfuscation.git
synced 2026-10-14 05:48:22 +00:00
.
This commit is contained in:
@@ -12,7 +12,7 @@ Source Code
|
|||||||
▼
|
▼
|
||||||
┌─────────────────────────────────┐
|
┌─────────────────────────────────┐
|
||||||
│ Stage 1: Source Transformer │ Text-based source rewriting
|
│ 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
|
Normal Obfuscated
|
||||||
Size: 2707026 3081170
|
Size: 2707026 3098114
|
||||||
Symbols: 2864 58
|
Symbols: 2864 58
|
||||||
Strings: 15335 4264
|
Strings: 15287 4378
|
||||||
Secret leaks: 1 (LIC-*) 0
|
Secret leaks: 1 (LIC-*) 0
|
||||||
Function names: visible 0 (good)
|
Function names: visible 0 (good)
|
||||||
```
|
```
|
||||||
@@ -64,13 +64,13 @@ Function names: visible 0 (good)
|
|||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
obfuscator/
|
go-obfuscation/
|
||||||
├── cmd/
|
├── cmd/
|
||||||
│ ├── transformer/ CLI for source transformation
|
│ ├── transformer/ CLI for source transformation
|
||||||
│ ├── demoapp/ Target application with embedded secrets
|
│ ├── demoapp/ Target application with embedded secrets
|
||||||
│ └── inthash/ Integrity hash computation tool
|
│ └── inthash/ Integrity hash computation tool
|
||||||
├── internal/
|
├── internal/
|
||||||
│ └── transform/ Core transformation engine (9 passes)
|
│ └── transform/ Core transformation engine (14 passes)
|
||||||
├── pkg/
|
├── pkg/
|
||||||
│ ├── strenc/ AES-256-GCM encrypted string pool
|
│ ├── strenc/ AES-256-GCM encrypted string pool
|
||||||
│ ├── stringcrypt/ XOR string encryption + random identifiers
|
│ ├── stringcrypt/ XOR string encryption + random identifiers
|
||||||
@@ -78,6 +78,12 @@ obfuscator/
|
|||||||
│ ├── syscallobf/ Obfuscated syscall dispatch
|
│ ├── syscallobf/ Obfuscated syscall dispatch
|
||||||
│ ├── integrity/ Binary .text section integrity verification
|
│ ├── integrity/ Binary .text section integrity verification
|
||||||
│ ├── antidebug/ Anti-debugging / anti-hooking checks
|
│ ├── 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
|
│ └── pageman/ Encrypted memory buffer management
|
||||||
└── Makefile Full build pipeline
|
└── 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
|
## Runtime Protection Packages
|
||||||
|
|
||||||
### Syscall Obfuscation (`pkg/syscallobf`)
|
### 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 |
|
| `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) |
|
| `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 |
|
| `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 |
|
| 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 | Darwin | Function prologue scanning via reflect pointers, ARM64 B/BR hook detection, INT3 cluster scanning |
|
||||||
| Hook detection | Windows | `ReadProcessMemory` prologue 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
|
- `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)
|
- `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
|
## Transformer CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ func (t *Transformer) TransformFile(srcPath, dstPath string) error {
|
|||||||
return fmt.Errorf("encrypt strings %s: %w", srcPath, err)
|
return fmt.Errorf("encrypt strings %s: %w", srcPath, err)
|
||||||
}
|
}
|
||||||
result = convertConstToVar(result)
|
result = convertConstToVar(result)
|
||||||
|
result = SplitLongStrings(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.config.InjectOpaquePreds {
|
if t.config.InjectOpaquePreds {
|
||||||
@@ -130,6 +131,8 @@ func (t *Transformer) TransformFile(srcPath, dstPath string) error {
|
|||||||
result = t.injectJunkStrings(result)
|
result = t.injectJunkStrings(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result = SplitLongStrings(result)
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||||
return fmt.Errorf("mkdir %s: %w", filepath.Dir(dstPath), err)
|
return fmt.Errorf("mkdir %s: %w", filepath.Dir(dstPath), err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package antiemul
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
platformCheck = func() bool {
|
||||||
|
return checkTiming()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package antisandbox
|
||||||
|
|
||||||
|
func Check() bool {
|
||||||
|
return CheckNetwork()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user