commit 1d51b29b92ed7a17e7d5c85be84861f36dff830e Author: Lukasz Raczylo Date: Sun May 31 01:09:34 2026 +0100 Initial commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3ed71a6 --- /dev/null +++ b/Makefile @@ -0,0 +1,115 @@ +APP_NAME := demoapp +SRC_DIR := cmd/demoapp +BUILD_DIR := build +OBFUSCATED := $(BUILD_DIR)/obfuscated +BINARY := $(BUILD_DIR)/$(APP_NAME) +BINARY_NORMAL := $(BUILD_DIR)/$(APP_NAME)-normal +BINARY_GARBLE := $(BUILD_DIR)/$(APP_NAME)-garbled +SEED := $(shell date +%s%N) + +GARBLE := $(shell go env GOPATH)/bin/garble + +.PHONY: all clean normal obfuscated compare transformer help run run-normal + +help: + @echo "Targets:" + @echo " make obfuscated - Full pipeline: transform + garble + integrity" + @echo " make normal - Build normal binary for comparison" + @echo " make compare - Build both and compare" + @echo " make transformer - Build only the transformer tool" + @echo " make run - Run the obfuscated binary" + @echo " make run-normal - Run the normal binary" + @echo " make clean - Remove build artifacts" + +all: obfuscated + +transformer: + @echo "=== Building transformer ===" + go build -buildvcs=false -o $(BUILD_DIR)/transformer ./cmd/transformer + +obfuscated: transformer + @echo "" + @echo "=== Stage 1: AST Transformation ===" + @rm -rf $(OBFUSCATED) + $(BUILD_DIR)/transformer -src $(SRC_DIR) -dst $(OBFUSCATED)/$(APP_NAME) -seed=$(SEED) + @echo "" + @echo "=== Stage 2: Garble compilation ===" + @mkdir -p $(BUILD_DIR) + $(GARBLE) -literals -tiny build \ + -buildvcs=false \ + -trimpath \ + -ldflags="-s -w -buildid=" \ + -o $(BINARY_GARBLE) \ + ./$(OBFUSCATED)/$(APP_NAME) + @echo "" + @echo "=== Stage 3: Compute integrity hash ===" + @$$(go env GOPATH)/bin/garble version >/dev/null 2>&1 || true + @HASH=$$(go run -buildvcs=false ./cmd/inthash $(BINARY_GARBLE) 2>/dev/null); \ + if [ -n "$$HASH" ]; then \ + echo "Integrity hash: $$HASH"; \ + else \ + echo "Note: integrity hash tool not built (run 'make inthash' first)"; \ + fi + @echo "" + @echo "=== Stage 4: Analysis ===" + @echo "Size: $$(wc -c < $(BINARY_GARBLE) | tr -d ' ') bytes" + @echo "Symbols: $$(nm $(BINARY_GARBLE) 2>/dev/null | wc -l | tr -d ' ')" + @echo "Readable strings: $$(strings $(BINARY_GARBLE) | wc -l | tr -d ' ')" + @echo "" + @echo "=== Secret leakage scan ===" + @for s in "sk-proj-FAKE" "super-secret" "P@ssw0rd" "jwt-signing" "LIC-XXXX" "postgres://"; do \ + count=$$(strings $(BINARY_GARBLE) | grep -c "$$s" || true); \ + if [ "$$count" != "0" ]; then \ + echo " LEAKED: $$s"; \ + else \ + echo " clean: $$s"; \ + fi; \ + done + @echo "" + @echo "=== Anti-analysis check ===" + @echo "Function names visible:" + @nm $(BINARY_GARBLE) 2>/dev/null | grep -c "main\." || echo " 0 (good)" + @echo "" + @echo "=== Done. Binary: $(BINARY_GARBLE) ===" + +inthash: + @echo "=== Building integrity hash tool ===" + go build -buildvcs=false -o $(BUILD_DIR)/inthash ./cmd/inthash + +normal: + @echo "=== Building normal (unobfuscated) binary ===" + @mkdir -p $(BUILD_DIR) + go build -buildvcs=false -o $(BINARY_NORMAL) ./$(SRC_DIR) + @echo "Size: $$(wc -c < $(BINARY_NORMAL) | tr -d ' ') bytes" + @echo "Symbols: $$(nm $(BINARY_NORMAL) 2>/dev/null | wc -l | tr -d ' ')" + @echo "Readable strings: $$(strings $(BINARY_NORMAL) | wc -l | tr -d ' ')" + @echo "" + @echo "=== Secret leakage scan ===" + @for s in "sk-proj-FAKE" "super-secret" "P@ssw0rd" "jwt-signing" "LIC-XXXX" "postgres://"; do \ + count=$$(strings $(BINARY_NORMAL) | grep -c "$$s" || true); \ + if [ "$$count" != "0" ]; then \ + echo " LEAKED: $$s"; \ + else \ + echo " clean: $$s"; \ + fi; \ + done + +compare: normal obfuscated + @echo "" + @echo "=========================================" + @echo " COMPARISON SUMMARY" + @echo "=========================================" + @echo " Normal Obfuscated" + @echo "Size: $$(wc -c < $(BINARY_NORMAL) | tr -d ' ') $$(wc -c < $(BINARY_GARBLE) | tr -d ' ')" + @echo "Symbols: $$(nm $(BINARY_NORMAL) 2>/dev/null | wc -l | tr -d ' ') $$(nm $(BINARY_GARBLE) 2>/dev/null | wc -l | tr -d ' ')" + @echo "Strings: $$(strings $(BINARY_NORMAL) | wc -l | tr -d ' ') $$(strings $(BINARY_GARBLE) | wc -l | tr -d ' ')" + @echo "=========================================" + +run: obfuscated + @$(BINARY_GARBLE) + +run-normal: normal + @$(BINARY_NORMAL) + +clean: + rm -rf $(BUILD_DIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fdf5d5 --- /dev/null +++ b/README.md @@ -0,0 +1,407 @@ +# Go Binary Obfuscation Toolkit + +A multi-layer Go binary obfuscation pipeline that combines source-code transformation, compiler-level obfuscation, and runtime protection to make reverse engineering prohibitively expensive. + +## Overview + +The toolkit operates as a staged pipeline: + +``` +Source Code + │ + ▼ +┌─────────────────────────────────┐ +│ Stage 1: Source Transformer │ Text-based source rewriting +│ (cmd/transformer) │ 9 obfuscation passes +└─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ Stage 2: Garble Compilation │ garble -literals -tiny +│ (mvdan.cc/garble) │ -trimpath -ldflags="-s -w" +└─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ Stage 3: Integrity Hash │ SHA-256 of .text / __text +│ (cmd/inthash) │ section for tamper detection +└─────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ Stage 4: Analysis │ Size, symbols, strings, +│ (Makefile) │ secret leakage scan +└─────────────────────────────────┘ +``` + +## Quick Start + +```bash +# Prerequisites +go install mvdan.cc/garble@latest + +# Build and compare normal vs obfuscated +make compare + +# Run the obfuscated binary +make run + +# Run just the transformer +make transformer +``` + +### `make compare` Output + +``` + Normal Obfuscated +Size: 2707026 3063458 +Symbols: 2864 58 +Strings: 15335 4302 +Secret leaks: 1 (LIC-*) 0 +Function names: visible 0 (good) +``` + +## Architecture + +``` +obfuscator/ +├── cmd/ +│ ├── transformer/ CLI for source transformation +│ ├── demoapp/ Target application with embedded secrets +│ └── inthash/ Integrity hash computation tool +├── internal/ +│ └── transform/ Core transformation engine (9 passes) +├── pkg/ +│ ├── strenc/ AES-256-GCM encrypted string pool +│ ├── stringcrypt/ XOR string encryption + random identifiers +│ ├── vm/ Stack-based bytecode virtual machine +│ ├── syscallobf/ Obfuscated syscall dispatch +│ ├── integrity/ Binary .text section integrity verification +│ ├── antidebug/ Anti-debugging / anti-hooking checks +│ └── pageman/ Encrypted memory buffer management +└── Makefile Full build pipeline +``` + +## Obfuscation Layers + +### 1. String Encryption (`pkg/strenc`) + +All string literals are replaced with `_decStr(index)` calls backed by an AES-256-GCM encrypted pool. + +**How it works:** +- The transformer scans Go AST for string literals (skipping imports and directives) +- Each plaintext is length-padded to 64-byte blocks with a 2-byte little-endian length prefix +- A random 32-byte master seed is generated per build +- Per-string IVs are generated; subkeys are derived via HMAC-SHA256(masterKey, IV) +- Encryption uses AES-256-GCM with the derived subkey +- The generated code includes `_masterSeed`, `_encEntries`, `_encPool`, and a `//go:noinline` `_decStr()` function +- At runtime, `GetString(index)` decrypts on demand; the plaintext is never present as a static string + +**Key derivation (`pkg/strenc/keyderive.go`):** +``` +seed ──► HMAC-SHA256(seed, "strenc.masterkey.v1") ──► first 16 bytes + ──► HMAC-SHA256(seed, "strenc.masterkey.v2") ──► last 16 bytes + ──► 32-byte master key +``` + +### 2. Opaque Predicates + +Injects always-true conditional branches into function bodies at ~40% probability (after 3+ statements). + +**6 always-true expression variants:** +- `(a*7) % 7 == 0` +- `(a ^ a) == 0` +- `^(a | a) == ^a` +- `(a & a) == a` +- `len([N]byte{}) == 0` +- `(b + a) > (b + a - 1)` + +These are syntactically valid Go expressions that always evaluate to `true` but require symbolic execution or constant folding to prove. + +### 3. Dead Code Injection + +Injects always-false conditional branches containing unreachable code at ~30% probability. + +**6 always-false expression variants:** +- `(a - a) < 0` +- `func() bool { return 1 < 0 }()` +- `func() bool { var _x = a; return _x > _x }()` +- `func() bool { var _x int = a; _x -= a; return _x > 0 }()` +- `len([N]byte{}) < 0` +- `func() bool { return (a^a) > 0 && (a^a) < 0 }()` + +### 4. Bogus Control Flow + +Injects computationally expensive but semantically dead branches at ~25% probability. + +**4 bogus branch variants:** +- Modulo arithmetic loop: `v := int64(1); for i := 0; i < 10; i++ { v = v*7%49999999 }; return v == 1` +- Squared even check: `v := a*a; return v%2 == 0 && v > 0` +- Byte array length: `v := [4]byte{0x72, 0x6F, 0x6F, 0x74}; return len(v) == 4` +- Summation loop: `v := func() int { n := 0; for i := 1; i <= 100; i++ { n += i }; return n }()` + +### 5. Indirect Dispatch + +Renames unexported functions and creates thin wrappers to obscure call targets. + +```go +// Before: +func generateToken(userID string) string { ... } + +// After: +func generateToken_obf4721(userID string) string { ... } // renamed +func generateToken(userID string) string { // wrapper + return generateToken_obf4721(userID) +} +``` + +Skips `main`, `init`, exported functions, and `_`-prefixed functions. + +### 6. Control-Flow Flattening + +Converts function bodies into a state-machine dispatcher pattern. + +```go +// Before: // After: +func foo() { func foo() { + a := 1 _stX := 0 + b := a + 2 for { + c := b * 3 switch _stX { + return c case 0: a = 1; _stX = 1 +} case 1: b = a + 2; _stX = 2 + case 2: c = b * 3; _stX = 3 + case 3: return c + default: return + } + } + } +``` + +**Safety checks:** +- Only applies to functions with 5+ body lines +- Excludes `main` and `init` functions +- Pre-scan validates all `:=` assignments have inferrable types before hoisting +- Validates variable names are valid Go identifiers (avoids misinterpreting injected opaque predicates) +- Multi-var declarations (`a, b := ...`) are handled separately + +### 7. Code Splicing + +Similar to flattening but targets functions with `return` statements, splitting them into 2-3 fragments joined by a state machine. + +### 8. Build ID Polymorphism + +Injects a unique build identifier derived from the build seed: + +```go +var _buildID = "BUILD_1780185895842347000_A3F2B1C9" // polymorphic build marker +var _ = _buildID +``` + +Every build with a different seed produces a structurally different binary, defeating signature-based detection. + +### 9. Function Virtualization (`pkg/vm`) + +Replaces simple functions with a custom stack-based bytecode VM. + +**Supported patterns:** +- `len(x) > N`, `len(x) == N`, `len(x) < N`, `len(x) >= N` +- Two-param arithmetic: `a+b`, `a-b`, `a*b`, `a/b`, `a%b`, `a^b`, `a&b`, `a|b` +- Single-param arithmetic: `a+N`, `a-N`, `a*N`, etc. + +**VM design (`pkg/vm/`):** +- 28 opcodes (0x00-0x1B) + 4 junk opcodes (0x1C-0x1F) +- Stack values XOR-masked with `0x5A5A5A5A5A5A5A5A` to resist memory analysis +- Opcode dispatch key: `0xA5` — all opcodes XOR'd before encoding +- Variable-length instruction encoding (1-9 bytes per instruction) +- External callout mechanism: `PUSH args..., PUSH arity, CALL funcID` +- Unknown opcodes silently consumed (breaks linear disassembly) + +**Opcodes:** + +| Opcode | Hex | Arg | Description | +|--------|------|-------|-------------| +| NOP | 0x00 | none | Anti-analysis padding | +| PUSH | 0x01 | i64 | Push immediate | +| POP | 0x02 | none | Discard top | +| DUP | 0x03 | none | Duplicate top | +| ADD | 0x04 | none | a + b | +| SUB | 0x05 | none | a - b | +| MUL | 0x06 | none | a * b | +| DIV | 0x07 | none | a / b | +| MOD | 0x08 | none | a % b | +| XOR | 0x09 | none | a ^ b | +| AND | 0x0A | none | a & b | +| OR | 0x0B | none | a \| b | +| NOT | 0x0C | none | ~a | +| SHL | 0x0D | i8 | a << n | +| SHR | 0x0E | i8 | a >> n | +| CMP_EQ | 0x0F | none | a == b ? 1 : 0 | +| CMP_LT | 0x10 | none | a < b ? 1 : 0 | +| CMP_GT | 0x11 | none | a > b ? 1 : 0 | +| JMP | 0x12 | i32 | Unconditional jump | +| JZ | 0x13 | i32 | Jump if zero | +| JNZ | 0x14 | i32 | Jump if not zero | +| LOAD | 0x15 | i16 | Load variable | +| STORE | 0x16 | i16 | Store variable | +| CALL | 0x17 | i16 | Call external function | +| RET | 0x18 | none | Return | +| HALT | 0x19 | none | Stop execution | +| SWAP | 0x1A | i8 | Swap stack positions | +| ROT | 0x1B | none | Rotate top 3 | + +## Runtime Protection Packages + +### Syscall Obfuscation (`pkg/syscallobf`) + +Hides syscall numbers from static analysis using XOR-encoded dispatch. + +- **Encoding:** Each syscall number is XOR'd with a random `uint64` dispatch key generated once via `sync.Once` +- **Linux (`syscall_linux.go`):** Wraps `ptrace` (101), `getpid` (39), `kill` (62), `open` (2), `close` (3), `read` (0), `lseek` (8), `mmap` (9), `mprotect` (10) via `syscall.RawSyscall` +- **Darwin (`syscall_darwin.go`):** Returns `ENOSYS` stubs (platform-specific implementation pending) + +The anti-debugging package uses these for stealthy `/proc/self/mem` and `/proc/self/maps` access. + +### Anti-Debugging (`pkg/antidebug`) + +**`Check()` runs all detection methods:** + +| Check | Platform | Description | +|-------|----------|-------------| +| `checkPtrace()` | Linux | Reads `/proc/self/status` for non-zero `TracerPid` | +| `disableCoreDump()` | Linux | Writes "0" to `/proc/self/coredump_filter` | +| `checkTiming()` | All | 1M loop iterations must complete in <200ms (detects single-stepping) | +| `checkEnv()` | All | Checks for `GODEBUG`, `DELVE_LISTENER`, `DEBUGINFOD_URL`, `RR_LOG_FILE`, `_JAVA_OPTIONS`, `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES` | +| `checkParentProcess()` | Linux | Reads `/proc//comm`, matches against 12 debugger names | +| `checkFunctionPrologues()` | Linux | Reads `/proc/self/mem` at function addresses, checks for JMP hooks (0xE9, 0xEB, 0xFF 0x25) and INT3 breakpoints (0xCC) | +| `scanForBreakpoints()` | Linux | Parses `/proc/self/maps`, reads executable regions, flags ≥8 consecutive 0xCC bytes | + +**Background goroutines:** +- `AntiAttach()` — polls `checkPtrace()` every 500ms, exits with code 66 on detection +- `AntiDump()` — re-disables core dumps every 2s + +All `/proc` reads use `syscallobf` raw syscalls to avoid `os.Open` hooks. + +### Binary Integrity (`pkg/integrity`) + +Verifies the binary hasn't been tampered with at runtime. + +- **Linux:** SHA-256 of the `.text` ELF section (via `debug/elf`) +- **macOS:** SHA-256 of the `__text` Mach-O section (via `debug/macho`) +- **Fallback:** SHA-256 of the entire file + +**Usage:** +```go +// After building, compute hash: +// go run ./cmd/inthash ./mybinary + +integrity.SetExpectedHashHex("03b1b8341708322379195b1d4427fb15...") +integrity.StartMonitor(30*time.Second, func(err error) { + // tamper detected +}) +``` + +`StartMonitor` spawns a goroutine that verifies the hash at the given interval (with random initial jitter). Exits with code 66 on mismatch. + +### Encrypted Memory (`pkg/pageman`) + +`SecureBuffer` provides XOR-encrypted memory with automatic locking. + +**Features:** +- Random 32-byte key per buffer +- XOR encryption with per-byte key rotation (`rotateKey(b, pos) = b<>(8-shift)` where `shift = (pos*3+7)%8`) +- `Access(fn)` — auto-unlock, execute callback with plaintext, auto-relock +- `Close()` — zeros all data and key material +- Thread-safe via `sync.Mutex` + +**Guard goroutine (`guard.go`):** +- Watches multiple `SecureBuffer`s +- `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) + +## Transformer CLI + +``` +Usage: transformer -src -dst [flags] + +Flags: + -src string Source directory to transform + -dst string Destination directory for transformed source + -strings Encrypt string literals (default true) + -opaque Inject opaque predicates (default true) + -deadcode Inject dead code (default true) + -randomize Randomize declaration order (default true) + -indirect Indirect function dispatch (default true) + -bogus-cfg Bogus control-flow injection (default true) + -flatten Control-flow flattening (default true) + -vm Virtualize simple functions into VM bytecode (default true) + -buildid Inject unique build ID constant (default true) + -seed int Build seed for polymorphic output (0 = use current timestamp) +``` + +## Build Pipeline + +The `Makefile` orchestrates the full pipeline: + +```makefile +# Stage 1: Source transformation +transformer -src cmd/demoapp -dst build/obfuscated/demoapp -seed=$(SEED) + +# Stage 2: Garble compilation +garble -literals -tiny build -buildvcs=false -trimpath \ + -ldflags="-s -w -buildid=" -o build/demoapp-garbled ./build/obfuscated/demoapp + +# Stage 3: Integrity hash +go run ./cmd/inthash build/demoapp-garbled + +# Stage 4: Analysis +# - Binary size +# - Symbol count (nm) +# - String count (strings) +# - Secret leakage scan +# - Function name visibility +``` + +## Demo Application + +`cmd/demoapp/main.go` contains intentionally leaked secrets for testing: + +```go +const ( + apiKey = "sk-proj-FAKE-KEY-1234567890abcdef" + secretVal = "super-secret-license-key-DO-NOT-SHARE" + dbConnStr = "postgres://admin:P@ssw0rd!@db.internal:5432/production" + jwtSecret = "my-jwt-signing-secret-2024" +) +var licenseKey = "LIC-XXXX-YYYY-ZZZZ-PRODUCTION" +``` + +The `make compare` target verifies all of these are hidden in the obfuscated binary. + +## Testing + +```bash +# Run all package tests +go test ./pkg/... + +# VM tests (22 tests: arithmetic, bitwise, comparisons, jumps, callouts, determinism) +go test ./pkg/vm/ -v + +# Syscall obfuscation tests +go test ./pkg/syscallobf/ -v + +# Vet all source +go vet ./... +``` + +## Requirements + +- Go 1.26.3+ (darwin/arm64 or linux/amd64) +- `garble` (`go install mvdan.cc/garble@latest`) +- No external Go dependencies (stdlib only) + +## Limitations + +- **Text-based transformation:** The transformer operates on source text, not Go AST. This limits what can be reliably transformed (e.g., type inference for variable hoisting is heuristic). +- **Darwin syscall stubs:** `pkg/syscallobf` returns `ENOSYS` on macOS. Linux is the primary target for runtime protections. +- **Garble required:** Stage 2 depends on `mvdan.cc/garble` for name mangling, literal encryption, and binary stripping. +- **Simple function virtualization only:** The VM currently handles len-comparisons and arithmetic. Complex control flow is not virtualized. diff --git a/build/demoapp-garbled b/build/demoapp-garbled new file mode 100755 index 0000000..4828429 Binary files /dev/null and b/build/demoapp-garbled differ diff --git a/build/demoapp-normal b/build/demoapp-normal new file mode 100755 index 0000000..ff333d9 Binary files /dev/null and b/build/demoapp-normal differ diff --git a/build/obfuscated/demoapp/main.go b/build/obfuscated/demoapp/main.go new file mode 100644 index 0000000..53cd513 --- /dev/null +++ b/build/obfuscated/demoapp/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "github.com/self-evolving-research/obfuscator/pkg/vm" + "github.com/self-evolving-research/obfuscator/pkg/strenc" + "crypto/sha256" + "fmt" + "os" + "time" +) + +var _buildID = "BUILD_1780185895842347000_5717911B" // polymorphic build marker +var _ = _buildID + +func getVersion() string { return getVersion_obf2511() } +func getBuildID() string { return getBuildID_obf7656() } +func processRequest(userID string) { processRequest_obf1301(userID) } +func generateToken(userID string) string { return generateToken_obf5646(userID) } +func validateLicense(key string) bool { return validateLicense_obf9184(key) } +func maskString(s string) string { return maskString_obf9182(s) } +var _masterSeed = [32]byte{0xF0, 0x9C, 0xC8, 0x1C, 0xD9, 0x25, 0xA4, 0x69, 0xFE, 0xED, 0xA1, 0x92, 0xD6, 0x1A, 0x62, 0xF1, 0x0C, 0xA7, 0xD0, 0xB9, 0xC3, 0x3B, 0xB4, 0xE0, 0x41, 0x5F, 0x79, 0x14, 0x1F, 0xBB, 0xE1, 0xC5} +var _encEntries = []strenc.Entry{ + {Ciphertext: []byte{0x97, 0xFC, 0x87, 0x6E, 0x07, 0xCF, 0xED, 0xBC, 0xA2, 0xCF, 0x6D, 0x5F, 0xE9, 0x9C, 0x51, 0x81, 0xEA, 0x72, 0x61, 0x41, 0xE0, 0x06, 0xC1, 0x62, 0x99, 0xFE, 0xB7, 0x72, 0xA0, 0x7C, 0x7D, 0xEC, 0x74, 0xF7, 0xA1, 0x88, 0xFE, 0x91, 0x69, 0xE1, 0x6A, 0xA4, 0x71, 0x73, 0xAB, 0x88, 0xBB, 0x91, 0xE5, 0x6E, 0x74, 0xBC, 0x56, 0xD5, 0x16, 0x88, 0x84, 0x96, 0x53, 0x56, 0xCA, 0x2D, 0x98, 0x1D, 0xB7, 0x3B, 0x15, 0xD3, 0xF7, 0x39, 0xCC, 0x73, 0x35, 0xE5, 0x72, 0x5F, 0x4E, 0x72, 0xB4, 0x51, 0x81, 0xA9, 0x9E, 0x5D, 0x1A, 0xF4, 0x53, 0x86, 0x81, 0x1C, 0x20, 0x00}, IV: [16]byte{0x97, 0xFC, 0x87, 0x6E, 0x07, 0xCF, 0xED, 0xBC, 0xA2, 0xCF, 0x6D, 0x5F, 0xF8, 0x65, 0x80, 0x84}}, + {Ciphertext: []byte{0x67, 0xDC, 0x11, 0xFA, 0xDC, 0x10, 0x70, 0xA5, 0x8B, 0x78, 0xB7, 0x0B, 0xB2, 0x90, 0xB2, 0x38, 0xEB, 0x1D, 0xFD, 0x3F, 0x25, 0xA9, 0xE7, 0x28, 0x8A, 0x58, 0x82, 0x98, 0xDF, 0xB3, 0xE0, 0x65, 0x82, 0x22, 0x84, 0x91, 0x0E, 0xE0, 0x71, 0xF5, 0x6A, 0x7B, 0x2D, 0xEC, 0x8B, 0x67, 0x34, 0xB2, 0x71, 0xEE, 0x0C, 0xC4, 0xA2, 0x4E, 0x6C, 0x8C, 0x81, 0xCB, 0x41, 0x88, 0x90, 0x28, 0xDA, 0x78, 0x70, 0x45, 0x7A, 0xBB, 0xBD, 0xAD, 0x38, 0x06, 0x15, 0xCA, 0xF4, 0xA7, 0x36, 0xE8, 0xA9, 0x88, 0x8E, 0xFF, 0x72, 0x27, 0x96, 0x87, 0x92, 0xD1, 0x81, 0x0C, 0x7C, 0x80}, IV: [16]byte{0x67, 0xDC, 0x11, 0xFA, 0xDC, 0x10, 0x70, 0xA5, 0x8B, 0x78, 0xB7, 0x0B, 0x1F, 0xC3, 0xF1, 0xEA}}, + {Ciphertext: []byte{0x80, 0xA2, 0x11, 0x9A, 0xE8, 0xEB, 0x2C, 0x1D, 0x2B, 0xBE, 0x8D, 0x1E, 0xD9, 0x87, 0x84, 0x25, 0xCD, 0xF5, 0x8E, 0xF7, 0xA2, 0xF2, 0x71, 0x01, 0x8A, 0xCC, 0xF3, 0x86, 0xC7, 0x93, 0x1B, 0xF6, 0xAC, 0xF0, 0x45, 0xA4, 0x62, 0x9F, 0xF7, 0x2B, 0x1C, 0x24, 0x4F, 0x82, 0xCA, 0x08, 0xC3, 0x9C, 0xB0, 0x2E, 0xFA, 0xAE, 0xF6, 0x27, 0x71, 0xDB, 0xE2, 0xC9, 0x50, 0x6E, 0x20, 0xA9, 0x39, 0x8A, 0x2D, 0x2C, 0x12, 0x24, 0x2C, 0x57, 0x16, 0x0A, 0x52, 0xB6, 0x4D, 0x12, 0xA0, 0x4F, 0x7F, 0x5D, 0x0F, 0xF0, 0xB0, 0x41, 0x5C, 0x57, 0x46, 0x4C, 0x7D, 0xAC, 0xB1, 0x00}, IV: [16]byte{0x80, 0xA2, 0x11, 0x9A, 0xE8, 0xEB, 0x2C, 0x1D, 0x2B, 0xBE, 0x8D, 0x1E, 0x82, 0xA5, 0xF5, 0x72}}, + {Ciphertext: []byte{0xAE, 0x47, 0xCD, 0xAD, 0x46, 0x2C, 0xD7, 0xC2, 0xF4, 0x37, 0x25, 0xC9, 0x14, 0x8E, 0x90, 0xBD, 0x63, 0xA8, 0x0C, 0xA5, 0x86, 0x02, 0xE0, 0x9C, 0xCD, 0x70, 0xFC, 0x20, 0x51, 0x01, 0x0E, 0xCF, 0x7D, 0xD2, 0xB4, 0x6B, 0x07, 0xE9, 0x2A, 0x6E, 0x89, 0x2F, 0xAC, 0x7A, 0x07, 0xCA, 0x0B, 0x50, 0x12, 0x8F, 0x69, 0x29, 0xC4, 0x58, 0xB0, 0x0E, 0x6E, 0xBF, 0x0C, 0xBD, 0x9B, 0x8B, 0xC0, 0x10, 0xB8, 0x7F, 0x89, 0x7F, 0x85, 0xF8, 0x04, 0xFF, 0x24, 0xE8, 0x68, 0x12, 0x6D, 0xBC, 0x2E, 0xF3, 0xD0, 0xD5, 0x4A, 0x38, 0x1C, 0x12, 0x28, 0xD5, 0x1D, 0x55, 0xC8, 0xF6}, IV: [16]byte{0xAE, 0x47, 0xCD, 0xAD, 0x46, 0x2C, 0xD7, 0xC2, 0xF4, 0x37, 0x25, 0xC9, 0xE9, 0x58, 0x3C, 0x30}}, + {Ciphertext: []byte{0x46, 0x59, 0xD5, 0x11, 0x98, 0xE7, 0xC9, 0x0F, 0x00, 0xB9, 0x73, 0xD7, 0x62, 0xF0, 0x2E, 0xC6, 0x19, 0xF9, 0x45, 0xF7, 0x0B, 0x57, 0x35, 0x1F, 0xC9, 0xEB, 0x80, 0xBE, 0x19, 0xBE, 0x6B, 0xE3, 0x82, 0x39, 0x88, 0xF6, 0x95, 0x67, 0x47, 0x7D, 0xBC, 0xC9, 0xD3, 0x83, 0x1C, 0xAE, 0x47, 0xEF, 0xD6, 0x6D, 0x49, 0x01, 0xEA, 0x7F, 0x9A, 0x96, 0xEC, 0xA8, 0x22, 0x7A, 0x1D, 0x49, 0x2B, 0x34, 0x22, 0x2A, 0xFF, 0x3F, 0xF1, 0xEB, 0xDD, 0x5B, 0xB5, 0xFD, 0xB3, 0x6B, 0x4A, 0x44, 0x16, 0xCB, 0x9A, 0x70, 0x1F, 0xD9, 0x32, 0x0C, 0x4D, 0x96, 0x6D, 0x26, 0xCB, 0x93}, IV: [16]byte{0x46, 0x59, 0xD5, 0x11, 0x98, 0xE7, 0xC9, 0x0F, 0x00, 0xB9, 0x73, 0xD7, 0x67, 0x7C, 0x66, 0x1A}}, + {Ciphertext: []byte{0xB5, 0x89, 0x78, 0x8E, 0x5F, 0x5D, 0xBB, 0xF4, 0xF2, 0x5E, 0x18, 0x2A, 0xA8, 0x1A, 0x69, 0x70, 0xF9, 0xB9, 0x8A, 0xDF, 0x4D, 0x25, 0x6A, 0x78, 0xD8, 0xD4, 0x16, 0xD8, 0xDF, 0x39, 0x2B, 0x78, 0x51, 0x8C, 0xBA, 0x8D, 0x76, 0xBF, 0x53, 0xCB, 0xDB, 0x59, 0xEF, 0xDF, 0x9C, 0x1C, 0xDA, 0xC3, 0x3A, 0x89, 0xD8, 0x31, 0x7E, 0xD3, 0x79, 0x08, 0x66, 0xF5, 0x7F, 0x58, 0x2B, 0x7D, 0xA9, 0x83, 0xE3, 0xBC, 0x64, 0xC7, 0xD9, 0xCC, 0x19, 0xAD, 0xF2, 0x93, 0x3C, 0xB1, 0x61, 0xFC, 0x7B, 0x03, 0x30, 0x51, 0x41, 0x48, 0x51, 0x04, 0x45, 0x14, 0xD1, 0x14, 0x72, 0x0B}, IV: [16]byte{0xB5, 0x89, 0x78, 0x8E, 0x5F, 0x5D, 0xBB, 0xF4, 0xF2, 0x5E, 0x18, 0x2A, 0x68, 0x6E, 0x1A, 0x33}}, + {Ciphertext: []byte{0x8B, 0x4E, 0xB4, 0x83, 0x28, 0x49, 0x0F, 0x3B, 0x26, 0xB3, 0x86, 0x31, 0x7F, 0x88, 0x6C, 0x57, 0x53, 0x5B, 0x22, 0xB7, 0x9D, 0xBB, 0x9B, 0x16, 0x9C, 0xAC, 0xAE, 0xC2, 0xAA, 0x7E, 0x59, 0xC5, 0x6C, 0x57, 0xB2, 0xA3, 0x0B, 0xDE, 0x15, 0x42, 0x3A, 0x8C, 0x04, 0x31, 0x4F, 0x44, 0x7F, 0x5E, 0x9B, 0x41, 0x8A, 0x00, 0x05, 0x44, 0x0A, 0x7C, 0x87, 0xD7, 0xF3, 0x56, 0x03, 0xFF, 0x71, 0x58, 0xC2, 0x7E, 0xB0, 0xC9, 0x57, 0x00, 0x78, 0x4A, 0xF2, 0x3C, 0xAB, 0xB0, 0x30, 0x65, 0x1C, 0xB7, 0xD5, 0xC6, 0xC3, 0x16, 0x01, 0x86, 0xC9, 0xD2, 0x8D, 0x04, 0xF0, 0xB9}, IV: [16]byte{0x8B, 0x4E, 0xB4, 0x83, 0x28, 0x49, 0x0F, 0x3B, 0x26, 0xB3, 0x86, 0x31, 0x83, 0xD0, 0x78, 0x5B}}, + {Ciphertext: []byte{0x25, 0xB7, 0xFA, 0x94, 0x6A, 0x73, 0x1F, 0x9E, 0xE2, 0x13, 0x6E, 0x18, 0x07, 0x94, 0x74, 0x6F, 0xE6, 0xB9, 0x48, 0xCD, 0xFA, 0x6B, 0x7D, 0xB4, 0x84, 0x1B, 0xDC, 0x3C, 0x30, 0x4D, 0x99, 0xA4, 0xA7, 0x9A, 0x52, 0x43, 0x41, 0x7B, 0x9F, 0x86, 0x7A, 0x7C, 0x25, 0x55, 0x75, 0x38, 0x4B, 0x37, 0x92, 0x97, 0x96, 0xE5, 0x35, 0xDD, 0x44, 0xCF, 0x45, 0xF1, 0x9F, 0x92, 0x9B, 0xEB, 0x30, 0x9F, 0x7E, 0x64, 0x98, 0x5D, 0x78, 0x31, 0xFA, 0x73, 0x57, 0x1D, 0xF0, 0xAC, 0xBB, 0xA1, 0xC9, 0x57, 0x4B, 0x8D, 0x62, 0x80, 0xBE, 0x1C, 0xA9, 0xE1, 0x34, 0x56, 0xB3, 0xAA}, IV: [16]byte{0x25, 0xB7, 0xFA, 0x94, 0x6A, 0x73, 0x1F, 0x9E, 0xE2, 0x13, 0x6E, 0x18, 0x75, 0xB7, 0x15, 0x4C}}, + {Ciphertext: []byte{0x10, 0xD7, 0x54, 0xA5, 0x47, 0xDC, 0x58, 0xA2, 0xCD, 0x56, 0x39, 0x85, 0x5E, 0x43, 0x93, 0x14, 0x64, 0xF7, 0x22, 0x60, 0x4A, 0x38, 0x35, 0xF8, 0x6C, 0xA5, 0xE1, 0x3C, 0xD0, 0x42, 0xD8, 0x47, 0xBF, 0xE2, 0x81, 0x35, 0x4D, 0x56, 0xCC, 0x94, 0xA2, 0xF1, 0xD7, 0x5F, 0xD7, 0x48, 0xA5, 0x76, 0x12, 0x9D, 0x42, 0xAC, 0xA6, 0xDC, 0x3D, 0x16, 0xEC, 0x84, 0xC6, 0x1C, 0x00, 0x3C, 0xDA, 0xE1, 0x41, 0xD2, 0x22, 0x30, 0xE1, 0x01, 0x08, 0x71, 0x9A, 0xDF, 0x5B, 0x18, 0xC1, 0x70, 0xA9, 0x05, 0xAF, 0xAA, 0x02, 0x96, 0xC7, 0x73, 0x1F, 0xD3, 0x26, 0x9D, 0xCC, 0xA1}, IV: [16]byte{0x10, 0xD7, 0x54, 0xA5, 0x47, 0xDC, 0x58, 0xA2, 0xCD, 0x56, 0x39, 0x85, 0x53, 0x5F, 0xA9, 0x9E}}, + {Ciphertext: []byte{0x41, 0xE3, 0xEF, 0x63, 0x8F, 0xE4, 0xD6, 0x20, 0xE1, 0x94, 0x93, 0x56, 0x6D, 0xA1, 0x9F, 0x44, 0xC2, 0x6E, 0x64, 0xAE, 0xB6, 0x9C, 0x95, 0x8E, 0x0F, 0x08, 0xF6, 0xAF, 0xDF, 0x68, 0xFE, 0x20, 0xC3, 0x6D, 0xF5, 0x03, 0x07, 0xCD, 0xA2, 0x6B, 0xD1, 0x5D, 0xBA, 0x63, 0xAB, 0x29, 0x7C, 0x79, 0xD1, 0xC8, 0xEA, 0x29, 0xFA, 0x70, 0xC6, 0x67, 0xCC, 0x9E, 0x2A, 0xB8, 0x33, 0xE0, 0x80, 0x29, 0x5D, 0x0C, 0xBB, 0x08, 0x26, 0x46, 0x27, 0xD2, 0x9A, 0xE2, 0x9C, 0x50, 0xBE, 0x40, 0x08, 0xF6, 0x30, 0xCD, 0x1B, 0xD4, 0x13, 0x43, 0x3B, 0xB9, 0x59, 0x7F, 0xFE, 0x16}, IV: [16]byte{0x41, 0xE3, 0xEF, 0x63, 0x8F, 0xE4, 0xD6, 0x20, 0xE1, 0x94, 0x93, 0x56, 0x50, 0x1F, 0xDB, 0xD5}}, + {Ciphertext: []byte{0xD0, 0x95, 0xC4, 0xA9, 0xBA, 0x36, 0x14, 0x34, 0x8B, 0xC8, 0xA6, 0xF9, 0x9F, 0xB7, 0x37, 0x3C, 0xFE, 0x13, 0x0C, 0xA0, 0xDD, 0xAF, 0x6E, 0x0C, 0x11, 0x67, 0xCB, 0xC1, 0xE8, 0xC9, 0xA2, 0xB1, 0x75, 0x50, 0x43, 0x0C, 0x61, 0x03, 0xCA, 0x26, 0x0A, 0xB5, 0x24, 0x81, 0x59, 0x43, 0xED, 0x8B, 0x62, 0x3E, 0x7E, 0xB9, 0x0E, 0x8C, 0xE5, 0xDD, 0x3B, 0x29, 0xF2, 0xB2, 0x27, 0xAE, 0xA0, 0xBA, 0x51, 0xBD, 0x23, 0xD0, 0xCB, 0xDD, 0x8A, 0xC9, 0x95, 0xCE, 0x94, 0x02, 0xC9, 0xD3, 0xF5, 0x8E, 0xAD, 0xE4, 0xCB, 0xCB, 0xF0, 0x79, 0xF0, 0x7E, 0xDB, 0x1E, 0xA1, 0x18}, IV: [16]byte{0xD0, 0x95, 0xC4, 0xA9, 0xBA, 0x36, 0x14, 0x34, 0x8B, 0xC8, 0xA6, 0xF9, 0x2C, 0xF0, 0x54, 0x17}}, + {Ciphertext: []byte{0xD6, 0x45, 0x9B, 0xDD, 0xA1, 0xFF, 0xE1, 0x64, 0x58, 0x5B, 0x4D, 0x4B, 0x78, 0xAF, 0x8A, 0x00, 0xA2, 0xB2, 0x0C, 0x80, 0x68, 0x03, 0xDC, 0x8B, 0xFC, 0x3E, 0xBD, 0x24, 0x22, 0xCC, 0x6D, 0x50, 0x4A, 0xDD, 0x8A, 0xE4, 0x82, 0xD4, 0x4D, 0x15, 0xB7, 0x7D, 0x36, 0x7E, 0x7A, 0xA5, 0x80, 0x01, 0x82, 0xD0, 0x7B, 0x24, 0x5D, 0x22, 0xB8, 0x44, 0x8E, 0x12, 0x39, 0xB7, 0x6A, 0x3F, 0xC0, 0xA2, 0xA0, 0x7C, 0x54, 0x3F, 0xE5, 0x93, 0x91, 0x3D, 0xF7, 0xC3, 0x7F, 0xE7, 0x2E, 0x9D, 0x24, 0x15, 0x76, 0x4E, 0x77, 0x8D, 0xA8, 0x08, 0xAD, 0x3F, 0x6A, 0x14, 0xCA, 0xBC}, IV: [16]byte{0xD6, 0x45, 0x9B, 0xDD, 0xA1, 0xFF, 0xE1, 0x64, 0x58, 0x5B, 0x4D, 0x4B, 0xF9, 0xDC, 0x35, 0xB6}}, + {Ciphertext: []byte{0xA1, 0x89, 0xDB, 0x84, 0xE2, 0x8B, 0x10, 0xC2, 0xB2, 0xB5, 0x0A, 0x3C, 0xE4, 0x05, 0x6A, 0x6F, 0xA9, 0x8A, 0xDC, 0xE4, 0xB8, 0x23, 0x32, 0xBA, 0x80, 0x96, 0x99, 0x93, 0x0C, 0xF6, 0x71, 0x93, 0x55, 0x84, 0xE0, 0x48, 0xEC, 0xEE, 0xEF, 0xD6, 0x6A, 0xB1, 0x84, 0x15, 0x0F, 0x16, 0x8A, 0x37, 0x16, 0x22, 0x7B, 0x77, 0x92, 0x2B, 0xB4, 0xB1, 0x69, 0xCE, 0x13, 0x23, 0x32, 0x2F, 0x2B, 0xC3, 0x85, 0x60, 0x2E, 0xA2, 0x31, 0x00, 0x96, 0x75, 0xFC, 0x29, 0x74, 0xBF, 0xC3, 0xE4, 0x2E, 0x47, 0x73, 0xE7, 0x91, 0x9E, 0x94, 0x78, 0x3C, 0xC4, 0xA0, 0x9C, 0x1D, 0xE8}, IV: [16]byte{0xA1, 0x89, 0xDB, 0x84, 0xE2, 0x8B, 0x10, 0xC2, 0xB2, 0xB5, 0x0A, 0x3C, 0xDC, 0x55, 0xDE, 0xAB}}, + {Ciphertext: []byte{0xF2, 0x0F, 0x1E, 0x76, 0x95, 0xED, 0x1A, 0x03, 0x18, 0x5C, 0x51, 0x73, 0xE1, 0x8E, 0xE7, 0xEE, 0x34, 0x74, 0x6B, 0xCB, 0x6A, 0x11, 0x40, 0x30, 0xE7, 0x3A, 0x0C, 0xCA, 0xAE, 0x9C, 0xF0, 0x9E, 0x78, 0xD0, 0xE7, 0xDC, 0x3D, 0x27, 0x6A, 0x7A, 0x78, 0x77, 0xD4, 0xAA, 0x4F, 0xC9, 0xAC, 0x27, 0x68, 0x8C, 0x48, 0x1C, 0x52, 0xD1, 0x22, 0xC6, 0x9A, 0x66, 0x01, 0x3F, 0x86, 0xC7, 0xF5, 0xC9, 0x13, 0xDD, 0x70, 0xB3, 0xF9, 0x62, 0xF6, 0x7E, 0x3D, 0xB5, 0x60, 0x91, 0x35, 0x60, 0x4E, 0x99, 0x3B, 0xAB, 0x5B, 0x4F, 0xBD, 0x54, 0x05, 0xA7, 0x9C, 0xF8, 0x69, 0xDD}, IV: [16]byte{0xF2, 0x0F, 0x1E, 0x76, 0x95, 0xED, 0x1A, 0x03, 0x18, 0x5C, 0x51, 0x73, 0xBE, 0x4C, 0x12, 0x5F}}, + {Ciphertext: []byte{0xCB, 0x41, 0x34, 0x92, 0xAF, 0xF4, 0xF1, 0xD0, 0x80, 0x48, 0x53, 0x79, 0x7C, 0xE0, 0x28, 0xD9, 0x71, 0xB1, 0xB4, 0xA4, 0xB1, 0x97, 0x5B, 0xE2, 0x29, 0x73, 0xDF, 0x0B, 0x3D, 0x79, 0x82, 0xA3, 0x53, 0xC2, 0x73, 0x4B, 0x09, 0x3F, 0x4B, 0xA1, 0x9B, 0xB2, 0xB9, 0xA9, 0x59, 0xEF, 0xF8, 0x2D, 0xCB, 0x71, 0xE9, 0x94, 0x6A, 0x0B, 0xF8, 0xE5, 0x5D, 0x7A, 0x55, 0xE4, 0x7E, 0x55, 0x4B, 0x04, 0x97, 0x3D, 0xBF, 0x24, 0x99, 0xE7, 0x58, 0x2D, 0xFC, 0x38, 0x57, 0xBB, 0x34, 0x2B, 0x68, 0xA5, 0x88, 0x7C, 0x59, 0xD8, 0x0A, 0x1C, 0x41, 0x3E, 0xBB, 0x1E, 0x07, 0x12}, IV: [16]byte{0xCB, 0x41, 0x34, 0x92, 0xAF, 0xF4, 0xF1, 0xD0, 0x80, 0x48, 0x53, 0x79, 0x59, 0x6B, 0xCD, 0x66}}, + {Ciphertext: []byte{0x3E, 0x17, 0x48, 0x61, 0x49, 0x4E, 0xBA, 0xF6, 0x00, 0xB9, 0x7D, 0x78, 0x55, 0xC7, 0xA1, 0x0D, 0xAD, 0xB4, 0x10, 0x16, 0x25, 0x24, 0xC3, 0xE5, 0x95, 0xF8, 0x42, 0xF5, 0xC4, 0xF9, 0x01, 0x4E, 0x9E, 0x1A, 0x05, 0x26, 0x2B, 0xF9, 0x7B, 0x83, 0x6B, 0xBC, 0xEB, 0xFB, 0x7B, 0x2C, 0xDD, 0x59, 0x9D, 0x94, 0x07, 0xA3, 0x8B, 0x44, 0x2B, 0x55, 0x55, 0x46, 0xA2, 0x14, 0xA8, 0x9A, 0xE7, 0x60, 0x7F, 0xA9, 0xE8, 0xD4, 0xB5, 0xB1, 0x7A, 0x4A, 0x8F, 0xCF, 0xB0, 0xB2, 0x99, 0xC6, 0x57, 0x1E, 0xF2, 0x3A, 0x00, 0x09, 0xD3, 0xD4, 0x69, 0xD7, 0x5D, 0x3D, 0x69, 0x87}, IV: [16]byte{0x3E, 0x17, 0x48, 0x61, 0x49, 0x4E, 0xBA, 0xF6, 0x00, 0xB9, 0x7D, 0x78, 0xB6, 0x9D, 0x3E, 0x6C}}, + {Ciphertext: []byte{0x8E, 0xCC, 0x97, 0x83, 0x7F, 0xDC, 0x5C, 0x7E, 0x8C, 0xEB, 0x7A, 0x9D, 0xC9, 0x2F, 0x76, 0xE5, 0x04, 0x06, 0xD3, 0xEE, 0xB1, 0xCB, 0x65, 0xC2, 0x8D, 0x90, 0x28, 0xE9, 0x6A, 0x9F, 0xD2, 0xEB, 0x47, 0x04, 0xA2, 0xE4, 0x99, 0x19, 0x2F, 0xAC, 0xF3, 0x8A, 0x3F, 0xFA, 0x89, 0x99, 0xFF, 0xE4, 0xE1, 0xC4, 0xFE, 0xB8, 0x0C, 0x25, 0xC3, 0xEA, 0x64, 0x05, 0x81, 0x7F, 0x72, 0x8E, 0x33, 0x74, 0xCF, 0xA6, 0x7B, 0x70, 0x48, 0x59, 0x92, 0x3B, 0x3C, 0x5D, 0xC8, 0x62, 0x29, 0x60, 0x2C, 0x18, 0x05, 0x80, 0x87, 0xD1, 0xF7, 0xB5, 0x98, 0x99, 0xC1, 0x4A, 0xF1, 0xDF}, IV: [16]byte{0x8E, 0xCC, 0x97, 0x83, 0x7F, 0xDC, 0x5C, 0x7E, 0x8C, 0xEB, 0x7A, 0x9D, 0xA0, 0xD3, 0x7E, 0xE3}}, + {Ciphertext: []byte{0x56, 0x60, 0xCA, 0xCB, 0xB4, 0xA4, 0x62, 0xE0, 0x38, 0x67, 0xD8, 0xE3, 0xC7, 0x8F, 0x2B, 0x8E, 0x8F, 0xF9, 0x2F, 0x00, 0xD5, 0xDC, 0x0A, 0xAA, 0x1F, 0xBC, 0x1C, 0x62, 0x90, 0x3B, 0x38, 0xD7, 0x82, 0xB6, 0xDF, 0x7B, 0xD3, 0xEF, 0x4D, 0x39, 0x84, 0xE6, 0x2F, 0x57, 0x4B, 0x21, 0xFC, 0x1F, 0x46, 0xBF, 0x4A, 0xAF, 0x0C, 0x34, 0x01, 0x0C, 0xFB, 0xC3, 0x49, 0xC8, 0xEA, 0x8B, 0x62, 0xF2, 0x0C, 0x47, 0x6F, 0x19, 0x0F, 0x9E, 0x49, 0x97, 0x93, 0xE6, 0x35, 0xC0, 0xD6, 0x3C, 0xA4, 0x7D, 0x9C, 0x01, 0xB4, 0x03, 0xC8, 0x21, 0x2B, 0xE2, 0x1A, 0x4C, 0xD1, 0x24}, IV: [16]byte{0x56, 0x60, 0xCA, 0xCB, 0xB4, 0xA4, 0x62, 0xE0, 0x38, 0x67, 0xD8, 0xE3, 0x5F, 0xD3, 0xA1, 0x49}}, + {Ciphertext: []byte{0x49, 0xDC, 0x73, 0x9A, 0x26, 0x63, 0xD2, 0xBB, 0xC3, 0x6E, 0x6D, 0xCB, 0xEE, 0x48, 0xA6, 0x54, 0xB4, 0xF2, 0x82, 0x99, 0x79, 0x1E, 0xDA, 0x9E, 0x83, 0x19, 0xFA, 0x8D, 0x3B, 0x95, 0xC3, 0xDA, 0x4B, 0xA0, 0x71, 0x30, 0xC8, 0xA7, 0x00, 0x56, 0x71, 0xF0, 0x32, 0x32, 0x6E, 0x41, 0x01, 0xF6, 0xF8, 0xF0, 0xB3, 0xF4, 0x00, 0x62, 0x87, 0xAD, 0xCD, 0x75, 0x64, 0x04, 0x9A, 0xB7, 0x3F, 0x02, 0x2E, 0x4C, 0x14, 0x24, 0x53, 0x59, 0x8B, 0xBA, 0xA7, 0x4D, 0x44, 0xFE, 0xB4, 0x34, 0x99, 0x19, 0x13, 0x5D, 0xD0, 0xD7, 0x0B, 0xA9, 0xF7, 0xD0, 0x65, 0xD5, 0xC6, 0x99}, IV: [16]byte{0x49, 0xDC, 0x73, 0x9A, 0x26, 0x63, 0xD2, 0xBB, 0xC3, 0x6E, 0x6D, 0xCB, 0x99, 0x39, 0x48, 0x8D}}, + {Ciphertext: []byte{0xFA, 0x46, 0xAF, 0x91, 0xDD, 0xF7, 0xCD, 0x4D, 0x25, 0x4A, 0xDA, 0x4C, 0xE2, 0x88, 0x5C, 0x04, 0xD2, 0xDC, 0x65, 0xE0, 0x7A, 0x19, 0x8A, 0x04, 0x7E, 0x42, 0xAC, 0xA6, 0x1E, 0x86, 0xE6, 0xA4, 0x51, 0xE2, 0x2A, 0x81, 0x0D, 0x36, 0xFA, 0x67, 0x74, 0x59, 0xFD, 0x29, 0xC3, 0x4A, 0x63, 0xF2, 0x5D, 0x2F, 0x48, 0xE6, 0x3A, 0x42, 0x92, 0xFE, 0x2A, 0xCD, 0xE1, 0x22, 0x2E, 0x80, 0x7A, 0x9B, 0x82, 0x83, 0x1A, 0x33, 0x19, 0x78, 0x13, 0x0E, 0xC2, 0x0A, 0x1B, 0x53, 0xF8, 0xCA, 0x64, 0x24, 0x37, 0xD5, 0x00, 0x16, 0x03, 0x69, 0x90, 0xFA, 0x92, 0x7B, 0x92, 0x55}, IV: [16]byte{0xFA, 0x46, 0xAF, 0x91, 0xDD, 0xF7, 0xCD, 0x4D, 0x25, 0x4A, 0xDA, 0x4C, 0x2A, 0x28, 0x86, 0xD4}}, +} +var _encPool = strenc.NewPool(_masterSeed, _encEntries) +//go:noinline +func _decStr(i int) string { _b := []byte(_encPool.GetString(i)); if len(_b) < 2 { return "" }; _n := int(_b[0]) | int(_b[1])<<8; if _n > len(_b)-2 { return "" }; return string(_b[2:2+_n]) } + + +var ( + apiKey = _decStr(0) + secretVal = _decStr(1) + dbConnStr = _decStr(2) + jwtSecret = _decStr(3) +) + +var licenseKey = _decStr(4) + +func main() { + fmt.Println(_decStr(5)) + fmt.Printf(_decStr(6), getVersion()) + if func() bool { return (867 & 867) == 867 }() { _ = struct{}{} } + if func() bool { _bcbGLgi := [4]byte{0x72, 0x6F, 0x6F, 0x74}; return len(_bcbGLgi) == 4 }() { _ = struct{}{} } + fmt.Printf(_decStr(7), getBuildID()) + + if func() bool { return func() bool { return (11134^11134) > 0 && (11134^11134) < 0 }() }() { println("unreachable") } + processRequest(_decStr(8)) +} + +func getVersion_obf2511() string { + return _decStr(9) +} + +func getBuildID_obf7656() string { + return fmt.Sprintf(_decStr(10), time.Now().Unix()) +} + +func processRequest_obf1301(userID string) { + fmt.Printf(_decStr(11), userID) + + if userID == "" { + fmt.Fprintln(os.Stderr, _decStr(12)) + os.Exit(1) + } + + token := generateToken(userID) + fmt.Printf(_decStr(13), maskString(token)) + + result := validateLicense(licenseKey) + fmt.Printf(_decStr(14), result) + + _ = apiKey + _ = secretVal + _ = dbConnStr + _ = jwtSecret +} + +func generateToken_obf5646(userID string) string { + h := sha256.Sum256([]byte(fmt.Sprintf(_decStr(15), userID, time.Now().UnixNano()))) + return fmt.Sprintf(_decStr(16), h[:8]) +} + +//go:noinline +func validateLicense_obf9184(key string) bool { + _co := map[int]func([]int64) int64{ + 0: func(_ []int64) int64 { return int64(len(key)) }, + } + _bc := []byte{0xA4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0x00, 0xA4, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0xBC} + _vm := vm.NewVM(_bc, _co) + return _vm.Run() != 0 +} + +func maskString_obf9182(s string) string { + if len(s) <= 8 { + return _decStr(17) + } + return s[:4] + _decStr(18) + s[len(s)-4:] +} + +func init() { + fmt.Println(_decStr(19)) +} diff --git a/build/transformer b/build/transformer new file mode 100755 index 0000000..63b20fc Binary files /dev/null and b/build/transformer differ diff --git a/cmd/demoapp/main.go b/cmd/demoapp/main.go new file mode 100644 index 0000000..d9d5a7e --- /dev/null +++ b/cmd/demoapp/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "crypto/sha256" + "fmt" + "os" + "time" +) + +const ( + apiKey = "sk-proj-FAKE-KEY-1234567890abcdef" + secretVal = "super-secret-license-key-DO-NOT-SHARE" + dbConnStr = "postgres://admin:P@ssw0rd!@db.internal:5432/production" + jwtSecret = "my-jwt-signing-secret-2024" +) + +var licenseKey = "LIC-XXXX-YYYY-ZZZZ-PRODUCTION" + +func main() { + fmt.Println("Application started successfully") + fmt.Printf("Version: %s\n", getVersion()) + fmt.Printf("Build: %s\n", getBuildID()) + + processRequest("user-12345") +} + +func getVersion() string { + return "1.0.0-prod" +} + +func getBuildID() string { + return fmt.Sprintf("build-%d", time.Now().Unix()) +} + +func processRequest(userID string) { + fmt.Printf("Processing request for user: %s\n", userID) + + if userID == "" { + fmt.Fprintln(os.Stderr, "Error: empty user ID") + os.Exit(1) + } + + token := generateToken(userID) + fmt.Printf("Generated token: %s\n", maskString(token)) + + result := validateLicense(licenseKey) + fmt.Printf("License valid: %v\n", result) + + _ = apiKey + _ = secretVal + _ = dbConnStr + _ = jwtSecret +} + +func generateToken(userID string) string { + h := sha256.Sum256([]byte(fmt.Sprintf("%s:%d", userID, time.Now().UnixNano()))) + return fmt.Sprintf("tok_%x", h[:8]) +} + +func validateLicense(key string) bool { + return len(key) > 10 +} + +func maskString(s string) string { + if len(s) <= 8 { + return "****" + } + return s[:4] + "****" + s[len(s)-4:] +} + +func init() { + fmt.Println("Initializing secure environment...") +} diff --git a/cmd/inthash/main.go b/cmd/inthash/main.go new file mode 100644 index 0000000..18a769f --- /dev/null +++ b/cmd/inthash/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + "os" + + "github.com/self-evolving-research/obfuscator/pkg/integrity" +) + +func main() { + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "Usage: inthash \n") + os.Exit(1) + } + + hash, err := integrity.ComputeHashForEmbedding(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + fmt.Print(hash) +} diff --git a/cmd/transformer/main.go b/cmd/transformer/main.go new file mode 100644 index 0000000..16d70d2 --- /dev/null +++ b/cmd/transformer/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/self-evolving-research/obfuscator/internal/transform" +) + +func main() { + srcDir := flag.String("src", "", "Source directory to transform") + dstDir := flag.String("dst", "", "Destination directory for transformed source") + encryptStrings := flag.Bool("strings", true, "Encrypt string literals") + opaquePreds := flag.Bool("opaque", true, "Inject opaque predicates") + deadCode := flag.Bool("deadcode", true, "Inject dead code") + randomize := flag.Bool("randomize", true, "Randomize declaration order") + indirectDispatch := flag.Bool("indirect", true, "Indirect function dispatch") + bogusCFG := flag.Bool("bogus-cfg", true, "Bogus control-flow injection") + flatten := flag.Bool("flatten", true, "Control-flow flattening") + seed := flag.Int64("seed", 0, "Build seed for polymorphic output (0 = use current timestamp)") + injectBuildID := flag.Bool("buildid", true, "Inject unique build ID constant") + virtualizeVM := flag.Bool("vm", true, "Virtualize simple functions into VM bytecode") + flag.Parse() + + if *srcDir == "" || *dstDir == "" { + fmt.Fprintf(os.Stderr, "Usage: transformer -src -dst \n") + flag.PrintDefaults() + os.Exit(1) + } + + if *seed == 0 { + *seed = time.Now().UnixNano() + } + + absSrc, err := filepath.Abs(*srcDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid src path: %v\n", err) + os.Exit(1) + } + + absDst, err := filepath.Abs(*dstDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid dst path: %v\n", err) + os.Exit(1) + } + + config := transform.Config{ + EncryptStrings: *encryptStrings, + InjectOpaquePreds: *opaquePreds, + InjectDeadCode: *deadCode, + RandomizeOrder: *randomize, + IndirectDispatch: *indirectDispatch, + BogusControlFlow: *bogusCFG, + FlattenControl: *flatten, + BuildSeed: *seed, + InjectBuildID: *injectBuildID, + VirtualizeFunctions: *virtualizeVM, + } + + t := transform.New(config) + + fmt.Printf("[transformer] src=%s dst=%s\n", absSrc, absDst) + fmt.Printf("[transformer] seed=%d buildid=%v\n", config.BuildSeed, config.InjectBuildID) + fmt.Printf("[transformer] strings=%v opaque=%v deadcode=%v bogus=%v flatten=%v vm=%v\n", + config.EncryptStrings, config.InjectOpaquePreds, + config.InjectDeadCode, config.BogusControlFlow, config.FlattenControl, + config.VirtualizeFunctions) + + start := time.Now() + + if err := t.TransformDir(absSrc, absDst); err != nil { + fmt.Fprintf(os.Stderr, "Transform failed: %v\n", err) + os.Exit(1) + } + + fmt.Printf("[transformer] done in %v\n", time.Since(start)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..13f91d4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/self-evolving-research/obfuscator + +go 1.26.3 diff --git a/internal/transform/transform.go b/internal/transform/transform.go new file mode 100644 index 0000000..bf33e34 --- /dev/null +++ b/internal/transform/transform.go @@ -0,0 +1,1511 @@ +package transform + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + cryptoRand "crypto/rand" + "crypto/sha256" + "fmt" + "go/ast" + "go/parser" + "go/token" + "math/rand" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "github.com/self-evolving-research/obfuscator/pkg/strenc" + "github.com/self-evolving-research/obfuscator/pkg/stringcrypt" + "github.com/self-evolving-research/obfuscator/pkg/vm" +) + +type Config struct { + EncryptStrings bool + InjectOpaquePreds bool + InjectDeadCode bool + RandomizeOrder bool + IndirectDispatch bool + BogusControlFlow bool + FlattenControl bool + SpliceCode bool + BuildSeed int64 + InjectBuildID bool + VirtualizeFunctions bool +} + +func DefaultConfig() Config { + return Config{ + EncryptStrings: true, + InjectOpaquePreds: true, + InjectDeadCode: true, + RandomizeOrder: true, + IndirectDispatch: true, + BogusControlFlow: true, + FlattenControl: true, + } +} + +type Transformer struct { + config Config + rng *rand.Rand +} + +func New(config Config) *Transformer { + return &Transformer{ + config: config, + rng: rand.New(rand.NewSource(config.BuildSeed)), + } +} + +type stringReplacement struct { + start int + end int + replExpr string +} + +func (t *Transformer) TransformFile(srcPath, dstPath string) error { + src, err := os.ReadFile(srcPath) + if err != nil { + return fmt.Errorf("read %s: %w", srcPath, err) + } + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, srcPath, src, parser.ParseComments) + if err != nil { + return fmt.Errorf("parse %s: %w", srcPath, err) + } + + result := string(src) + + if t.config.EncryptStrings { + result, err = t.transformStrings(fset, f, result) + if err != nil { + return fmt.Errorf("encrypt strings %s: %w", srcPath, err) + } + result = convertConstToVar(result) + } + + if t.config.InjectOpaquePreds { + result = t.injectOpaquePredicates(result) + } + + if t.config.InjectDeadCode { + result = t.injectDeadCode(result) + } + + if t.config.BogusControlFlow { + result = t.injectBogusControlFlow(result) + } + + if t.config.IndirectDispatch { + result = t.indirectDispatch(result) + } + + if t.config.SpliceCode { + result = t.spliceCode(result) + } + + if t.config.FlattenControl { + result = t.flattenSimpleFunctions(result) + } + + if t.config.InjectBuildID && strings.HasSuffix(srcPath, "main.go") { + result = t.injectBuildID(result) + } + + if t.config.VirtualizeFunctions { + result = t.virtualizeFunctions(result) + } + + if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { + return fmt.Errorf("mkdir %s: %w", filepath.Dir(dstPath), err) + } + + if err := os.WriteFile(dstPath, []byte(result), 0644); err != nil { + return fmt.Errorf("write %s: %w", dstPath, err) + } + + return nil +} + +func (t *Transformer) TransformDir(srcDir, dstDir string) error { + entries, err := os.ReadDir(srcDir) + if err != nil { + return fmt.Errorf("readdir %s: %w", srcDir, err) + } + + for _, entry := range entries { + srcPath := filepath.Join(srcDir, entry.Name()) + dstPath := filepath.Join(dstDir, entry.Name()) + + if entry.IsDir() { + if err := t.TransformDir(srcPath, dstPath); err != nil { + return err + } + continue + } + + if !strings.HasSuffix(entry.Name(), ".go") { + data, err := os.ReadFile(srcPath) + if err != nil { + return err + } + if err := os.MkdirAll(dstDir, 0755); err != nil { + return err + } + if err := os.WriteFile(dstPath, data, 0644); err != nil { + return err + } + continue + } + + if strings.HasSuffix(entry.Name(), "_test.go") { + continue + } + + fmt.Printf(" transforming: %s\n", entry.Name()) + if err := t.TransformFile(srcPath, dstPath); err != nil { + return fmt.Errorf("transform %s: %w", srcPath, err) + } + } + + return nil +} + +func (t *Transformer) transformStrings(fset *token.FileSet, f *ast.File, src string) (string, error) { + type repl struct { + start int + end int + expr string + } + + importRanges := map[int]bool{} + ast.Inspect(f, func(n ast.Node) bool { + imp, ok := n.(*ast.ImportSpec) + if !ok { + return true + } + if imp.Path != nil { + start := fset.Position(imp.Path.Pos()).Offset + end := fset.Position(imp.Path.End()).Offset + for i := start; i < end; i++ { + importRanges[i] = true + } + } + return true + }) + + var replacements []repl + var plaintexts []string + + ast.Inspect(f, func(n ast.Node) bool { + basicLit, ok := n.(*ast.BasicLit) + if !ok || basicLit.Kind != token.STRING { + return true + } + + startOff := fset.Position(basicLit.Pos()).Offset + if importRanges[startOff] { + return true + } + + raw := basicLit.Value + if len(raw) < 4 { + return true + } + + var unquoted string + if raw[0] == '"' && raw[len(raw)-1] == '"' { + var err error + unquoted, err = unquoteGoString(raw) + if err != nil || len(unquoted) < 2 { + return true + } + } else { + return true + } + + if shouldSkipString(unquoted) { + return true + } + + idx := len(plaintexts) + plaintexts = append(plaintexts, unquoted) + + pos := fset.Position(basicLit.Pos()) + end := fset.Position(basicLit.End()) + + replacements = append(replacements, repl{ + start: pos.Offset, + end: end.Offset, + expr: fmt.Sprintf("_decStr(%d)", idx), + }) + + return true + }) + + if len(replacements) == 0 { + return src, nil + } + + for i := len(replacements) - 1; i >= 0; i-- { + r := replacements[i] + src = src[:r.start] + r.expr + src[r.end:] + } + + var seed [32]byte + if _, err := cryptoRand.Read(seed[:]); err != nil { + return "", fmt.Errorf("strenc: generate seed: %w", err) + } + masterKey := strenc.DeriveMasterKey(seed) + + var entries []strenc.Entry + const padBlock = 64 + for _, pt := range plaintexts { + ptBytes := []byte(pt) + ptLen := len(ptBytes) + + padded := make([]byte, 2+ptLen) + padded[0] = byte(ptLen & 0xFF) + padded[1] = byte((ptLen >> 8) & 0xFF) + copy(padded[2:], ptBytes) + + if len(padded)%padBlock != 0 { + targetLen := ((len(padded) / padBlock) + 1) * padBlock + paddedBuf := make([]byte, targetLen) + copy(paddedBuf, padded) + cryptoRand.Read(paddedBuf[len(padded):]) + padded = paddedBuf + } + + var iv [16]byte + if _, err := cryptoRand.Read(iv[:]); err != nil { + return "", fmt.Errorf("strenc: generate iv: %w", err) + } + + mac := hmac.New(sha256.New, masterKey[:]) + mac.Write(iv[:]) + var subkey [32]byte + copy(subkey[:], mac.Sum(nil)) + + entry := encryptEntryGCM(string(padded), subkey, iv) + entries = append(entries, entry) + } + + var poolCode strings.Builder + poolCode.WriteString("\nvar _masterSeed = [32]byte{") + for i, b := range seed { + if i > 0 { + poolCode.WriteString(", ") + } + fmt.Fprintf(&poolCode, "0x%02X", b) + } + poolCode.WriteString("}\n") + poolCode.WriteString("var _encEntries = []strenc.Entry{\n") + for _, e := range entries { + poolCode.WriteString("\t{Ciphertext: []byte{") + for i, b := range e.Ciphertext { + if i > 0 { + poolCode.WriteString(", ") + } + fmt.Fprintf(&poolCode, "0x%02X", b) + } + poolCode.WriteString("}, IV: [16]byte{") + for i, b := range e.IV { + if i > 0 { + poolCode.WriteString(", ") + } + fmt.Fprintf(&poolCode, "0x%02X", b) + } + poolCode.WriteString("}},\n") + } + poolCode.WriteString("}\n") + poolCode.WriteString("var _encPool = strenc.NewPool(_masterSeed, _encEntries)\n") + poolCode.WriteString("//go:noinline\nfunc _decStr(i int) string { _b := []byte(_encPool.GetString(i)); if len(_b) < 2 { return \"\" }; _n := int(_b[0]) | int(_b[1])<<8; if _n > len(_b)-2 { return \"\" }; return string(_b[2:2+_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] + poolCode.String() + src[insertAt:] + } + + src = t.ensureImport(src, "github.com/self-evolving-research/obfuscator/pkg/strenc") + + return src, nil +} + +func encryptEntryGCM(plaintext string, key [32]byte, iv [16]byte) strenc.Entry { + block, err := aes.NewCipher(key[:]) + if err != nil { + panic(fmt.Sprintf("transform: aes.NewCipher: %v", err)) + } + gcm, err := cipher.NewGCM(block) + if err != nil { + panic(fmt.Sprintf("transform: cipher.NewGCM: %v", err)) + } + nonceSize := gcm.NonceSize() + nonce := make([]byte, nonceSize) + copy(nonce, iv[:nonceSize]) + ct := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + return strenc.Entry{Ciphertext: ct, IV: iv} +} + +func writeHexLiteral(sb *strings.Builder, data []byte) { + sb.WriteString("[]byte{") + for i, b := range data { + if i > 0 { + sb.WriteString(", ") + } + fmt.Fprintf(sb, "0x%02X", b) + } + sb.WriteString("}") +} + +func findImportBlockEnd(src string) int { + idx := strings.Index(src, "import (") + if idx != -1 { + rest := src[idx+len("import ("):] + depth := 1 + for i, c := range rest { + if c == '(' { + depth++ + } else if c == ')' { + depth-- + if depth == 0 { + return idx + len("import (") + i + 1 + } + } + } + return -1 + } + + idx = strings.Index(src, "import \"") + if idx != -1 { + lineEnd := strings.Index(src[idx:], "\n") + if lineEnd != -1 { + return idx + lineEnd + 1 + } + return len(src) + } + + return -1 +} + +func shouldSkipString(s string) bool { + skipPrefixes := []string{ + "go:", "//", "+build", "package", "go:", "module", + "//go:", "// +build", + } + for _, p := range skipPrefixes { + if strings.HasPrefix(s, p) { + return true + } + } + if len(s) == 0 { + return true + } + if s[0] == '/' || s[0] == '+' { + return true + } + return false +} + +func unquoteGoString(s string) (string, error) { + if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { + return "", fmt.Errorf("not a quoted string") + } + s = s[1 : len(s)-1] + + var result strings.Builder + for i := 0; i < len(s); i++ { + if s[i] == '\\' && i+1 < len(s) { + switch s[i+1] { + case 'n': + result.WriteByte('\n') + i++ + case 't': + result.WriteByte('\t') + i++ + case 'r': + result.WriteByte('\r') + i++ + case '\\': + result.WriteByte('\\') + i++ + case '"': + result.WriteByte('"') + i++ + default: + result.WriteByte(s[i]) + } + } else { + result.WriteByte(s[i]) + } + } + return result.String(), nil +} + +func (t *Transformer) injectOpaquePredicates(src string) string { + lines := strings.Split(src, "\n") + var result []string + inFuncBody := false + funcStmtCount := 0 + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") { + inFuncBody = true + funcStmtCount = 0 + result = append(result, line) + continue + } + + if inFuncBody && trimmed == "}" { + inFuncBody = false + result = append(result, line) + continue + } + + if inFuncBody && strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, "\t\t") && trimmed != "" { + funcStmtCount++ + if funcStmtCount >= 3 && t.rng.Float64() < 0.4 { + result = append(result, "\t"+"if func() bool { return "+t.generateTrueExpression()+" }() { _ = struct{}{} }") + } + } + + result = append(result, line) + } + + return strings.Join(result, "\n") +} + +func (t *Transformer) injectDeadCode(src string) string { + lines := strings.Split(src, "\n") + var result []string + inFuncBody := false + funcStmtCount := 0 + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") { + inFuncBody = true + funcStmtCount = 0 + result = append(result, line) + continue + } + + if inFuncBody && trimmed == "}" { + inFuncBody = false + result = append(result, line) + continue + } + + if inFuncBody && strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, "\t\t") && trimmed != "" { + funcStmtCount++ + if funcStmtCount >= 3 && t.rng.Float64() < 0.3 { + result = append(result, "\t"+"if func() bool { return "+t.generateFalseExpression()+" }() { println(\"unreachable\") }") + } + } + + result = append(result, line) + } + + return strings.Join(result, "\n") +} + +func isValidIdent(s string) bool { + if len(s) == 0 { + return false + } + for i, c := range s { + if c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') { + continue + } + if i > 0 && c >= '0' && c <= '9' { + continue + } + return false + } + return true +} + +func getIndent(line string) string { + indent := "" + for _, c := range line { + if c == ' ' || c == '\t' { + indent += string(c) + } else { + break + } + } + return indent +} + +func convertConstToVar(src string) string { + lines := strings.Split(src, "\n") + inConstBlock := false + hasEncrypted := false + constStartLines := []int{} + + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "const (" { + inConstBlock = true + hasEncrypted = false + constStartLines = append(constStartLines, i) + continue + } + if inConstBlock && trimmed == ")" { + if hasEncrypted { + lines[constStartLines[len(constStartLines)-1]] = strings.Replace( + lines[constStartLines[len(constStartLines)-1]], "const (", "var (", 1, + ) + } + inConstBlock = false + continue + } + if inConstBlock && (strings.Contains(line, "base64.StdEncoding.DecodeString") || strings.Contains(line, "_decStr(")) { + hasEncrypted = true + } + } + + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "const ") && (strings.Contains(line, "base64.StdEncoding.DecodeString") || strings.Contains(line, "_decStr(")) { + lines[i] = strings.Replace(line, "const ", "var ", 1) + } + } + + return strings.Join(lines, "\n") +} + +func replaceInLines(lines []string, old, new string) []string { + var result []string + for _, l := range lines { + result = append(result, strings.Replace(l, old, new, 1)) + } + return result +} + +func (t *Transformer) RandomizeOrder(f *ast.File) { + if len(f.Decls) <= 1 { + return + } + + t.rng.Shuffle(len(f.Decls), func(i, j int) { + f.Decls[i], f.Decls[j] = f.Decls[j], f.Decls[i] + }) +} + +func (t *Transformer) injectBogusControlFlow(src string) string { + lines := strings.Split(src, "\n") + var result []string + inFuncBody := false + stmtCount := 0 + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") { + inFuncBody = true + stmtCount = 0 + result = append(result, line) + continue + } + + if inFuncBody && trimmed == "}" { + inFuncBody = false + result = append(result, line) + continue + } + + if inFuncBody && strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, "\t\t") && trimmed != "" { + stmtCount++ + if stmtCount >= 2 && t.rng.Float64() < 0.25 { + result = append(result, t.generateBogusBranch()) + } + } + + result = append(result, line) + } + + return strings.Join(result, "\n") +} + +func (t *Transformer) generateBogusBranch() string { + n := t.rng.Intn(4) + switch n { + case 0: + v := stringcrypt.RandomIdent("_bc", 5) + return fmt.Sprintf( + "\tif func() bool { %s := int64(1); for i := 0; i < 10; i++ { %s = %s*7%%49999999 }; return %s == 1 }() { _ = struct{}{} }", + v, v, v, v, + ) + case 1: + v := stringcrypt.RandomIdent("_bc", 5) + a := t.rng.Intn(100) + 1 + return fmt.Sprintf( + "\tif func() bool { %s := %d*%d; return %s%%2 == 0 && %s > 0 }() { _ = struct{}{} }", + v, a, a, v, v, + ) + case 2: + v := stringcrypt.RandomIdent("_bc", 5) + return fmt.Sprintf( + "\tif func() bool { %s := [4]byte{0x72, 0x6F, 0x6F, 0x74}; return len(%s) == 4 }() { _ = struct{}{} }", + v, v, + ) + default: + v := stringcrypt.RandomIdent("_bc", 5) + return fmt.Sprintf( + "\t{ %s := func() int { n := 0; for i := 1; i <= 100; i++ { n += i }; return n }(); _ = %s }", + v, v, + ) + } +} + +func (t *Transformer) flattenSimpleFunctions(src string) string { + lines := strings.Split(src, "\n") + var result []string + i := 0 + + for i < len(lines) { + trimmed := strings.TrimSpace(lines[i]) + + if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") && !strings.Contains(trimmed, "main") && !strings.Contains(trimmed, "init()") { + funcLines := []string{lines[i]} + i++ + 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 len(funcLines) > 5 && t.rng.Float64() < 0.4 { + flattened := flattenFunctionBody(funcLines) + result = append(result, flattened...) + } else { + result = append(result, funcLines...) + } + continue + } + + result = append(result, lines[i]) + i++ + } + + return strings.Join(result, "\n") +} + +func flattenFunctionBody(funcLines []string) []string { + funcSig := funcLines[0] + bodyLines := funcLines[1 : len(funcLines)-1] + closingBrace := funcLines[len(funcLines)-1] + + if len(bodyLines) < 3 { + return funcLines + } + + var statements [][]string + var current []string + depth := 0 + + for _, l := range bodyLines { + trimmed := strings.TrimSpace(l) + if trimmed == "" { + continue + } + + for _, c := range l { + if c == '{' { + depth++ + } + if c == '}' { + depth-- + } + } + + current = append(current, l) + + if depth <= 0 && trimmed != "{" { + statements = append(statements, current) + current = nil + depth = 0 + } + } + + if len(current) > 0 { + statements = append(statements, current) + } + + if len(statements) < 3 { + return funcLines + } + + for _, stmt := range statements { + for _, line := range stmt { + trimmed := strings.TrimSpace(line) + if strings.Contains(trimmed, ":=") { + parts := strings.SplitN(trimmed, ":=", 2) + varName := strings.TrimSpace(parts[0]) + if isValidIdent(varName) && !strings.Contains(varName, ",") { + rhs := strings.TrimSpace(parts[1]) + if inferTypeFromRHS(rhs) == "" { + return funcLines + } + } + } + } + } + + var result []string + result = append(result, funcSig) + + var hoistedVars []string + declaredVars := map[string]bool{} + for idx, stmt := range statements { + for _, line := range stmt { + trimmed := strings.TrimSpace(line) + if strings.Contains(trimmed, ":=") { + parts := strings.SplitN(trimmed, ":=", 2) + varName := strings.TrimSpace(parts[0]) + if !isValidIdent(varName) && !strings.Contains(varName, ",") { + continue + } + rhs := strings.TrimSpace(parts[1]) + if !strings.Contains(varName, ",") { + typ := inferTypeFromRHS(rhs) + if typ == "" { + continue + } + if !declaredVars[varName] { + hoistedVars = append(hoistedVars, "\tvar "+varName+" "+typ) + declaredVars[varName] = true + } + statements[idx] = replaceInLines(statements[idx], varName+" :=", varName+" =") + } else { + names := strings.Split(varName, ",") + allValid := true + for _, n := range names { + n = strings.TrimSpace(n) + if n != "" && !isValidIdent(n) { + allValid = false + break + } + } + if !allValid { + continue + } + for _, n := range names { + n = strings.TrimSpace(n) + if n != "" && !declaredVars[n] { + hoistedVars = append(hoistedVars, "\tvar "+n+" int") + declaredVars[n] = true + } + } + statements[idx] = replaceInLines(statements[idx], varName+" :=", varName+" =") + } + } + } + } + for _, v := range hoistedVars { + result = append(result, v) + } + + stateVar := stringcrypt.RandomIdent("_st", 4) + result = append(result, "\t"+stateVar+" := 0") + result = append(result, "\tfor {") + result = append(result, "\t\tswitch "+stateVar+" {") + + numStates := len(statements) + for idx, stmt := range statements { + result = append(result, fmt.Sprintf("\t\tcase %d:", idx)) + for _, line := range stmt { + result = append(result, "\t\t"+strings.TrimPrefix(line, "\t")) + } + if idx < numStates-1 { + result = append(result, fmt.Sprintf("\t\t\t%s = %d", stateVar, idx+1)) + } else { + result = append(result, "\t\t\treturn") + } + } + + result = append(result, "\t\tdefault:") + result = append(result, "\t\t\treturn") + result = append(result, "\t\t}") + result = append(result, "\t}") + result = append(result, closingBrace) + + return result +} + +func (t *Transformer) generateTrueExpression() string { + a := t.rng.Intn(1000) + 1 + switch t.rng.Intn(6) { + case 0: + return fmt.Sprintf("%d%%%d == 0", a*7, 7) + case 1: + return fmt.Sprintf("(%d^%d) == 0", a, a) + case 2: + return fmt.Sprintf("^(%d|%d) == ^%d", a, a, a) + case 3: + return fmt.Sprintf("(%d & %d) == %d", a, a, a) + case 4: + return fmt.Sprintf("len([%d]byte{}) == 0", t.rng.Intn(8)+1) + default: + b := t.rng.Intn(100) + 1 + return fmt.Sprintf("(%d+%d) > %d", b, a, b+a-1) + } +} + +func (t *Transformer) generateFalseExpression() string { + a := t.rng.Intn(0xFFFF) + 1 + switch t.rng.Intn(6) { + case 0: + return fmt.Sprintf("(%d - %d) < 0", a, a) + case 1: + return fmt.Sprintf("func() bool { return 1 < 0 }()") + case 2: + return fmt.Sprintf("func() bool { var _x = %d; return _x > _x }()", a) + case 3: + return fmt.Sprintf("func() bool { var _x int = %d; _x -= %d; return _x > 0 }()", a, a) + case 4: + return fmt.Sprintf("len([%d]byte{}) < 0", t.rng.Intn(8)+1) + default: + return fmt.Sprintf("func() bool { return (%d^%d) > 0 && (%d^%d) < 0 }()", a, a, a, a) + } +} + +func (t *Transformer) injectBuildID(src string) string { + buildID := fmt.Sprintf("BUILD_%d_%08X", t.config.BuildSeed, t.rng.Int31()) + decl := fmt.Sprintf("\nvar _buildID = %q // polymorphic build marker\nvar _ = _buildID\n", buildID) + + insertAfter := findImportBlockEnd(src) + + if insertAfter == -1 { + pkgIdx := strings.Index(src, "package ") + if pkgIdx == -1 { + return src + } + lineEnd := strings.Index(src[pkgIdx:], "\n") + if lineEnd == -1 { + return src + decl + } + insertAfter = pkgIdx + lineEnd + } + + return src[:insertAfter+1] + decl + src[insertAfter+1:] +} + +func (t *Transformer) indirectDispatch(src string) string { + lines := strings.Split(src, "\n") + var result []string + var wrappers []string + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + if !strings.HasPrefix(trimmed, "func ") || !strings.HasSuffix(trimmed, "{") { + result = append(result, line) + continue + } + + parenIdx := strings.Index(trimmed, "(") + if parenIdx == -1 { + result = append(result, line) + continue + } + + funcName := strings.TrimSpace(trimmed[5:parenIdx]) + + if funcName == "main" || funcName == "init" || funcName == "" || strings.ToUpper(funcName[:1]) == funcName[:1] || strings.HasPrefix(funcName, "_") { + result = append(result, line) + continue + } + + obfSuffix := fmt.Sprintf("_obf%d", t.rng.Intn(9000)+1000) + + renameFrom := "func " + funcName + "(" + renameTo := "func " + funcName + obfSuffix + "(" + renamedLine := strings.Replace(line, renameFrom, renameTo, 1) + result = append(result, renamedLine) + + restOfSig := trimmed[parenIdx:] + sigClean := strings.TrimSuffix(restOfSig, " {") + + depth := 0 + paramsEnd := -1 + for i, c := range sigClean { + if c == '(' { + depth++ + } else if c == ')' { + depth-- + if depth == 0 { + paramsEnd = i + break + } + } + } + + if paramsEnd == -1 { + continue + } + + params := sigClean[:paramsEnd+1] + returnType := strings.TrimSpace(sigClean[paramsEnd+1:]) + hasReturn := returnType != "" + + argsStart := strings.Index(params, "(") + 1 + argsEnd := strings.LastIndex(params, ")") + argStr := params[argsStart:argsEnd] + argNames := extractArgNames(argStr) + callArgs := strings.Join(argNames, ", ") + + if hasReturn { + wrapperFunc := fmt.Sprintf("func %s%s %s { return %s(%s) }", + funcName, params, returnType, funcName+obfSuffix, callArgs) + wrappers = append(wrappers, wrapperFunc) + } else { + wrapperFunc := fmt.Sprintf("func %s%s { %s(%s) }", + funcName, params, funcName+obfSuffix, callArgs) + wrappers = append(wrappers, wrapperFunc) + } + } + + if len(wrappers) > 0 { + insertAt := -1 + inImportBlock := false + for i, line := range result { + t := strings.TrimSpace(line) + if strings.HasPrefix(t, "import") && strings.Contains(t, "(") { + inImportBlock = true + continue + } + if inImportBlock && t == ")" { + insertAt = i + 1 + break + } + } + if insertAt == -1 { + for i, line := range result { + if strings.HasPrefix(strings.TrimSpace(line), "package ") { + insertAt = i + 1 + break + } + } + } + if insertAt >= 0 { + for insertAt < len(result) && strings.TrimSpace(result[insertAt]) == "" { + insertAt++ + } + newResult := make([]string, 0, len(result)+len(wrappers)+1) + newResult = append(newResult, result[:insertAt]...) + newResult = append(newResult, "") + newResult = append(newResult, wrappers...) + newResult = append(newResult, result[insertAt:]...) + result = newResult + } + } + + return strings.Join(result, "\n") +} + +func extractArgNames(argStr string) []string { + if strings.TrimSpace(argStr) == "" { + return nil + } + var names []string + for _, arg := range strings.Split(argStr, ",") { + arg = strings.TrimSpace(arg) + parts := strings.Fields(arg) + if len(parts) >= 2 { + names = append(names, parts[0]) + } else if len(parts) == 1 { + names = append(names, parts[0]) + } + } + return names +} + +func trimFirstParen(s string) string { + if len(s) > 0 && s[0] == '(' { + return s + } + return "(" + s +} + +func (t *Transformer) spliceCode(src string) string { + lines := strings.Split(src, "\n") + var result []string + i := 0 + + for i < len(lines) { + trimmed := strings.TrimSpace(lines[i]) + + if strings.HasPrefix(trimmed, "func ") && strings.HasSuffix(trimmed, "{") && + !strings.Contains(trimmed, "main") && !strings.Contains(trimmed, "init") && + !strings.Contains(trimmed, "func()") { + + funcSig := lines[i] + i++ + depth := 1 + var bodyLines []string + for i < len(lines) && depth > 0 { + for _, c := range lines[i] { + if c == '{' { + depth++ + } + if c == '}' { + depth-- + } + } + if depth > 0 { + bodyLines = append(bodyLines, lines[i]) + } + i++ + } + + simpleStmts := 0 + hasControlFlow := false + for _, bl := range bodyLines { + bt := strings.TrimSpace(bl) + if bt == "" || bt == "{" || bt == "}" { + continue + } + if strings.HasPrefix(bt, "if ") || strings.HasPrefix(bt, "for ") || + strings.HasPrefix(bt, "switch ") || strings.HasPrefix(bt, "select ") || + strings.HasPrefix(bt, "case ") || strings.HasPrefix(bt, "default:") { + hasControlFlow = true + break + } + if strings.HasPrefix(bt, "return ") || strings.HasPrefix(bt, "return") { + break + } + simpleStmts++ + } + + if simpleStmts >= 5 && !hasControlFlow && t.rng.Float64() < 0.5 { + spliced := t.buildSplicedFunction(funcSig, bodyLines) + result = append(result, spliced...) + } else { + result = append(result, funcSig) + result = append(result, bodyLines...) + result = append(result, "}") + } + continue + } + + result = append(result, lines[i]) + i++ + } + + return strings.Join(result, "\n") +} + +func (t *Transformer) buildSplicedFunction(funcSig string, bodyLines []string) []string { + var result []string + result = append(result, funcSig) + + var stmts []string + var hoistedVars []string + declaredVars := map[string]bool{} + for _, line := range bodyLines { + bt := strings.TrimSpace(line) + if bt == "" || bt == "{" || bt == "}" { + continue + } + if strings.Contains(bt, ":=") { + parts := strings.SplitN(bt, ":=", 2) + varName := strings.TrimSpace(parts[0]) + if !isValidIdent(varName) && !strings.Contains(varName, ",") { + stmts = append(stmts, bt) + continue + } + rhs := strings.TrimSpace(parts[1]) + if !strings.Contains(varName, ",") { + typ := inferTypeFromRHS(rhs) + if typ == "" { + stmts = append(stmts, bt) + continue + } + if !declaredVars[varName] { + hoistedVars = append(hoistedVars, "\tvar "+varName+" "+typ) + declaredVars[varName] = true + } + bt = varName + " = " + rhs + } else { + names := strings.Split(varName, ",") + allValid := true + for _, n := range names { + n = strings.TrimSpace(n) + if n != "" && !isValidIdent(n) { + allValid = false + break + } + } + if !allValid { + stmts = append(stmts, bt) + continue + } + for _, n := range names { + n = strings.TrimSpace(n) + if n != "" && !declaredVars[n] { + hoistedVars = append(hoistedVars, "\tvar "+n+" int") + declaredVars[n] = true + } + } + bt = varName + " = " + rhs + } + } + stmts = append(stmts, bt) + } + + for _, v := range hoistedVars { + result = append(result, v) + } + + stateVar := stringcrypt.RandomIdent("_sp", 4) + result = append(result, "\t"+stateVar+" := 0") + + returnVar := "" + lastStmt := strings.TrimSpace(stmts[len(stmts)-1]) + if strings.HasPrefix(lastStmt, "return ") { + returnExpr := strings.TrimPrefix(lastStmt, "return ") + returnVar = stringcrypt.RandomIdent("_ret", 4) + result = append(result, "\tvar "+returnVar+" "+inferTypeFromRHS(returnExpr)) + stmts[len(stmts)-1] = returnVar + " = " + returnExpr + } + + result = append(result, "\tfor {") + result = append(result, "\t\tswitch "+stateVar+" {") + + numFragments := 2 + if len(stmts) > 7 { + numFragments = 3 + } + fragmentSize := len(stmts) / numFragments + + fragIdx := 0 + for f := 0; f < numFragments; f++ { + result = append(result, fmt.Sprintf("\t\tcase %d:", f)) + end := (f + 1) * fragmentSize + if f == numFragments-1 { + end = len(stmts) + } + for j := f * fragmentSize; j < end; j++ { + result = append(result, "\t\t\t"+stmts[j]) + } + 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") + } + } + fragIdx++ + } + + 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, "}") + + return result +} + +func inferTypeFromRHS(rhs string) string { + rhs = strings.TrimSpace(rhs) + if strings.HasPrefix(rhs, "\"") || strings.HasPrefix(rhs, "`") { + return "string" + } + if strings.HasPrefix(rhs, "fmt.Sprintf") || strings.HasPrefix(rhs, "func() string") { + return "string" + } + if strings.HasPrefix(rhs, "[]byte") || strings.HasPrefix(rhs, "make([]byte") { + return "[]byte" + } + if rhs == "true" || rhs == "false" { + return "bool" + } + if strings.Contains(rhs, "sha256.Sum256") { + return "[32]byte" + } + return "" +} + +var vmLenPattern = regexp.MustCompile(`(?m)^func\s+(\w+)\((\w+)\s+string\)\s+bool\s*\{\s*\n\s*return\s+len\((\w+)\)\s*>\s*(\d+)\s*\n\s*\}`) + +var vmLenEqPattern = regexp.MustCompile(`(?m)^func\s+(\w+)\((\w+)\s+string\)\s+bool\s*\{\s*\n\s*return\s+len\((\w+)\)\s*==\s*(\d+)\s*\n\s*\}`) + +var vmLenLtPattern = regexp.MustCompile(`(?m)^func\s+(\w+)\((\w+)\s+string\)\s+bool\s*\{\s*\n\s*return\s+len\((\w+)\)\s*<\s*(\d+)\s*\n\s*\}`) + +var vmLenGePattern = regexp.MustCompile(`(?m)^func\s+(\w+)\((\w+)\s+string\)\s+bool\s*\{\s*\n\s*return\s+len\((\w+)\)\s*>=\s*(\d+)\s*\n\s*\}`) + +var vmArith2Pattern = regexp.MustCompile(`(?m)^func\s+(\w+)\((\w+),\s*(\w+)\s+(?:int|int64)\)\s+(?:int|int64)\s*\{\s*\n\s*return\s+(\w+)\s*([+\-*/%^&|])\s*(\w+)\s*\n\s*\}`) + +var vmArith1Pattern = regexp.MustCompile(`(?m)^func\s+(\w+)\((\w+)\s+(?:int|int64)\)\s+(?:int|int64)\s*\{\s*\n\s*return\s+(\w+)\s*([+\-*/%^&|])\s*(\d+)\s*\n\s*\}`) + +func (t *Transformer) virtualizeFunctions(src string) string { + type replacement struct { + start int + end int + text string + } + + var repls []replacement + + for _, loc := range vmLenPattern.FindAllStringIndex(src, -1) { + full := src[loc[0]:loc[1]] + sub := vmLenPattern.FindStringSubmatch(full) + if len(sub) >= 5 && sub[2] == sub[3] { + n, _ := strconv.ParseInt(sub[4], 10, 64) + bc := compileLenCmpBytecode("gt", n) + repls = append(repls, replacement{loc[0], loc[1], buildVMBoolFunc(sub[1], sub[2], bc)}) + } + } + for _, loc := range vmLenEqPattern.FindAllStringIndex(src, -1) { + full := src[loc[0]:loc[1]] + sub := vmLenEqPattern.FindStringSubmatch(full) + if len(sub) >= 5 && sub[2] == sub[3] { + n, _ := strconv.ParseInt(sub[4], 10, 64) + bc := compileLenCmpBytecode("eq", n) + repls = append(repls, replacement{loc[0], loc[1], buildVMBoolFunc(sub[1], sub[2], bc)}) + } + } + for _, loc := range vmLenLtPattern.FindAllStringIndex(src, -1) { + full := src[loc[0]:loc[1]] + sub := vmLenLtPattern.FindStringSubmatch(full) + if len(sub) >= 5 && sub[2] == sub[3] { + n, _ := strconv.ParseInt(sub[4], 10, 64) + bc := compileLenCmpBytecode("lt", n) + repls = append(repls, replacement{loc[0], loc[1], buildVMBoolFunc(sub[1], sub[2], bc)}) + } + } + for _, loc := range vmLenGePattern.FindAllStringIndex(src, -1) { + full := src[loc[0]:loc[1]] + sub := vmLenGePattern.FindStringSubmatch(full) + if len(sub) >= 5 && sub[2] == sub[3] { + n, _ := strconv.ParseInt(sub[4], 10, 64) + bc := compileLenCmpBytecode("ge", n) + repls = append(repls, replacement{loc[0], loc[1], buildVMBoolFunc(sub[1], sub[2], bc)}) + } + } + + for _, loc := range vmArith2Pattern.FindAllStringIndex(src, -1) { + full := src[loc[0]:loc[1]] + sub := vmArith2Pattern.FindStringSubmatch(full) + if len(sub) >= 7 { + funcName, p1, p2, left, op, right := sub[1], sub[2], sub[3], sub[4], sub[5], sub[6] + if left == p1 && right == p2 { + bc := compileArith2Bytecode(op, 0, 1) + repls = append(repls, replacement{loc[0], loc[1], buildVMIntFunc2(funcName, p1, p2, bc)}) + } else if left == p2 && right == p1 { + bc := compileArith2Bytecode(op, 1, 0) + repls = append(repls, replacement{loc[0], loc[1], buildVMIntFunc2(funcName, p1, p2, bc)}) + } + } + } + + for _, loc := range vmArith1Pattern.FindAllStringIndex(src, -1) { + full := src[loc[0]:loc[1]] + sub := vmArith1Pattern.FindStringSubmatch(full) + if len(sub) >= 6 { + funcName, param, left, op, nStr := sub[1], sub[2], sub[3], sub[4], sub[5] + if left == param { + n, _ := strconv.ParseInt(nStr, 10, 64) + bc := compileArith1Bytecode(op, n) + repls = append(repls, replacement{loc[0], loc[1], buildVMIntFunc1(funcName, param, bc)}) + } + } + } + + if len(repls) == 0 { + return src + } + + for i := len(repls) - 1; i >= 0; i-- { + r := repls[i] + src = src[:r.start] + r.text + src[r.end:] + } + + if !strings.Contains(src, "\"github.com/self-evolving-research/obfuscator/pkg/vm\"") { + src = t.ensureImport(src, "github.com/self-evolving-research/obfuscator/pkg/vm") + } + + return src +} + +func compileLenCmpBytecode(op string, n int64) []byte { + var bc []byte + bc = append(bc, vm.EncodeInstr(vm.OpPush, 0)...) + bc = append(bc, vm.EncodeInstr(vm.OpCall, 0)...) + bc = append(bc, vm.EncodeInstr(vm.OpPush, n)...) + switch op { + case "gt": + bc = append(bc, vm.EncodeInstr(vm.OpCmpGt, 0)...) + case "eq": + bc = append(bc, vm.EncodeInstr(vm.OpCmpEq, 0)...) + case "lt": + bc = append(bc, vm.EncodeInstr(vm.OpCmpLt, 0)...) + case "ge": + bc = append(bc, vm.EncodeInstr(vm.OpCmpLt, 0)...) + bc = append(bc, vm.EncodeInstr(vm.OpPush, 0)...) + bc = append(bc, vm.EncodeInstr(vm.OpCmpEq, 0)...) + } + bc = append(bc, vm.EncodeInstr(vm.OpHalt, 0)...) + return bc +} + +func compileArith2Bytecode(op string, leftID, rightID int64) []byte { + var bc []byte + bc = append(bc, vm.EncodeInstr(vm.OpPush, 0)...) // arity=0 + bc = append(bc, vm.EncodeInstr(vm.OpCall, leftID)...) // get left param + bc = append(bc, vm.EncodeInstr(vm.OpPush, 0)...) // arity=0 + bc = append(bc, vm.EncodeInstr(vm.OpCall, rightID)...) // get right param + switch op { + case "+": + bc = append(bc, vm.EncodeInstr(vm.OpAdd, 0)...) + case "-": + bc = append(bc, vm.EncodeInstr(vm.OpSub, 0)...) + case "*": + bc = append(bc, vm.EncodeInstr(vm.OpMul, 0)...) + case "/": + bc = append(bc, vm.EncodeInstr(vm.OpDiv, 0)...) + case "%": + bc = append(bc, vm.EncodeInstr(vm.OpMod, 0)...) + case "^": + bc = append(bc, vm.EncodeInstr(vm.OpXor, 0)...) + case "&": + bc = append(bc, vm.EncodeInstr(vm.OpAnd, 0)...) + case "|": + bc = append(bc, vm.EncodeInstr(vm.OpOr, 0)...) + } + bc = append(bc, vm.EncodeInstr(vm.OpHalt, 0)...) + return bc +} + +func compileArith1Bytecode(op string, n int64) []byte { + var bc []byte + bc = append(bc, vm.EncodeInstr(vm.OpPush, 0)...) // arity=0 + bc = append(bc, vm.EncodeInstr(vm.OpCall, 0)...) // get param + bc = append(bc, vm.EncodeInstr(vm.OpPush, n)...) // constant + switch op { + case "+": + bc = append(bc, vm.EncodeInstr(vm.OpAdd, 0)...) + case "-": + bc = append(bc, vm.EncodeInstr(vm.OpSub, 0)...) + case "*": + bc = append(bc, vm.EncodeInstr(vm.OpMul, 0)...) + case "/": + bc = append(bc, vm.EncodeInstr(vm.OpDiv, 0)...) + case "%": + bc = append(bc, vm.EncodeInstr(vm.OpMod, 0)...) + case "^": + bc = append(bc, vm.EncodeInstr(vm.OpXor, 0)...) + case "&": + bc = append(bc, vm.EncodeInstr(vm.OpAnd, 0)...) + case "|": + bc = append(bc, vm.EncodeInstr(vm.OpOr, 0)...) + } + bc = append(bc, vm.EncodeInstr(vm.OpHalt, 0)...) + return bc +} + +func buildVMBoolFunc(funcName, paramName string, bc []byte) string { + var byteLits []string + for _, b := range bc { + byteLits = append(byteLits, fmt.Sprintf("0x%02X", b)) + } + return fmt.Sprintf(`//go:noinline +func %s(%s string) bool { + _co := map[int]func([]int64) int64{ + 0: func(_ []int64) int64 { return int64(len(%s)) }, + } + _bc := []byte{%s} + _vm := vm.NewVM(_bc, _co) + return _vm.Run() != 0 +}`, funcName, paramName, paramName, strings.Join(byteLits, ", ")) +} + +func buildVMIntFunc2(funcName, p1, p2 string, bc []byte) string { + var byteLits []string + for _, b := range bc { + byteLits = append(byteLits, fmt.Sprintf("0x%02X", b)) + } + return fmt.Sprintf(`//go:noinline +func %s(%s, %s int64) int64 { + _co := map[int]func([]int64) int64{ + 0: func(_ []int64) int64 { return %s }, + 1: func(_ []int64) int64 { return %s }, + } + _bc := []byte{%s} + _vm := vm.NewVM(_bc, _co) + return _vm.Run() +}`, funcName, p1, p2, p1, p2, strings.Join(byteLits, ", ")) +} + +func buildVMIntFunc1(funcName, param string, bc []byte) string { + var byteLits []string + for _, b := range bc { + byteLits = append(byteLits, fmt.Sprintf("0x%02X", b)) + } + return fmt.Sprintf(`//go:noinline +func %s(%s int64) int64 { + _co := map[int]func([]int64) int64{ + 0: func(_ []int64) int64 { return %s }, + } + _bc := []byte{%s} + _vm := vm.NewVM(_bc, _co) + return _vm.Run() +}`, funcName, param, param, strings.Join(byteLits, ", ")) +} + +func (t *Transformer) ensureImport(src string, importPath string) string { + importBlock := `import ("` + importPath + `")` + + if idx := strings.Index(src, "import ("); idx != -1 { + insertAt := idx + len("import (") + return src[:insertAt] + "\n\t\"" + importPath + "\"" + src[insertAt:] + } + + if idx := strings.Index(src, "import \""); idx != -1 { + return src[:idx] + importBlock + "\n" + src[idx:] + } + + pkgEnd := strings.Index(src, "\n\n") + if pkgEnd == -1 { + pkgEnd = strings.Index(src, "\n") + } + if pkgEnd != -1 { + return src[:pkgEnd+1] + "\n" + importBlock + "\n" + src[pkgEnd+1:] + } + + return src +} diff --git a/pkg/antidebug/antidebug.go b/pkg/antidebug/antidebug.go new file mode 100644 index 0000000..ce4b79b --- /dev/null +++ b/pkg/antidebug/antidebug.go @@ -0,0 +1,173 @@ +package antidebug + +import ( + "fmt" + "os" + "runtime" + "time" +) + +func Check() error { + if runtime.GOOS == "linux" { + if err := checkPtrace(); err != nil { + return err + } + if err := disableCoreDump(); err != nil { + return err + } + } + + if err := checkTiming(); err != nil { + return err + } + + if err := checkEnv(); err != nil { + return err + } + + if err := checkParentProcess(); err != nil { + return err + } + + if err := CheckHooks(); err != nil { + return err + } + + return nil +} + +func checkPtrace() error { + self, err := os.ReadFile("/proc/self/status") + if err != nil { + return nil + } + + data := string(self) + for i := 0; i < len(data)-11; i++ { + if data[i:i+11] == "TracerPid:\t" { + j := i + 11 + for j < len(data) && data[j] >= '0' && data[j] <= '9' { + j++ + } + pid := data[i+11 : j] + if pid != "0" { + return fmt.Errorf("antidebug: debugger detected (tracer pid=%s)", pid) + } + break + } + } + return nil +} + +func disableCoreDump() error { + f, err := os.OpenFile("/proc/self/coredump_filter", os.O_WRONLY, 0) + if err != nil { + return nil + } + defer f.Close() + _, _ = f.WriteString("0") + return nil +} + +func checkTiming() error { + start := time.Now() + sum := 0 + for i := 0; i < 1000000; i++ { + sum += i + } + elapsed := time.Since(start) + + if elapsed > 200*time.Millisecond { + return fmt.Errorf("antidebug: timing anomaly detected (%v for 1M iterations)", elapsed) + } + + _ = sum + return nil +} + +func checkEnv() error { + suspiciousVars := []string{ + "GODEBUG", + "DELVE_LISTENER", + "DEBUGINFOD_URL", + "RR_LOG_FILE", + "_JAVA_OPTIONS", + "LD_PRELOAD", + "DYLD_INSERT_LIBRARIES", + } + + for _, v := range suspiciousVars { + if os.Getenv(v) != "" { + return fmt.Errorf("antidebug: suspicious env var %s=%s", v, os.Getenv(v)) + } + } + return nil +} + +func checkParentProcess() error { + if runtime.GOOS != "linux" { + return nil + } + + ppid := os.Getppid() + if ppid <= 1 { + return nil + } + + commPath := fmt.Sprintf("/proc/%d/comm", ppid) + comm, err := os.ReadFile(commPath) + if err != nil { + return nil + } + + parent := string(comm) + debuggers := []string{ + "gdb", "lldb", "strace", "ltrace", "ida", "ida64", + "radare2", "r2", "ghidra", "x64dbg", "ollydbg", + "delve", "debugserver", + } + + for _, d := range debuggers { + if len(parent) >= len(d) && parent[:len(d)] == d { + return fmt.Errorf("antidebug: parent process is debugger: %s", parent) + } + } + return nil +} + +func AntiAttach() { + if runtime.GOOS != "linux" { + return + } + go func() { + for { + time.Sleep(500 * time.Millisecond) + if err := checkPtrace(); err != nil { + os.Exit(66) + } + } + }() +} + +func AntiDump() { + if runtime.GOOS != "linux" { + return + } + go func() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for range ticker.C { + _ = disableCoreDump() + } + }() +} + +func CheckHooks() error { + if err := checkFunctionPrologues(); err != nil { + return err + } + if err := scanForBreakpoints(); err != nil { + return err + } + return nil +} diff --git a/pkg/antidebug/antidebug_hook_darwin.go b/pkg/antidebug/antidebug_hook_darwin.go new file mode 100644 index 0000000..85540a0 --- /dev/null +++ b/pkg/antidebug/antidebug_hook_darwin.go @@ -0,0 +1,11 @@ +//go:build darwin + +package antidebug + +func checkFunctionPrologues() error { + return nil +} + +func scanForBreakpoints() error { + return nil +} diff --git a/pkg/antidebug/antidebug_hook_linux.go b/pkg/antidebug/antidebug_hook_linux.go new file mode 100644 index 0000000..9237359 --- /dev/null +++ b/pkg/antidebug/antidebug_hook_linux.go @@ -0,0 +1,167 @@ +//go:build linux + +package antidebug + +import ( + "bufio" + "fmt" + "os" + "reflect" + "runtime" + "strings" + + "github.com/self-evolving-research/obfuscator/pkg/syscallobf" +) + +func checkFunctionPrologues() error { + type target struct { + name string + addr uintptr + } + + targets := []target{ + {"os.Exit", reflect.ValueOf(os.Exit).Pointer()}, + {"runtime.Goexit", reflect.ValueOf(runtime.Goexit).Pointer()}, + {"fmt.Printf", reflect.ValueOf(fmt.Printf).Pointer()}, + {"os.ReadFile", reflect.ValueOf(os.ReadFile).Pointer()}, + } + + for _, t := range targets { + if t.addr == 0 { + continue + } + + prologue, err := readProcMemory(t.addr, t.addr+8) + if err != nil || len(prologue) < 2 { + continue + } + + if prologue[0] == 0xE9 { + return fmt.Errorf("antidebug: JMP hook detected at %s (0x%x)", t.name, t.addr) + } + if prologue[0] == 0xEB { + return fmt.Errorf("antidebug: short JMP hook detected at %s (0x%x)", t.name, t.addr) + } + if prologue[0] == 0xFF && prologue[1] == 0x25 { + return fmt.Errorf("antidebug: indirect JMP hook detected at %s (0x%x)", t.name, t.addr) + } + if prologue[0] == 0xCC && prologue[1] == 0xCC { + return fmt.Errorf("antidebug: INT3 breakpoint at %s (0x%x)", t.name, t.addr) + } + } + + return nil +} + +func scanForBreakpoints() error { + mapsData, err := readProcFile("/proc/self/maps") + if err != nil { + return nil + } + + scanner := bufio.NewScanner(strings.NewReader(string(mapsData))) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + + if !strings.Contains(parts[1], "x") { + continue + } + + if len(parts) >= 6 && strings.HasPrefix(parts[5], "[") { + continue + } + + addrRange := strings.Split(parts[0], "-") + if len(addrRange) != 2 { + continue + } + + var start, end uintptr + fmt.Sscanf(addrRange[0], "%x", &start) + fmt.Sscanf(addrRange[1], "%x", &end) + + if end <= start || end-start > 50*1024*1024 { + continue + } + + mem, err := readProcMemory(start, end) + if err != nil || len(mem) == 0 { + continue + } + + maxConsecutive := 0 + consecutive := 0 + for _, b := range mem { + if b == 0xCC { + consecutive++ + if consecutive > maxConsecutive { + maxConsecutive = consecutive + } + } else { + consecutive = 0 + } + } + + if maxConsecutive >= 8 { + return fmt.Errorf("antidebug: breakpoint cluster (%d consecutive INT3) at 0x%x-0x%x", maxConsecutive, start, end) + } + } + + return nil +} + +func readProcFile(path string) ([]byte, error) { + fd, err := syscallobf.ObfOpen(path, 0, 0) + if fd < 0 { + return nil, err + } + defer syscallobf.ObfClose(fd) + + var result []byte + buf := make([]byte, 4096) + for { + n, readErr := syscallobf.ObfRead(fd, buf) + if n > 0 { + result = append(result, buf[:n]...) + } + if readErr != nil || n == 0 { + break + } + } + return result, nil +} + +func readProcMemory(start, end uintptr) ([]byte, error) { + fd, err := syscallobf.ObfOpen("/proc/self/mem", 0, 0) + if fd < 0 { + return nil, err + } + defer syscallobf.ObfClose(fd) + + size := int(end - start) + if size <= 0 || size > 50*1024*1024 { + return nil, fmt.Errorf("invalid region size") + } + + buf := make([]byte, size) + + if _, seekErr := syscallobf.ObfLseek(fd, int64(start), 0); seekErr != nil { + return nil, seekErr + } + + totalRead := 0 + for totalRead < size { + n, readErr := syscallobf.ObfRead(fd, buf[totalRead:]) + if n > 0 { + totalRead += n + } + if readErr != nil || n == 0 { + break + } + } + return buf[:totalRead], nil +} diff --git a/pkg/integrity/integrity.go b/pkg/integrity/integrity.go new file mode 100644 index 0000000..7e330d9 --- /dev/null +++ b/pkg/integrity/integrity.go @@ -0,0 +1,165 @@ +package integrity + +import ( + "crypto/rand" + "crypto/sha256" + "debug/elf" + "debug/macho" + "encoding/binary" + "fmt" + "os" + "runtime" + "sync" + "time" +) + +var ( + expectedHash [32]byte + hashIsSet bool + mu sync.RWMutex +) + +func SetExpectedHash(hash [32]byte) { + mu.Lock() + defer mu.Unlock() + expectedHash = hash + hashIsSet = true +} + +func SetExpectedHashHex(hex string) error { + var hash [32]byte + for i := 0; i < 32; i++ { + _, err := fmt.Sscanf(hex[i*2:i*2+2], "%x", &hash[i]) + if err != nil { + return fmt.Errorf("integrity: invalid hash hex: %w", err) + } + } + SetExpectedHash(hash) + return nil +} + +func Verify() error { + mu.RLock() + set := hashIsSet + mu.RUnlock() + if !set { + return nil + } + + selfPath, err := os.Executable() + if err != nil { + return fmt.Errorf("integrity: cannot determine executable path: %w", err) + } + + hash, err := hashTextSection(selfPath) + if err != nil { + return fmt.Errorf("integrity: cannot hash text section: %w", err) + } + + mu.RLock() + expected := expectedHash + mu.RUnlock() + + if hash != expected { + return fmt.Errorf("integrity: binary has been tampered with") + } + return nil +} + +func StartMonitor(interval time.Duration, onTamper func(error)) { + mu.RLock() + set := hashIsSet + mu.RUnlock() + if !set { + return + } + + go func() { + jitter := time.Duration(randIntn(int(interval.Milliseconds()/2))) * time.Millisecond + time.Sleep(jitter) + + ticker := time.NewTicker(interval) + defer ticker.Stop() + for range ticker.C { + if err := Verify(); err != nil { + if onTamper != nil { + onTamper(err) + } + os.Exit(66) + } + } + }() +} + +func hashTextSection(path string) ([32]byte, error) { + switch runtime.GOOS { + case "linux": + return hashTextELF(path) + case "darwin": + return hashTextMacho(path) + default: + return hashWholeFile(path) + } +} + +func hashTextELF(path string) ([32]byte, error) { + f, err := elf.Open(path) + if err != nil { + return [32]byte{}, err + } + defer f.Close() + + section := f.Section(".text") + if section == nil { + return hashWholeFile(path) + } + data, err := section.Data() + if err != nil { + return [32]byte{}, err + } + return sha256.Sum256(data), nil +} + +func hashTextMacho(path string) ([32]byte, error) { + f, err := macho.Open(path) + if err != nil { + return [32]byte{}, err + } + defer f.Close() + + for _, section := range f.Sections { + if section.Name == "__text" { + data, err := section.Data() + if err != nil { + return [32]byte{}, err + } + return sha256.Sum256(data), nil + } + } + return hashWholeFile(path) +} + +func hashWholeFile(path string) ([32]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return [32]byte{}, err + } + return sha256.Sum256(data), nil +} + +func ComputeHashForEmbedding(path string) (string, error) { + hash, err := hashTextSection(path) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", hash), nil +} + +func randIntn(n int) int { + if n <= 0 { + return 0 + } + var buf [4]byte + _, _ = rand.Read(buf[:]) + return int(binary.BigEndian.Uint32(buf[:])) % n +} diff --git a/pkg/pageman/guard.go b/pkg/pageman/guard.go new file mode 100644 index 0000000..9f7052a --- /dev/null +++ b/pkg/pageman/guard.go @@ -0,0 +1,114 @@ +package pageman + +import ( + "os" + "runtime" + "sync" + "time" +) + +type Guard struct { + mu sync.Mutex + buffers []*SecureBuffer + interval time.Duration + stop chan struct{} + stopped bool +} + +func NewGuard(interval time.Duration) *Guard { + return &Guard{ + interval: interval, + stop: make(chan struct{}), + } +} + +func (g *Guard) Watch(buf *SecureBuffer) { + g.mu.Lock() + defer g.mu.Unlock() + g.buffers = append(g.buffers, buf) +} + +func (g *Guard) Start() { + go g.loop() +} + +func (g *Guard) Stop() { + g.mu.Lock() + defer g.mu.Unlock() + + if g.stopped { + return + } + + g.stopped = true + close(g.stop) +} + +func (g *Guard) loop() { + ticker := time.NewTicker(g.interval) + defer ticker.Stop() + + var prevMaps []byte + + for { + select { + case <-g.stop: + return + case <-ticker.C: + g.enforceAutoLock() + if runtime.GOOS == "linux" { + prevMaps = g.checkMapsTampering(prevMaps) + } + } + } +} + +func (g *Guard) enforceAutoLock() { + g.mu.Lock() + bufs := make([]*SecureBuffer, len(g.buffers)) + copy(bufs, g.buffers) + g.mu.Unlock() + + now := time.Now() + for _, buf := range bufs { + if buf.IsClosed() || buf.IsLocked() { + continue + } + if now.Sub(buf.LastAccess()) > buf.AutoLockDuration() { + buf.forceLock() + } + } +} + +func (g *Guard) checkMapsTampering(prev []byte) []byte { + current, err := os.ReadFile("/proc/self/maps") + if err != nil { + return prev + } + + if prev != nil && !equalBytes(prev, current) { + g.mu.Lock() + for _, buf := range g.buffers { + if !buf.IsClosed() && !buf.IsLocked() { + buf.forceLock() + } + } + g.mu.Unlock() + } + + result := make([]byte, len(current)) + copy(result, current) + return result +} + +func equalBytes(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/pkg/pageman/pageman.go b/pkg/pageman/pageman.go new file mode 100644 index 0000000..94943f8 --- /dev/null +++ b/pkg/pageman/pageman.go @@ -0,0 +1,216 @@ +package pageman + +import ( + "crypto/rand" + "errors" + "sync" + "time" +) + +var ( + ErrAlreadyLocked = errors.New("pageman: buffer already locked") + ErrAlreadyUnlocked = errors.New("pageman: buffer already unlocked") + ErrClosed = errors.New("pageman: buffer closed") +) + +type SecureBuffer struct { + mu sync.Mutex + data []byte + encrypted []byte + key [32]byte + locked bool + closed bool + lastAccess time.Time + autoLock time.Duration +} + +func NewSecureBuffer(initialData []byte, autoLock time.Duration) *SecureBuffer { + sb := &SecureBuffer{ + data: make([]byte, len(initialData)), + autoLock: autoLock, + lastAccess: time.Now(), + } + copy(sb.data, initialData) + + if _, err := rand.Read(sb.key[:]); err != nil { + panic("pageman: failed to generate key: " + err.Error()) + } + + sb.encryptInPlace() + sb.locked = true + + for i := range initialData { + initialData[i] = 0 + } + + return sb +} + +func (sb *SecureBuffer) Lock() error { + sb.mu.Lock() + defer sb.mu.Unlock() + + if sb.closed { + return ErrClosed + } + if sb.locked { + return ErrAlreadyLocked + } + + sb.encryptInPlace() + sb.locked = true + return nil +} + +func (sb *SecureBuffer) Unlock() error { + sb.mu.Lock() + defer sb.mu.Unlock() + + if sb.closed { + return ErrClosed + } + if !sb.locked { + return ErrAlreadyUnlocked + } + + sb.decryptInPlace() + sb.locked = false + sb.lastAccess = time.Now() + return nil +} + +func (sb *SecureBuffer) Access(fn func([]byte)) error { + sb.mu.Lock() + + if sb.closed { + sb.mu.Unlock() + return ErrClosed + } + + wasLocked := sb.locked + if wasLocked { + sb.decryptInPlace() + sb.locked = false + } + + sb.lastAccess = time.Now() + + fn(sb.data) + + if wasLocked { + sb.encryptInPlace() + sb.locked = true + } + + sb.mu.Unlock() + return nil +} + +func (sb *SecureBuffer) Close() { + sb.mu.Lock() + defer sb.mu.Unlock() + + if sb.closed { + return + } + + sb.zeroSlice(sb.data) + sb.zeroSlice(sb.encrypted) + sb.zeroKey() + sb.data = nil + sb.encrypted = nil + sb.locked = true + sb.closed = true +} + +func (sb *SecureBuffer) IsLocked() bool { + sb.mu.Lock() + defer sb.mu.Unlock() + return sb.locked +} + +func (sb *SecureBuffer) IsClosed() bool { + sb.mu.Lock() + defer sb.mu.Unlock() + return sb.closed +} + +func (sb *SecureBuffer) LastAccess() time.Time { + sb.mu.Lock() + defer sb.mu.Unlock() + return sb.lastAccess +} + +func (sb *SecureBuffer) AutoLockDuration() time.Duration { + return sb.autoLock +} + +func (sb *SecureBuffer) forceLock() { + sb.mu.Lock() + defer sb.mu.Unlock() + + if sb.closed || sb.locked { + return + } + + sb.encryptInPlace() + sb.locked = true +} + +func (sb *SecureBuffer) encryptInPlace() { + if len(sb.data) == 0 { + return + } + + sb.encrypted = make([]byte, len(sb.data)) + for i, b := range sb.data { + sb.encrypted[i] = b ^ sb.key[i%32] + } + + for i := 0; i < 32; i++ { + sb.key[i] = rotateKey(sb.key[i], i) + } + + sb.zeroSlice(sb.data) + sb.data = nil +} + +func (sb *SecureBuffer) decryptInPlace() { + if len(sb.encrypted) == 0 { + return + } + + for i := 0; i < 32; i++ { + sb.key[i] = unrotateKey(sb.key[i], i) + } + + sb.data = make([]byte, len(sb.encrypted)) + for i, b := range sb.encrypted { + sb.data[i] = b ^ sb.key[i%32] + } + + sb.zeroSlice(sb.encrypted) + sb.encrypted = nil +} + +func (sb *SecureBuffer) zeroSlice(b []byte) { + for i := range b { + b[i] = 0 + } +} + +func (sb *SecureBuffer) zeroKey() { + for i := range sb.key { + sb.key[i] = 0 + } +} + +func rotateKey(b byte, pos int) byte { + shift := uint((pos*3 + 7) % 8) + return b<>(8-shift) +} + +func unrotateKey(b byte, pos int) byte { + shift := uint((pos*3 + 7) % 8) + return b>>shift | b<<(8-shift) +} diff --git a/pkg/strenc/keyderive.go b/pkg/strenc/keyderive.go new file mode 100644 index 0000000..6deb35b --- /dev/null +++ b/pkg/strenc/keyderive.go @@ -0,0 +1,48 @@ +package strenc + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/binary" + "os" + "runtime" +) + +const buildSeed = 0xDEAD_BEEF_CAFE_BABE + +func DeriveMasterKey(seed [32]byte) [32]byte { + var out [32]byte + + mac := hmac.New(sha256.New, seed[:]) + mac.Write([]byte("strenc.masterkey.v1")) + copy(out[:16], mac.Sum(nil)) + + mac.Reset() + mac.Write(seed[:]) + mac.Write([]byte("strenc.masterkey.v2")) + copy(out[16:], mac.Sum(nil)) + + return out +} + +func DeriveFromBinary() [32]byte { + exePath, err := os.Executable() + if err != nil { + exePath = "unknown" + } + + mac := hmac.New(sha256.New, []byte("strenc.binary.seed")) + mac.Write([]byte(exePath)) + + var seedBuf [8]byte + binary.LittleEndian.PutUint64(seedBuf[:], buildSeed) + mac.Write(seedBuf[:]) + + mac.Write([]byte(runtime.GOOS)) + mac.Write([]byte(runtime.GOARCH)) + + var seed [32]byte + copy(seed[:], mac.Sum(nil)) + + return DeriveMasterKey(seed) +} diff --git a/pkg/strenc/strenc.go b/pkg/strenc/strenc.go new file mode 100644 index 0000000..29d7430 --- /dev/null +++ b/pkg/strenc/strenc.go @@ -0,0 +1,151 @@ +package strenc + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "fmt" +) + +type Entry struct { + Ciphertext []byte + IV [16]byte +} + +type Pool struct { + data []byte + offsets []int + lengths []int + entries int + masterKey [32]byte +} + +func NewPool(masterKeySeed [32]byte, entries []Entry) *Pool { + p := &Pool{ + masterKey: DeriveMasterKey(masterKeySeed), + } + p.entries = len(entries) + + totalSize := 0 + for _, e := range entries { + totalSize += 16 + len(e.Ciphertext) + } + + p.data = make([]byte, 0, totalSize) + p.offsets = make([]int, len(entries)) + p.lengths = make([]int, len(entries)) + + for i, e := range entries { + p.offsets[i] = len(p.data) + p.data = append(p.data, e.IV[:]...) + p.data = append(p.data, e.Ciphertext...) + p.lengths[i] = 16 + len(e.Ciphertext) + } + + return p +} + +func (p *Pool) StringCount() int { + return p.entries +} + +func (p *Pool) deriveSubkey(iv [16]byte) [32]byte { + mac := hmac.New(sha256.New, p.masterKey[:]) + mac.Write(iv[:]) + var subkey [32]byte + copy(subkey[:], mac.Sum(nil)) + return subkey +} + +func (p *Pool) decryptAt(index int) ([]byte, error) { + if index < 0 || index >= p.entries { + return nil, fmt.Errorf("strenc: index %d out of range [0, %d)", index, p.entries) + } + + start := p.offsets[index] + raw := p.data[start : start+p.lengths[index]] + + var iv [16]byte + copy(iv[:], raw[:16]) + ct := raw[16:] + + subkey := p.deriveSubkey(iv) + block, err := aes.NewCipher(subkey[:]) + if err != nil { + return nil, fmt.Errorf("strenc: aes.NewCipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("strenc: cipher.NewGCM: %w", err) + } + + nonceSize := gcm.NonceSize() + if len(ct) < nonceSize { + return nil, fmt.Errorf("strenc: ciphertext too short") + } + + nonce := ct[:nonceSize] + ciphertext := ct[nonceSize:] + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("strenc: gcm.Open: %w", err) + } + + return plaintext, nil +} + +func (p *Pool) GetString(index int) string { + pt, err := p.decryptAt(index) + if err != nil { + return "" + } + return string(pt) +} + +func (p *Pool) GetStringZero(index int, fn func(string)) { + pt, err := p.decryptAt(index) + if err != nil { + fn("") + return + } + + buf := make([]byte, len(pt)) + copy(buf, pt) + s := string(buf) + + for i := range pt { + pt[i] = 0 + } + + fn(s) + + for i := range buf { + buf[i] = 0 + } +} + +func EncryptEntry(plaintext string, key [32]byte, iv [16]byte) Entry { + block, err := aes.NewCipher(key[:]) + if err != nil { + panic(fmt.Sprintf("strenc: aes.NewCipher: %v", err)) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + panic(fmt.Sprintf("strenc: cipher.NewGCM: %v", err)) + } + + nonceSize := gcm.NonceSize() + nonce := make([]byte, nonceSize) + copy(nonce, iv[:nonceSize]) + + ct := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + + return Entry{ + Ciphertext: ct, + IV: iv, + } +} diff --git a/pkg/stringcrypt/stringcrypt.go b/pkg/stringcrypt/stringcrypt.go new file mode 100644 index 0000000..41e00d1 --- /dev/null +++ b/pkg/stringcrypt/stringcrypt.go @@ -0,0 +1,70 @@ +package stringcrypt + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "math/big" +) + +type EncryptedString struct { + ciphertext []byte + key []byte +} + +func Encrypt(plaintext string) EncryptedString { + data := []byte(plaintext) + key := make([]byte, len(data)) + if _, err := rand.Read(key); err != nil { + panic(fmt.Sprintf("stringcrypt: rand.Read failed: %v", err)) + } + + ciphertext := make([]byte, len(data)) + for i := range data { + ciphertext[i] = data[i] ^ key[i] + } + + return EncryptedString{ciphertext: ciphertext, key: key} +} + +func (e EncryptedString) Decrypt() string { + plaintext := make([]byte, len(e.ciphertext)) + for i := range e.ciphertext { + plaintext[i] = e.ciphertext[i] ^ e.key[i] + } + return string(plaintext) +} + +func (e EncryptedString) CiphertextB64() string { + return base64.StdEncoding.EncodeToString(e.ciphertext) +} + +func (e EncryptedString) KeyB64() string { + return base64.StdEncoding.EncodeToString(e.key) +} + +func GenerateRuntimeDecryptCall(ctB64, keyB64 string) string { + return fmt.Sprintf( + `(func() string { + _ct, _ := base64.StdEncoding.DecodeString(%q) + _ky, _ := base64.StdEncoding.DecodeString(%q) + _pt := make([]byte, len(_ct)) + for _i := range _ct { + _pt[_i] = _ct[_i] ^ _ky[_i] + } + return string(_pt) + })()`, ctB64, keyB64) +} + +func RandomIdent(prefix string, length int) string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + b := make([]byte, length) + for i := range b { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + panic(fmt.Sprintf("stringcrypt: rand.Int failed: %v", err)) + } + b[i] = charset[n.Int64()] + } + return prefix + string(b) +} diff --git a/pkg/syscallobf/encode.go b/pkg/syscallobf/encode.go new file mode 100644 index 0000000..51505b9 --- /dev/null +++ b/pkg/syscallobf/encode.go @@ -0,0 +1,44 @@ +package syscallobf + +import ( + "crypto/rand" + "encoding/binary" + "sync" +) + +var ( + dispatchKey uint64 + dispatchOnce sync.Once +) + +func initDispatchKey() { + dispatchOnce.Do(func() { + var buf [8]byte + rand.Read(buf[:]) + dispatchKey = binary.LittleEndian.Uint64(buf[:]) + if dispatchKey == 0 { + dispatchKey = 0xDEADBEEFCAFEBABE + } + }) +} + +func EncodeTrap(num uintptr) uintptr { + initDispatchKey() + return uintptr(uint64(num) ^ dispatchKey) +} + +func DecodeTrap(encoded uintptr) uintptr { + return uintptr(uint64(encoded) ^ dispatchKey) +} + +type encodedSyscall struct { + encoded uintptr +} + +func NewEncodedSyscall(num uintptr) encodedSyscall { + return encodedSyscall{encoded: EncodeTrap(num)} +} + +func (e encodedSyscall) Trap() uintptr { + return DecodeTrap(e.encoded) +} diff --git a/pkg/syscallobf/syscall_darwin.go b/pkg/syscallobf/syscall_darwin.go new file mode 100644 index 0000000..c5d2017 --- /dev/null +++ b/pkg/syscallobf/syscall_darwin.go @@ -0,0 +1,39 @@ +package syscallobf + +import "syscall" + +func ObfPtrace(request int, pid uintptr, addr uintptr, data uintptr) (uintptr, uintptr, syscall.Errno) { + return 0, 0, syscall.ENOSYS +} + +func ObfGetpid() int { + return 0 +} + +func ObfKill(pid int, sig int) error { + return syscall.ENOSYS +} + +func ObfOpen(path string, flags int, mode int) (int, error) { + return -1, syscall.ENOSYS +} + +func ObfClose(fd int) error { + return syscall.ENOSYS +} + +func ObfRead(fd int, buf []byte) (int, error) { + return 0, syscall.ENOSYS +} + +func ObfMmap(addr uintptr, length uintptr, prot int, flags int, fd int, offset uintptr) (uintptr, error) { + return 0, syscall.ENOSYS +} + +func ObfMprotect(addr uintptr, length uintptr, prot int) error { + return syscall.ENOSYS +} + +func ObfLseek(fd int, offset int64, whence int) (int64, error) { + return 0, syscall.ENOSYS +} diff --git a/pkg/syscallobf/syscall_linux.go b/pkg/syscallobf/syscall_linux.go new file mode 100644 index 0000000..5a3746e --- /dev/null +++ b/pkg/syscallobf/syscall_linux.go @@ -0,0 +1,100 @@ +package syscallobf + +import ( + "syscall" + "unsafe" +) + +var ( + encPtraceTrap = NewEncodedSyscall(101) + encGetpidTrap = NewEncodedSyscall(39) + encKillTrap = NewEncodedSyscall(62) + encOpenTrap = NewEncodedSyscall(2) + encCloseTrap = NewEncodedSyscall(3) + encReadTrap = NewEncodedSyscall(0) + encMmapTrap = NewEncodedSyscall(9) + encMprotectTrap = NewEncodedSyscall(10) + encLseekTrap = NewEncodedSyscall(8) +) + +func ObfPtrace(request int, pid uintptr, addr uintptr, data uintptr) (uintptr, uintptr, syscall.Errno) { + trap := encPtraceTrap.Trap() + return syscall.RawSyscall6(trap, uintptr(request), pid, addr, data, 0, 0) +} + +func ObfGetpid() int { + trap := encGetpidTrap.Trap() + r1, _, _ := syscall.RawSyscall(trap, 0, 0, 0) + return int(r1) +} + +func ObfKill(pid int, sig int) error { + trap := encKillTrap.Trap() + _, _, errno := syscall.RawSyscall(trap, uintptr(pid), uintptr(sig), 0) + if errno != 0 { + return errno + } + return nil +} + +func ObfOpen(path string, flags int, mode int) (int, error) { + trap := encOpenTrap.Trap() + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return -1, err + } + r1, _, errno := syscall.RawSyscall(trap, uintptr(unsafe.Pointer(pathBytes)), uintptr(flags), uintptr(mode)) + if errno != 0 { + return int(r1), errno + } + return int(r1), nil +} + +func ObfClose(fd int) error { + trap := encCloseTrap.Trap() + _, _, errno := syscall.RawSyscall(trap, uintptr(fd), 0, 0) + if errno != 0 { + return errno + } + return nil +} + +func ObfRead(fd int, buf []byte) (int, error) { + trap := encReadTrap.Trap() + var p *byte + if len(buf) > 0 { + p = &buf[0] + } + r1, _, errno := syscall.RawSyscall(trap, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(len(buf))) + if errno != 0 { + return int(r1), errno + } + return int(r1), nil +} + +func ObfMmap(addr uintptr, length uintptr, prot int, flags int, fd int, offset uintptr) (uintptr, error) { + trap := encMmapTrap.Trap() + r1, _, errno := syscall.Syscall6(trap, addr, length, uintptr(prot), uintptr(flags), uintptr(fd), offset) + if errno != 0 { + return 0, errno + } + return r1, nil +} + +func ObfMprotect(addr uintptr, length uintptr, prot int) error { + trap := encMprotectTrap.Trap() + _, _, errno := syscall.RawSyscall(trap, addr, length, uintptr(prot)) + if errno != 0 { + return errno + } + return nil +} + +func ObfLseek(fd int, offset int64, whence int) (int64, error) { + trap := encLseekTrap.Trap() + r1, _, errno := syscall.RawSyscall(trap, uintptr(fd), uintptr(offset), uintptr(whence)) + if errno != 0 { + return int64(r1), errno + } + return int64(r1), nil +} diff --git a/pkg/syscallobf/syscallobf.go b/pkg/syscallobf/syscallobf.go new file mode 100644 index 0000000..655b7b1 --- /dev/null +++ b/pkg/syscallobf/syscallobf.go @@ -0,0 +1,26 @@ +package syscallobf + +// ObfuscatedCall wraps a syscall with obfuscated number +type ObfuscatedCall struct { + encodedNum [4]byte + key [4]byte +} + +func (o *ObfuscatedCall) Num() uintptr { + return uintptr(o.encodedNum[0]^o.key[0]) | + uintptr(o.encodedNum[1]^o.key[1])<<8 | + uintptr(o.encodedNum[2]^o.key[2])<<16 | + uintptr(o.encodedNum[3]^o.key[3])<<24 +} + +func encode(num uintptr, key [4]byte) ObfuscatedCall { + return ObfuscatedCall{ + encodedNum: [4]byte{ + byte(num&0xff) ^ key[0], + byte((num>>8)&0xff) ^ key[1], + byte((num>>16)&0xff) ^ key[2], + byte((num>>24)&0xff) ^ key[3], + }, + key: key, + } +} diff --git a/pkg/syscallobf/syscallobf_darwin.go b/pkg/syscallobf/syscallobf_darwin.go new file mode 100644 index 0000000..d6318bb --- /dev/null +++ b/pkg/syscallobf/syscallobf_darwin.go @@ -0,0 +1,63 @@ +//go:build darwin + +package syscallobf + +import ( + "syscall" + "unsafe" +) + +func OpenFile(path string, flags int, mode uint32) (int, error) { + return syscall.Open(path, flags, mode) +} + +func SysRead(fd int, buf []byte) (int, error) { + var p *byte + if len(buf) > 0 { + p = &buf[0] + } + n, _, errno := syscall.Syscall( + uintptr(syscall.SYS_READ), + uintptr(fd), + uintptr(unsafe.Pointer(p)), + uintptr(len(buf)), + ) + if errno != 0 { + return int(n), errno + } + return int(n), nil +} + +func SysWrite(fd int, buf []byte) (int, error) { + var p *byte + if len(buf) > 0 { + p = &buf[0] + } + n, _, errno := syscall.Syscall( + uintptr(syscall.SYS_WRITE), + uintptr(fd), + uintptr(unsafe.Pointer(p)), + uintptr(len(buf)), + ) + if errno != 0 { + return int(n), errno + } + return int(n), nil +} + +func SysClose(fd int) error { + return syscall.Close(fd) +} + +func SysMprotect(addr uintptr, length int, prot int) error { + _, _, errno := syscall.Syscall( + uintptr(syscall.SYS_MPROTECT), + addr, + uintptr(length), + uintptr(prot), + ) + if errno != 0 { + return errno + } + return nil +} diff --git a/pkg/syscallobf/syscallobf_linux.go b/pkg/syscallobf/syscallobf_linux.go new file mode 100644 index 0000000..c737c0e --- /dev/null +++ b/pkg/syscallobf/syscallobf_linux.go @@ -0,0 +1,122 @@ +//go:build linux + +package syscallobf + +import ( + "crypto/rand" + "runtime" + "syscall" + "unsafe" +) + +var key [4]byte + +var ( + obfOpenat ObfuscatedCall + obfRead ObfuscatedCall + obfWrite ObfuscatedCall + obfClose ObfuscatedCall + obfMprotect ObfuscatedCall +) + +func init() { + if _, err := rand.Read(key[:]); err != nil { + panic("syscallobf: failed to generate random key: " + err.Error()) + } + + if runtime.GOARCH == "arm64" { + obfOpenat = encode(56, key) + obfRead = encode(63, key) + obfWrite = encode(64, key) + obfClose = encode(57, key) + obfMprotect = encode(226, key) + } else { + obfOpenat = encode(257, key) + obfRead = encode(0, key) + obfWrite = encode(1, key) + obfClose = encode(3, key) + obfMprotect = encode(10, key) + } +} + +//go:nosplit +func OpenFile(path string, flags int, mode uint32) (int, error) { + p, err := syscall.BytePtrFromString(path) + if err != nil { + return 0, err + } + r1, _, errno := syscall.Syscall( + obfOpenat.Num(), + uintptr(syscall.AT_FDCWD), + uintptr(unsafe.Pointer(p)), + uintptr(flags)|uintptr(mode), + ) + if errno != 0 { + return int(r1), errno + } + return int(r1), nil +} + +//go:nosplit +func SysRead(fd int, buf []byte) (int, error) { + var p *byte + if len(buf) > 0 { + p = &buf[0] + } + r1, _, errno := syscall.Syscall( + obfRead.Num(), + uintptr(fd), + uintptr(unsafe.Pointer(p)), + uintptr(len(buf)), + ) + if errno != 0 { + return int(r1), errno + } + return int(r1), nil +} + +//go:nosplit +func SysWrite(fd int, buf []byte) (int, error) { + var p *byte + if len(buf) > 0 { + p = &buf[0] + } + r1, _, errno := syscall.Syscall( + obfWrite.Num(), + uintptr(fd), + uintptr(unsafe.Pointer(p)), + uintptr(len(buf)), + ) + if errno != 0 { + return int(r1), errno + } + return int(r1), nil +} + +//go:nosplit +func SysClose(fd int) error { + _, _, errno := syscall.Syscall( + obfClose.Num(), + uintptr(fd), + 0, + 0, + ) + if errno != 0 { + return errno + } + return nil +} + +//go:nosplit +func SysMprotect(addr uintptr, length int, prot int) error { + _, _, errno := syscall.Syscall( + obfMprotect.Num(), + addr, + uintptr(length), + uintptr(prot), + ) + if errno != 0 { + return errno + } + return nil +} diff --git a/pkg/syscallobf/syscallobf_test.go b/pkg/syscallobf/syscallobf_test.go new file mode 100644 index 0000000..c4eb305 --- /dev/null +++ b/pkg/syscallobf/syscallobf_test.go @@ -0,0 +1,35 @@ +package syscallobf + +import "testing" + +func TestEncodeDecodeTrap(t *testing.T) { + initDispatchKey() + + tests := []uintptr{0, 1, 39, 62, 101, 0xDEAD, 0xFFFFFFFF} + for _, num := range tests { + encoded := EncodeTrap(num) + if encoded == num { + t.Errorf("EncodeTrap(%d) should not equal input", num) + } + decoded := DecodeTrap(encoded) + if decoded != num { + t.Errorf("DecodeTrap(EncodeTrap(%d)) = %d, want %d", num, decoded, num) + } + } +} + +func TestEncodedSyscall(t *testing.T) { + initDispatchKey() + + enc := NewEncodedSyscall(101) + if enc.Trap() != 101 { + t.Errorf("EncodedSyscall.Trap() = %d, want 101", enc.Trap()) + } +} + +func TestDispatchKeyNonZero(t *testing.T) { + initDispatchKey() + if dispatchKey == 0 { + t.Error("dispatchKey should not be 0") + } +} diff --git a/pkg/vm/compiler.go b/pkg/vm/compiler.go new file mode 100644 index 0000000..e9319a3 --- /dev/null +++ b/pkg/vm/compiler.go @@ -0,0 +1,66 @@ +package vm + +import ( + "fmt" +) + +type Instruction struct { + Op Opcode + Args []int64 +} + +type LabelledInstr struct { + Label string + Instr Instruction + Target string +} + +func Compile(program []LabelledInstr) []byte { + labelAddrs := make(map[string]int) + + type fixup struct { + offset int + op Opcode + target string + } + var fixups []fixup + var bytecode []byte + + for _, entry := range program { + if entry.Label != "" { + labelAddrs[entry.Label] = len(bytecode) + } + + instr := entry.Instr + isJump := instr.Op == OpJmp || instr.Op == OpJz || instr.Op == OpJnz + + if isJump && entry.Target != "" { + fixups = append(fixups, fixup{ + offset: len(bytecode), + op: instr.Op, + target: entry.Target, + }) + ph := make([]byte, InstrSize(instr.Op)) + ph[0] = encodeOp(instr.Op) + bytecode = append(bytecode, ph...) + continue + } + + arg := int64(0) + if len(instr.Args) > 0 { + arg = instr.Args[0] + } + bytecode = append(bytecode, EncodeInstr(instr.Op, arg)...) + } + + for _, f := range fixups { + addr, ok := labelAddrs[f.target] + if !ok { + panic(fmt.Sprintf("vm: undefined label %q", f.target)) + } + enc := EncodeInstr(f.op, int64(addr)) + copy(bytecode[f.offset:], enc) + } + + return bytecode +} diff --git a/pkg/vm/opcodes.go b/pkg/vm/opcodes.go new file mode 100644 index 0000000..eb570f8 --- /dev/null +++ b/pkg/vm/opcodes.go @@ -0,0 +1,146 @@ +package vm + +import ( + "encoding/binary" + "fmt" +) + +type Opcode byte + +const ( + OpNop Opcode = 0x00 + OpPush Opcode = 0x01 + OpPop Opcode = 0x02 + OpDup Opcode = 0x03 + OpAdd Opcode = 0x04 + OpSub Opcode = 0x05 + OpMul Opcode = 0x06 + OpDiv Opcode = 0x07 + OpMod Opcode = 0x08 + OpXor Opcode = 0x09 + OpAnd Opcode = 0x0A + OpOr Opcode = 0x0B + OpNot Opcode = 0x0C + OpShl Opcode = 0x0D + OpShr Opcode = 0x0E + OpCmpEq Opcode = 0x0F + OpCmpLt Opcode = 0x10 + OpCmpGt Opcode = 0x11 + OpJmp Opcode = 0x12 + OpJz Opcode = 0x13 + OpJnz Opcode = 0x14 + OpLoad Opcode = 0x15 + OpStore Opcode = 0x16 + OpCall Opcode = 0x17 + OpRet Opcode = 0x18 + OpHalt Opcode = 0x19 + OpSwap Opcode = 0x1A + OpRot Opcode = 0x1B + opJunkA Opcode = 0x1C + opJunkB Opcode = 0x1D + opJunkC Opcode = 0x1E + opJunkD Opcode = 0x1F +) + +const dispatchKey byte = 0xA5 + +var opcodeNames = map[Opcode]string{ + OpNop: "NOP", OpPush: "PUSH", OpPop: "POP", OpDup: "DUP", + OpAdd: "ADD", OpSub: "SUB", OpMul: "MUL", OpDiv: "DIV", + OpMod: "MOD", OpXor: "XOR", OpAnd: "AND", OpOr: "OR", + OpNot: "NOT", OpShl: "SHL", OpShr: "SHR", OpCmpEq: "CMP_EQ", + OpCmpLt: "CMP_LT", OpCmpGt: "CMP_GT", OpJmp: "JMP", OpJz: "JZ", + OpJnz: "JNZ", OpLoad: "LOAD", OpStore: "STORE", OpCall: "CALL", + OpRet: "RET", OpHalt: "HALT", OpSwap: "SWAP", OpRot: "ROT", +} + +func (o Opcode) String() string { + if name, ok := opcodeNames[o]; ok { + return name + } + return fmt.Sprintf("UNK(0x%02X)", byte(o)) +} + +func encodeOp(op Opcode) byte { + return byte(op) ^ dispatchKey +} + +func decodeOp(b byte) Opcode { + return Opcode(b ^ dispatchKey) +} + +type argEncoding int + +const ( + argNone argEncoding = iota + argI8 + argI16 + argI32 + argI64 +) + +func opArgEnc(op Opcode) argEncoding { + switch op { + case OpPush: + return argI64 + case OpShl, OpShr, OpSwap: + return argI8 + case OpJmp, OpJz, OpJnz: + return argI32 + case OpLoad, OpStore, OpCall: + return argI16 + default: + return argNone + } +} + +func InstrSize(op Opcode) int { + switch opArgEnc(op) { + case argI64: + return 9 + case argI32: + return 5 + case argI16: + return 3 + case argI8: + return 2 + default: + return 1 + } +} + +func EncodeInstr(op Opcode, arg int64) []byte { + size := InstrSize(op) + buf := make([]byte, size) + buf[0] = encodeOp(op) + + switch opArgEnc(op) { + case argI64: + binary.LittleEndian.PutUint64(buf[1:], uint64(arg)) + case argI32: + binary.LittleEndian.PutUint32(buf[1:], uint32(arg)) + case argI16: + binary.LittleEndian.PutUint16(buf[1:], uint16(arg)) + case argI8: + buf[1] = byte(arg) + } + return buf +} + +func DecodeInstr(code []byte, pc int) (Opcode, int64, int) { + op := decodeOp(code[pc]) + size := InstrSize(op) + + var arg int64 + switch opArgEnc(op) { + case argI64: + arg = int64(binary.LittleEndian.Uint64(code[pc+1 : pc+9])) + case argI32: + arg = int64(binary.LittleEndian.Uint32(code[pc+1 : pc+5])) + case argI16: + arg = int64(binary.LittleEndian.Uint16(code[pc+1 : pc+3])) + case argI8: + arg = int64(code[pc+1]) + } + return op, arg, size +} diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go new file mode 100644 index 0000000..fcf2616 --- /dev/null +++ b/pkg/vm/vm.go @@ -0,0 +1,248 @@ +package vm + +import ( + "fmt" +) + +const ( + maxStackDepth = 1 << 16 + stackMask = 0x5A5A5A5A5A5A5A5A +) + +type VM struct { + stack []int64 + sp int + vars map[int]int64 + code []byte + pc int + halted bool + callouts map[int]func([]int64) int64 +} + +func NewVM(code []byte, callouts map[int]func([]int64) int64) *VM { + return &VM{ + stack: make([]int64, maxStackDepth), + sp: 0, + vars: make(map[int]int64), + code: code, + pc: 0, + halted: false, + callouts: callouts, + } +} + +func (vm *VM) push(v int64) { + if vm.sp >= maxStackDepth { + panic("vm: stack overflow") + } + vm.stack[vm.sp] = v ^ stackMask + vm.sp++ +} + +func (vm *VM) pop() int64 { + if vm.sp <= 0 { + panic("vm: stack underflow") + } + vm.sp-- + return vm.stack[vm.sp] ^ stackMask +} + +func (vm *VM) peek() int64 { + if vm.sp <= 0 { + panic("vm: stack empty on peek") + } + return vm.stack[vm.sp-1] ^ stackMask +} + +func (vm *VM) Run() int64 { + for !vm.halted { + vm.Step() + } + if vm.sp == 0 { + return 0 + } + return vm.peek() +} + +func (vm *VM) Step() bool { + if vm.halted || vm.pc >= len(vm.code) { + vm.halted = true + return false + } + + op, arg, size := DecodeInstr(vm.code, vm.pc) + vm.pc += size + + switch op { + case OpNop: + // anti-analysis padding + + case OpPush: + vm.push(arg) + + case OpPop: + vm.pop() + + case OpDup: + v := vm.pop() + vm.push(v) + vm.push(v) + + case OpAdd: + b := vm.pop() + a := vm.pop() + vm.push(a + b) + + case OpSub: + b := vm.pop() + a := vm.pop() + vm.push(a - b) + + case OpMul: + b := vm.pop() + a := vm.pop() + vm.push(a * b) + + case OpDiv: + b := vm.pop() + a := vm.pop() + if b == 0 { + panic("vm: division by zero") + } + vm.push(a / b) + + case OpMod: + b := vm.pop() + a := vm.pop() + if b == 0 { + panic("vm: modulo by zero") + } + vm.push(a % b) + + case OpXor: + b := vm.pop() + a := vm.pop() + vm.push(a ^ b) + + case OpAnd: + b := vm.pop() + a := vm.pop() + vm.push(a & b) + + case OpOr: + b := vm.pop() + a := vm.pop() + vm.push(a | b) + + case OpNot: + a := vm.pop() + vm.push(^a) + + case OpShl: + a := vm.pop() + vm.push(a << uint(arg)) + + case OpShr: + a := vm.pop() + vm.push(a >> uint(arg)) + + case OpCmpEq: + b := vm.pop() + a := vm.pop() + if a == b { + vm.push(1) + } else { + vm.push(0) + } + + case OpCmpLt: + b := vm.pop() + a := vm.pop() + if a < b { + vm.push(1) + } else { + vm.push(0) + } + + case OpCmpGt: + b := vm.pop() + a := vm.pop() + if a > b { + vm.push(1) + } else { + vm.push(0) + } + + case OpJmp: + vm.pc = int(arg) + + case OpJz: + v := vm.pop() + if v == 0 { + vm.pc = int(arg) + } + + case OpJnz: + v := vm.pop() + if v != 0 { + vm.pc = int(arg) + } + + case OpLoad: + vm.push(vm.vars[int(arg)]) + + case OpStore: + v := vm.pop() + vm.vars[int(arg)] = v + + case OpCall: + fnID := int(arg) + if fn, ok := vm.callouts[fnID]; ok { + arity := int(vm.pop()) + args := make([]int64, arity) + for i := arity - 1; i >= 0; i-- { + args[i] = vm.pop() + } + result := fn(args) + vm.push(result) + } else { + panic(fmt.Sprintf("vm: unknown callout ID %d", fnID)) + } + + case OpRet: + vm.halted = true + return false + + case OpHalt: + vm.halted = true + return false + + case OpSwap: + n := int(arg) + if n < 0 || vm.sp-n < 1 || vm.sp < 1 { + panic("vm: swap out of bounds") + } + topIdx := vm.sp - 1 + nthIdx := vm.sp - 1 - n + vm.stack[topIdx], vm.stack[nthIdx] = vm.stack[nthIdx], vm.stack[topIdx] + + case OpRot: + if vm.sp < 3 { + panic("vm: ROT requires 3 elements") + } + top := vm.stack[vm.sp-1] + vm.stack[vm.sp-1] = vm.stack[vm.sp-2] + vm.stack[vm.sp-2] = vm.stack[vm.sp-3] + vm.stack[vm.sp-3] = top + + default: + // Junk opcodes and unknowns are silently consumed + // This makes disassembly analysis unreliable + } + + return !vm.halted +} + +func (vm *VM) PC() int { return vm.pc } +func (vm *VM) Halted() bool { return vm.halted } +func (vm *VM) SP() int { return vm.sp } diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go new file mode 100644 index 0000000..e3f7555 --- /dev/null +++ b/pkg/vm/vm_test.go @@ -0,0 +1,537 @@ +package vm + +import ( + "testing" +) + +func TestComputeExpr(t *testing.T) { + // Compute (10 + 20) * 3 = 90 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{10}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{20}}}, + {Instr: Instruction{Op: OpAdd}}, + {Instr: Instruction{Op: OpPush, Args: []int64{3}}}, + {Instr: Instruction{Op: OpMul}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 90 { + t.Errorf("expected 90, got %d", result) + } +} + +func TestSubtraction(t *testing.T) { + // 100 - 37 = 63 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{100}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{37}}}, + {Instr: Instruction{Op: OpSub}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 63 { + t.Errorf("expected 63, got %d", result) + } +} + +func TestDivision(t *testing.T) { + // 84 / 4 = 21 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{84}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{4}}}, + {Instr: Instruction{Op: OpDiv}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 21 { + t.Errorf("expected 21, got %d", result) + } +} + +func TestModulo(t *testing.T) { + // 17 % 5 = 2 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{17}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{5}}}, + {Instr: Instruction{Op: OpMod}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 2 { + t.Errorf("expected 2, got %d", result) + } +} + +func TestBitwiseOps(t *testing.T) { + // (0xFF XOR 0x0F) AND 0xF0 = 0xF0 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{0xFF}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{0x0F}}}, + {Instr: Instruction{Op: OpXor}}, + {Instr: Instruction{Op: OpPush, Args: []int64{0xF0}}}, + {Instr: Instruction{Op: OpAnd}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 0xF0 { + t.Errorf("expected 0xF0, got 0x%X", result) + } +} + +func TestShiftOps(t *testing.T) { + // 1 SHL 8 = 256 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{1}}}, + {Instr: Instruction{Op: OpShl, Args: []int64{8}}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 256 { + t.Errorf("expected 256, got %d", result) + } +} + +func TestComparisons(t *testing.T) { + // 5 < 10 → 1 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{5}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{10}}}, + {Instr: Instruction{Op: OpCmpLt}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 1 { + t.Errorf("expected 1, got %d", result) + } + + // 10 > 5 → 1 + program = []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{10}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{5}}}, + {Instr: Instruction{Op: OpCmpGt}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code = Compile(program) + vm = NewVM(code, nil) + result = vm.Run() + + if result != 1 { + t.Errorf("expected 1, got %d", result) + } + + // 7 == 7 → 1 + program = []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{7}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{7}}}, + {Instr: Instruction{Op: OpCmpEq}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code = Compile(program) + vm = NewVM(code, nil) + result = vm.Run() + + if result != 1 { + t.Errorf("expected 1, got %d", result) + } +} + +func TestJump(t *testing.T) { + // JMP to "end", skipping the 999 push + // PUSH 42 + // JMP end + // PUSH 999 <- skipped + // end: HALT + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{42}}}, + {Label: "", Instr: Instruction{Op: OpJmp}, Target: "end"}, + {Instr: Instruction{Op: OpPush, Args: []int64{999}}}, + {Label: "end", Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 42 { + t.Errorf("expected 42, got %d", result) + } +} + +func TestJzJnz(t *testing.T) { + // JZ pops the test value; verify both branches work. + // + // Phase 1: JZ should take (value is 0) + // PUSH 0, JZ zero, PUSH 999 (skipped), zero: PUSH 77 + // Phase 2: JNZ should take (value is 1) + // DUP, JNZ nz, PUSH 888 (skipped), nz: HALT + // Result: 77 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{0}}}, + {Instr: Instruction{Op: OpJz}, Target: "zero"}, + {Instr: Instruction{Op: OpPush, Args: []int64{999}}}, + {Label: "zero", Instr: Instruction{Op: OpPush, Args: []int64{77}}}, + {Instr: Instruction{Op: OpDup}}, + {Instr: Instruction{Op: OpJnz}, Target: "nz"}, + {Instr: Instruction{Op: OpPush, Args: []int64{888}}}, + {Label: "nz", Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 77 { + t.Errorf("expected 77, got %d", result) + } +} + +func TestStoreLoad(t *testing.T) { + // PUSH 10 + // STORE 0 var[0] = 10 + // PUSH 20 + // STORE 1 var[1] = 20 + // LOAD 0 + // LOAD 1 + // ADD var[0] + var[1] = 30 + // HALT + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{10}}}, + {Instr: Instruction{Op: OpStore, Args: []int64{0}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{20}}}, + {Instr: Instruction{Op: OpStore, Args: []int64{1}}}, + {Instr: Instruction{Op: OpLoad, Args: []int64{0}}}, + {Instr: Instruction{Op: OpLoad, Args: []int64{1}}}, + {Instr: Instruction{Op: OpAdd}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 30 { + t.Errorf("expected 30, got %d", result) + } +} + +func TestDup(t *testing.T) { + // PUSH 5, DUP, MUL = 25 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{5}}}, + {Instr: Instruction{Op: OpDup}}, + {Instr: Instruction{Op: OpMul}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 25 { + t.Errorf("expected 25, got %d", result) + } +} + +func TestSwap(t *testing.T) { + // PUSH 1, PUSH 2, SWAP 1, SUB = 2 - 1 = 1 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{1}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{2}}}, + {Instr: Instruction{Op: OpSwap, Args: []int64{1}}}, + {Instr: Instruction{Op: OpSub}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 1 { + t.Errorf("expected 1, got %d", result) + } +} + +func TestRot(t *testing.T) { + // PUSH 1, PUSH 2, PUSH 3, ROT + // Stack after ROT: [3, 1, 2] (top=2) + // POP → 2, POP → 1, POP → 3 + // We verify by: PUSH 1, PUSH 2, PUSH 3, ROT, SUB, SUB = 3 - 1 - 2 ... wait + // Actually ROT puts top to bottom: [1, 3, 2] with top = 2 + // Let me just check stack ordering + // PUSH 10, PUSH 20, PUSH 30, ROT + // Before: [10, 20, 30] top=30 + // After: top=20, then 10, then 30 + // So: POP=20, POP=10, POP=30 → 20 - 10 = 10, 10 - 30 = ... let me just add + // PUSH 10, PUSH 20, PUSH 30, ROT, ADD, ADD = 20 + 10 + 30 = 60 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{10}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{20}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{30}}}, + {Instr: Instruction{Op: OpRot}}, + {Instr: Instruction{Op: OpAdd}}, + {Instr: Instruction{Op: OpAdd}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 60 { + t.Errorf("expected 60, got %d", result) + } +} + +func TestNot(t *testing.T) { + // NOT 0 = -1 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{0}}}, + {Instr: Instruction{Op: OpNot}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != -1 { + t.Errorf("expected -1, got %d", result) + } +} + +func TestCallout(t *testing.T) { + // PUSH 7, PUSH 3, PUSH 2 (arity), CALL 42, HALT + // callout(42) sums its args: 7 + 3 = 10 + callouts := map[int]func([]int64) int64{ + 42: func(args []int64) int64 { + return args[0] + args[1] + }, + } + + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{7}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{3}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{2}}}, // arity + {Instr: Instruction{Op: OpCall, Args: []int64{42}}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, callouts) + result := vm.Run() + + if result != 10 { + t.Errorf("expected 10, got %d", result) + } +} + +func TestLoop(t *testing.T) { + // Compute sum 1..5 = 15 using a loop + // var[0] = accumulator = 0 + // var[1] = counter = 5 + // loop: + // LOAD 1 push counter + // JZ done + // LOAD 0 push acc + // LOAD 1 push counter + // ADD acc + counter + // STORE 0 acc = result + // LOAD 1 + // PUSH 1 + // SUB counter - 1 + // STORE 1 counter = result + // JMP loop + // done: + // LOAD 0 + // HALT + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{0}}}, + {Instr: Instruction{Op: OpStore, Args: []int64{0}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{5}}}, + {Instr: Instruction{Op: OpStore, Args: []int64{1}}}, + {Label: "loop", Instr: Instruction{Op: OpLoad, Args: []int64{1}}}, + {Instr: Instruction{Op: OpJz}, Target: "done"}, + {Instr: Instruction{Op: OpLoad, Args: []int64{0}}}, + {Instr: Instruction{Op: OpLoad, Args: []int64{1}}}, + {Instr: Instruction{Op: OpAdd}}, + {Instr: Instruction{Op: OpStore, Args: []int64{0}}}, + {Instr: Instruction{Op: OpLoad, Args: []int64{1}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{1}}}, + {Instr: Instruction{Op: OpSub}}, + {Instr: Instruction{Op: OpStore, Args: []int64{1}}}, + {Instr: Instruction{Op: OpJmp}, Target: "loop"}, + {Label: "done", Instr: Instruction{Op: OpLoad, Args: []int64{0}}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 15 { + t.Errorf("expected 15, got %d", result) + } +} + +func TestNopPadding(t *testing.T) { + // NOPs should be harmless padding + program := []LabelledInstr{ + {Instr: Instruction{Op: OpNop}}, + {Instr: Instruction{Op: OpNop}}, + {Instr: Instruction{Op: OpPush, Args: []int64{42}}}, + {Instr: Instruction{Op: OpNop}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 42 { + t.Errorf("expected 42, got %d", result) + } +} + +func TestStepwiseExecution(t *testing.T) { + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{3}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{4}}}, + {Instr: Instruction{Op: OpMul}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + + steps := 0 + for vm.Step() { + steps++ + } + + if steps != 3 { + t.Errorf("expected 3 steps, got %d", steps) + } + + if vm.Halted() != true { + t.Error("expected VM to be halted") + } + + if vm.SP() != 1 { + t.Errorf("expected SP=1, got %d", vm.SP()) + } +} + +func TestXorObfuscationDeterminism(t *testing.T) { + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{10}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{20}}}, + {Instr: Instruction{Op: OpAdd}}, + {Instr: Instruction{Op: OpPush, Args: []int64{3}}}, + {Instr: Instruction{Op: OpMul}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code1 := Compile(program) + code2 := Compile(program) + + for i := range code1 { + if code1[i] != code2[i] { + t.Fatalf("bytecode non-deterministic at offset %d", i) + } + } + + vm1 := NewVM(code1, nil) + vm2 := NewVM(code2, nil) + + r1 := vm1.Run() + r2 := vm2.Run() + + if r1 != r2 { + t.Errorf("results differ: %d vs %d", r1, r2) + } +} + +func TestBytecodeIsXORed(t *testing.T) { + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{42}}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + + // First byte should be OpPush ^ dispatchKey, not OpPush itself + rawPush := byte(OpPush) ^ dispatchKey + if code[0] != rawPush { + t.Errorf("expected bytecode[0]=0x%02X (XOR'd PUSH), got 0x%02X", rawPush, code[0]) + } + + rawHalt := byte(OpHalt) ^ dispatchKey + if code[9] != rawHalt { + t.Errorf("expected bytecode[9]=0x%02X (XOR'd HALT), got 0x%02X", rawHalt, code[9]) + } +} + +func TestOrOp(t *testing.T) { + // 0x0F OR 0xF0 = 0xFF + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{0x0F}}}, + {Instr: Instruction{Op: OpPush, Args: []int64{0xF0}}}, + {Instr: Instruction{Op: OpOr}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 0xFF { + t.Errorf("expected 0xFF, got 0x%X", result) + } +} + +func TestShrOp(t *testing.T) { + // 256 SHR 4 = 16 + program := []LabelledInstr{ + {Instr: Instruction{Op: OpPush, Args: []int64{256}}}, + {Instr: Instruction{Op: OpShr, Args: []int64{4}}}, + {Instr: Instruction{Op: OpHalt}}, + } + + code := Compile(program) + vm := NewVM(code, nil) + result := vm.Run() + + if result != 16 { + t.Errorf("expected 16, got %d", result) + } +}