mirror of
https://github.com/lukaszraczylo/go-obfuscation.git
synced 2026-10-12 05:32:33 +00:00
.
This commit is contained in:
@@ -12,7 +12,7 @@ Source Code
|
||||
▼
|
||||
┌─────────────────────────────────┐
|
||||
│ Stage 1: Source Transformer │ Text-based source rewriting
|
||||
│ (cmd/transformer) │ 9 obfuscation passes
|
||||
│ (cmd/transformer) │ 14 obfuscation passes
|
||||
└─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
@@ -54,9 +54,9 @@ make transformer
|
||||
|
||||
```
|
||||
Normal Obfuscated
|
||||
Size: 2707026 3081170
|
||||
Size: 2707026 3098114
|
||||
Symbols: 2864 58
|
||||
Strings: 15335 4264
|
||||
Strings: 15287 4378
|
||||
Secret leaks: 1 (LIC-*) 0
|
||||
Function names: visible 0 (good)
|
||||
```
|
||||
@@ -64,13 +64,13 @@ Function names: visible 0 (good)
|
||||
## Architecture
|
||||
|
||||
```
|
||||
obfuscator/
|
||||
go-obfuscation/
|
||||
├── cmd/
|
||||
│ ├── transformer/ CLI for source transformation
|
||||
│ ├── demoapp/ Target application with embedded secrets
|
||||
│ └── inthash/ Integrity hash computation tool
|
||||
├── internal/
|
||||
│ └── transform/ Core transformation engine (9 passes)
|
||||
│ └── transform/ Core transformation engine (14 passes)
|
||||
├── pkg/
|
||||
│ ├── strenc/ AES-256-GCM encrypted string pool
|
||||
│ ├── stringcrypt/ XOR string encryption + random identifiers
|
||||
@@ -78,6 +78,12 @@ obfuscator/
|
||||
│ ├── syscallobf/ Obfuscated syscall dispatch
|
||||
│ ├── integrity/ Binary .text section integrity verification
|
||||
│ ├── antidebug/ Anti-debugging / anti-hooking checks
|
||||
│ ├── antidbi/ Frida/DynamoRIO/Pin detection
|
||||
│ ├── antivm/ VMware/VirtualBox/QEMU detection
|
||||
│ ├── antiemul/ Emulator detection (timing, CPU, memory)
|
||||
│ ├── antisandbox/ Sandbox detection (network, environment)
|
||||
│ ├── pclntab/ gopclntab section corruption
|
||||
│ ├── typewipe/ Go type metadata destruction
|
||||
│ └── pageman/ Encrypted memory buffer management
|
||||
└── Makefile Full build pipeline
|
||||
```
|
||||
@@ -286,6 +292,70 @@ func checkLicense(args ...interface{}) interface{} {
|
||||
}
|
||||
```
|
||||
|
||||
### 13. MBA (Mixed Boolean-Arithmetic) Expressions
|
||||
|
||||
Replaces simple arithmetic and bitwise operations with equivalent complex expressions.
|
||||
|
||||
```go
|
||||
// Before:
|
||||
x = a + b
|
||||
|
||||
// After:
|
||||
x = ((a ^ b) + 2 * (a & b))
|
||||
|
||||
// Other identities:
|
||||
// a - b → (a ^ b) - 2 * ((^a) & b)
|
||||
// a ^ b → (a | b) - (a & b)
|
||||
// a & b → (a | b) - (a ^ b)
|
||||
// a | b → (a ^ b) + (a & b)
|
||||
// ^a → (-a) - 1
|
||||
```
|
||||
|
||||
Each identity is randomly selected per occurrence. Requires symbolic execution or MBA-Blast-level simplification to reduce.
|
||||
|
||||
### 14. Constant Blinding
|
||||
|
||||
Replaces integer constants with runtime XOR decryption so no literal values appear in the binary.
|
||||
|
||||
```go
|
||||
// Before:
|
||||
x = 42
|
||||
|
||||
// After:
|
||||
x = _blind(0xDEADBEEF12345678, 0xDEADBEEF1234565A) // key ^ (key ^ 42)
|
||||
|
||||
//go:noinline
|
||||
func _blind(a, b uint64) uint64 { return a ^ b }
|
||||
```
|
||||
|
||||
Each constant gets a unique random key. Skips 0, 1, 2 and single-digit values.
|
||||
|
||||
### 15. Long String Splitting
|
||||
|
||||
Splits string literals ≥20 characters into randomized 5-9 character chunks joined at runtime.
|
||||
|
||||
```go
|
||||
// Before:
|
||||
key := "sk-proj-FAKE-KEY-1234567890abcdef"
|
||||
|
||||
// After:
|
||||
key := ("sk-pr" + "oj-FA" + "KE-KE" + "Y-123" + "45678" + "90abc" + "def")
|
||||
```
|
||||
|
||||
Ensures `strings` on the binary cannot find contiguous sensitive values like API keys, GPG keys, or connection strings. Skips import paths and strings with escape sequences.
|
||||
|
||||
### 16. Function Splitting
|
||||
|
||||
Splits function bodies with 8+ statements into fragments dispatched via a state machine.
|
||||
|
||||
### 17. Basic Block Reordering
|
||||
|
||||
Swaps if/else branches (negating conditions) and shuffles switch cases randomly.
|
||||
|
||||
### 18. Fake Function Signatures
|
||||
|
||||
Injects 2-5 realistic-but-dead `//go:noinline` functions per file to confuse disassembler function boundary detection.
|
||||
|
||||
## Runtime Protection Packages
|
||||
|
||||
### Syscall Obfuscation (`pkg/syscallobf`)
|
||||
@@ -313,6 +383,10 @@ The anti-debugging package uses these for stealthy `/proc/self/mem` and `/proc/s
|
||||
| `checkParentProcess()` | Linux | Reads `/proc/<ppid>/comm`, matches against 12 debugger names |
|
||||
| `checkFunctionPrologues()` | Linux | Reads `/proc/self/mem` at function addresses, checks for JMP hooks (0xE9, 0xEB, 0xFF 0x25) and INT3 breakpoints (0xCC) |
|
||||
| `scanForBreakpoints()` | Linux | Parses `/proc/self/maps`, reads executable regions, flags ≥8 consecutive 0xCC bytes |
|
||||
| `checkHardwareBreakpoints()` | Linux | Reads DR0-DR7 debug registers via ptrace child process |
|
||||
| `checkHardwareBreakpoints()` | Windows | `GetThreadContext` with `CONTEXT_DEBUG_REGISTERS` flag |
|
||||
| `checkSignalTrap()` | Linux/Darwin | SIGTRAP self-send; if handler doesn't fire, debugger intercepted it |
|
||||
| `checkReturnAddresses()` | All | Verifies stack PCs are within binary's code section; flags frida/dynamorio/hook names |
|
||||
| Windows anti-debug | Windows | 5 API-based checks + PEB direct read + hardware BP scan + ThreadHideFromDebugger |
|
||||
| Hook detection | Darwin | Function prologue scanning via reflect pointers, ARM64 B/BR hook detection, INT3 cluster scanning |
|
||||
| Hook detection | Windows | `ReadProcessMemory` prologue scanning |
|
||||
@@ -361,6 +435,81 @@ integrity.StartMonitor(30*time.Second, func(err error) {
|
||||
- `enforceAutoLock()` — re-locks buffers that exceeded their `autoLock` duration
|
||||
- `checkMapsTampering()` — compares `/proc/self/maps` snapshots; force-locks all buffers if the memory map changed (detects debugger attachment / memory mapping modifications)
|
||||
|
||||
### Frida/DBI Detection (`pkg/antidbi`)
|
||||
|
||||
Detects Dynamic Binary Instrumentation frameworks (Frida, DynamoRIO, Intel Pin).
|
||||
|
||||
| Method | Platform | Description |
|
||||
|--------|----------|-------------|
|
||||
| Memory maps | Linux | Scans `/proc/self/maps` for frida-agent, frida-gadget, dynamorio, libpin |
|
||||
| Unix sockets | Linux | Scans `/proc/net/unix` for frida/linjector entries |
|
||||
| Frida port | All | TCP connect to 127.0.0.1:27042 (Frida default) |
|
||||
| Thread names | Linux | Reads `/proc/self/task/*/comm` for gmain, frida |
|
||||
| Loaded images | Darwin | Checks DYLD_INSERT_LIBRARIES + scans lib dirs for frida dylibs |
|
||||
| Mach ports | Darwin | Checks for re.frida.server, re.frida.agent Mach ports |
|
||||
| Environment | All | Checks LD_PRELOAD, DYLD_* vars for frida/dynamorio/pin |
|
||||
|
||||
`AntiAttach()` background goroutine polls every 500ms and exits on detection.
|
||||
|
||||
### VM Environment Detection (`pkg/antivm`)
|
||||
|
||||
Detects virtual machine environments to prevent sandbox analysis.
|
||||
|
||||
| Method | Platform | Description |
|
||||
|--------|----------|-------------|
|
||||
| DMI/SMBIOS | Linux | Reads `/sys/class/dmi/id/{sys_vendor,product_name}` for VMware/VirtualBox/QEMU/KVM/Xen/Parallels |
|
||||
| MAC prefixes | All | Checks `00:0c:29` (VMware), `08:00:27` (VirtualBox), `52:54:00` (QEMU), `00:1c:42` (Parallels) |
|
||||
| CPUID | Linux | Checks `/proc/cpuinfo` for hypervisor flag and QEMU CPU model strings |
|
||||
| Disk devices | Linux | Reads `/sys/block/*/device/model` for VM disk names |
|
||||
| PCI vendors | Linux | Checks vendor IDs: `15ad` (VMware), `80ee` (VirtualBox), `1234` (QEMU) |
|
||||
| Timing | All | 100k tight loop; >10ms indicates VM exit overhead |
|
||||
| Registry | Windows | Checks `HKLM\SYSTEM\CurrentControlSet\Services\Disk\Enum` |
|
||||
| IOKit | Darwin | Checks `system_profiler` and `ioreg` output for VM devices |
|
||||
|
||||
### Anti-Emulation Detection (`pkg/antiemul`)
|
||||
|
||||
Detects emulated environments via timing and system characteristics.
|
||||
|
||||
- **Timing:** 1M iterations must complete in <500ms (emulators are 10-100x slower)
|
||||
- **Memory:** System RAM <2GB is suspicious
|
||||
- **Uptime:** System uptime <60s suggests freshly booted sandbox
|
||||
- **Core count:** <2 CPU cores is suspicious
|
||||
- **CPU model:** Checks `/proc/cpuinfo` for QEMU/Virtual/KVM strings (Linux)
|
||||
|
||||
### Sandbox Detection (`pkg/antisandbox`)
|
||||
|
||||
Detects sandbox-like network and environment characteristics.
|
||||
|
||||
- **Interface count:** ≤1 network interface is suspicious
|
||||
- **MAC prefixes:** VM-specific MAC address prefixes
|
||||
- **Hostname:** Checks for sandbox/cuckoo/malware/analysis in hostname
|
||||
- **ARP table:** Empty `/proc/net/arp` is suspicious (Linux)
|
||||
|
||||
### gopclntab Corruption (`pkg/pclntab`)
|
||||
|
||||
Corrupts the `.gopclntab` section — the #1 tool for Go binary reverse engineering. Breaks IDA/Ghidra Go plugins, GoReSym, and `go tool objdump`.
|
||||
|
||||
**Post-compilation (`CorruptPclntab`):**
|
||||
- Parses ELF/Mach-O/PE to find `.gopclntab` section
|
||||
- XOR-encrypts function name strings with a random key
|
||||
- Optionally scrambles magic header bytes and injects fake entries
|
||||
|
||||
**Runtime (`RuntimeCorrupt`):**
|
||||
- Reads `/proc/self/exe` (Linux) or binary path (macOS)
|
||||
- Parses ELF/Mach-O headers in memory to locate pclntab
|
||||
- Uses `mprotect` to make section writable, XOR-encrypts names in-place
|
||||
- Handles all 3 Go pclntab magic variants (Go 1.2+, 1.16+, 1.18+)
|
||||
|
||||
### Type Information Stripping (`pkg/typewipe`)
|
||||
|
||||
Destroys Go type metadata that tools like Ghidra's Go type recovery depend on.
|
||||
|
||||
- **`.typelink`** section: Zeroed out (type linkage info)
|
||||
- **`.itablink`** section: XOR-encrypted (interface method tables)
|
||||
- **pclntab names:** Type name strings encrypted via mprotect + XOR
|
||||
|
||||
**Warning:** Degrades reflection and panic output. Call during `init()` after the runtime has used type info for setup.
|
||||
|
||||
## Transformer CLI
|
||||
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
result = convertConstToVar(result)
|
||||
result = SplitLongStrings(result)
|
||||
}
|
||||
|
||||
if t.config.InjectOpaquePreds {
|
||||
@@ -130,6 +131,8 @@ func (t *Transformer) TransformFile(srcPath, dstPath string) error {
|
||||
result = t.injectJunkStrings(result)
|
||||
}
|
||||
|
||||
result = SplitLongStrings(result)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
|
||||
return fmt.Errorf("mkdir %s: %w", filepath.Dir(dstPath), err)
|
||||
}
|
||||
|
||||
@@ -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