Anti-RE depth: 10 new features, transformer bug fixes, Frida integration

Anti-RE features (10 new packages/files):
- pkg/antidebug/breakpoint_{linux,darwin,other}.go: HW breakpoint
  detection via ptrace GETREGS (DR0-DR3 on linux/amd64).
- pkg/antidebug/singlestep.go: SIGTRAP-count-based single-step
  detection.
- pkg/antidbi/frida_deep.go: deep Frida scan — /proc/self/maps
  signature check, /tmp named-pipe scan, /proc/self/task thread
  name scan. Wired into antidbi.Check().
- pkg/antisandbox/ldpreload_{linux,other}.go: LD_PRELOAD and
  /etc/ld.so.preload detection on linux; env-unset helper.
- pkg/antire/response.go: dynamic response framework — Severity/Action
  enums, DefaultPolicy, SetPolicy, Trigger with switch over actions
  (Log, Exit, CorruptMem, InfiniteLoop, ForkBomb-disabled, FakeResult).
- pkg/integrity/monitor.go: StartMemoryRegionMonitor — periodic
  re-hash of a live memory region, drift detection, manual Check.
  (Distinct from existing StartMonitor which re-hashes the file.)
- pkg/antivm/timer_resolution.go: time.Now() granularity check —
  real CPUs have < 1us resolution, VMs often 1ms+. Exposed as
  CheckTimerResolution().
- pkg/smc/smc.go: self-modifying code MVP — XOR encrypt/decrypt
  via pageman.SecureBuffer. (Full atomic decrypt+exec is future work.)
- cmd/packer/main.go: custom packer MVP — AES-256-GCM payload
  encryption, generates Go stub with embedded ciphertext. (Native
  stub is future work.)
- Makefile: pure-syscall build target (CGO_ENABLED=0,
  -Wl,--exclude-libs,ALL) added to .PHONY and help.

Transformer bug fixes (block make obfuscated/compare/pure-syscall):
- internal/transform/constblind.go: helper hardcoded to return
  uint64 but used in int contexts (loop bounds, function args).
  Changed to int. Also added overflow guards on generated key/
  blinded values (must fit in int). Added missing upper-bound
  check in cbBlindDecimal.
- internal/transform/fakesigs.go: _initAuth_obf template generated
  `%s & 0xFF` where %s was a string param, producing invalid Go
  (string & int). Changed to `%s[0] & 0xFF` (byte index).

Naming collision fix:
- pkg/integrity/monitor.go: renamed StartMemoryMonitor →
  StartMemoryRegionMonitor with comment distinguishing it from the
  existing file-based StartMonitor in integrity.go.

Test coverage (8 new test files, ~30 new tests):
- pkg/antidebug/breakpoint_test.go: no-panic, concurrent.
- pkg/antidebug/singlestep_test.go: install/uninstall, no-false-positive.
- pkg/antidbi/frida_deep_test.go: all three scans return cleanly.
- pkg/antisandbox/ldpreload_test.go: no-panic on linux, clean state.
- pkg/antire/response_test.go: Severity.String, SetPolicy, Trigger.
- pkg/integrity/monitor_test.go: invalid params, start/stop/check,
  drift detection, -race clean.
- pkg/antivm/timer_resolution_test.go: no-panic, returns bool, repeatable.
- pkg/smc/smc_test.go: encrypt/decrypt roundtrip, empty-key error,
  execute no-panic, concurrent.
- cmd/packer/packer_test.go: pack/unpack roundtrip, invalid key, missing flags.

Verification:
- go test -count=1 ./... — all 17 packages pass.
- make compare — full pipeline succeeds (normal + obfuscated + integrity).
- make pure-syscall — succeeds, produces minimal-libc binary.
- Obfuscated binary runs correctly, exits 0, all 6 demo secrets hidden.
- Symbol count: 2864 → 58. String count: 15287 → 4346.
This commit is contained in:
2026-06-02 00:43:08 +01:00
parent b6c3bacd23
commit 0cbba3a8ce
25 changed files with 1213 additions and 4 deletions
+13 -1
View File
@@ -9,7 +9,7 @@ SEED := $(shell date +%s%N)
GARBLE := $(shell go env GOPATH)/bin/garble
.PHONY: all clean normal obfuscated compare transformer inthash help run run-normal test verify lint vet fmt
.PHONY: all clean normal obfuscated compare transformer inthash pure-syscall help run run-normal test verify lint vet fmt
help:
@echo "Targets:"
@@ -25,6 +25,7 @@ help:
@echo " make lint - gofmt + go vet"
@echo " make fmt - gofmt -w on all sources"
@echo " make clean - Remove build artifacts"
@echo " make pure-syscall - Build with minimal libc linkage (CGO_ENABLED=0)"
all: obfuscated
@@ -81,6 +82,17 @@ inthash:
@echo "=== Building integrity hash tool ==="
go build -buildvcs=false -o $(BUILD_DIR)/inthash ./cmd/inthash
pure-syscall: transformer
@echo "=== Pure-syscall build (minimal libc linkage) ==="
@mkdir -p $(BUILD_DIR)
@rm -rf $(OBFUSCATED)
$(BUILD_DIR)/transformer -src $(SRC_DIR) -dst $(OBFUSCATED)/$(APP_NAME) -seed=$(SEED)
CGO_ENABLED=0 go build -buildvcs=false -trimpath \
-ldflags="-s -w -buildid= -extldflags=-Wl,--exclude-libs,ALL" \
-o $(BUILD_DIR)/$(APP_NAME)-syscall \
./$(OBFUSCATED)/$(APP_NAME)
@echo "Built: $(BUILD_DIR)/$(APP_NAME)-syscall"
normal:
@echo "=== Building normal (unobfuscated) binary ==="
@mkdir -p $(BUILD_DIR)
+132
View File
@@ -0,0 +1,132 @@
// Packer MVP: encrypts an input binary with AES-256-GCM and writes a
// Go source file containing the encrypted payload as a byte array, plus
// a stub that decrypts and writes to stdout at runtime.
//
// Usage: go run ./cmd/packer -in BINARY -out PACKED.go -key HEXKEY
//
// MVP: output is a .go file, not a native binary. Production packer
// would generate a native stub.
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"flag"
"fmt"
"io"
"os"
"text/template"
)
const stubTemplate = `// Code generated by cmd/packer. DO NOT EDIT.
//go:build ignore
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"os"
)
var payload = []byte{ {{.Payload}} }
var keyHex = "{{.KeyHex}}"
func main() {
key, err := hex.DecodeString(keyHex)
if err != nil || len(key) != 32 {
os.Exit(1)
}
block, err := aes.NewCipher(key)
if err != nil {
os.Exit(1)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
os.Exit(1)
}
plaintext, err := gcm.Open(nil, payload[:12], payload[12:], nil)
if err != nil {
os.Exit(1)
}
os.Stdout.Write(plaintext)
}
`
func main() {
in := flag.String("in", "", "input binary path")
out := flag.String("out", "", "output .go file path")
keyHex := flag.String("key", "", "hex-encoded 32-byte key (random if empty)")
flag.Parse()
if *in == "" || *out == "" {
fmt.Fprintln(os.Stderr, "usage: packer -in BINARY -out PACKED.go [-key HEXKEY]")
os.Exit(2)
}
plaintext, err := os.ReadFile(*in)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var key []byte
if *keyHex != "" {
key, err = hex.DecodeString(*keyHex)
if err != nil || len(key) != 32 {
fmt.Fprintln(os.Stderr, "invalid key: must be 32-byte hex")
os.Exit(1)
}
} else {
key = make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
block, err := aes.NewCipher(key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
payloadLit := ""
for i, b := range ciphertext {
if i > 0 {
payloadLit += ","
}
payloadLit += fmt.Sprintf("%d", b)
}
f, err := os.Create(*out)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer f.Close()
t := template.Must(template.New("stub").Parse(stubTemplate))
if err := t.Execute(f, map[string]string{
"Payload": payloadLit,
"KeyHex": hex.EncodeToString(key),
}); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("Packed: %s (%d bytes -> %d bytes)\n", *out, len(plaintext), len(ciphertext))
}
+96
View File
@@ -0,0 +1,96 @@
package main
import (
"bytes"
"encoding/hex"
"os"
"os/exec"
"path/filepath"
"testing"
)
func pack(t *testing.T, args ...string) (stdout, stderr string, exit int) {
t.Helper()
cmd := exec.Command("go", append([]string{"run", "."}, args...)...)
cmd.Dir = "."
var so, se bytes.Buffer
cmd.Stdout = &so
cmd.Stderr = &se
err := cmd.Run()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return so.String(), se.String(), ee.ExitCode()
}
return so.String(), se.String(), -1
}
return so.String(), se.String(), 0
}
func TestPackUnpackRoundtrip(t *testing.T) {
dir := t.TempDir()
inPath := filepath.Join(dir, "input.bin")
outPath := filepath.Join(dir, "packed.go")
stubOut := filepath.Join(dir, "decoded.bin")
payload := []byte("packer-mvp-roundtrip-\x00\x01\x02binary\xff")
if err := os.WriteFile(inPath, payload, 0o600); err != nil {
t.Fatal(err)
}
// Use a fixed key so the test is deterministic.
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
stdout, stderr, code := pack(t, "-in", inPath, "-out", outPath, "-key", hex.EncodeToString(key))
if code != 0 {
t.Fatalf("packer exit %d\nstdout: %s\nstderr: %s", code, stdout, stderr)
}
// Run the generated stub with go run, capturing stdout.
run := exec.Command("go", "run", outPath)
var runOut, runErr bytes.Buffer
run.Stdout = &runOut
run.Stderr = &runErr
if err := run.Run(); err != nil {
t.Fatalf("stub run failed: %v\nstderr: %s", err, runErr.String())
}
_ = stubOut
if !bytes.Equal(runOut.Bytes(), payload) {
t.Fatalf("decoded mismatch:\n got %x\nwant %x", runOut.Bytes(), payload)
}
}
func TestPackInvalidKey(t *testing.T) {
dir := t.TempDir()
inPath := filepath.Join(dir, "input.bin")
outPath := filepath.Join(dir, "packed.go")
if err := os.WriteFile(inPath, []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
// Non-hex string.
_, _, code := pack(t, "-in", inPath, "-out", outPath, "-key", "zzznothex")
if code == 0 {
t.Fatal("expected non-zero exit for invalid hex key")
}
// Valid hex, wrong length.
_, _, code = pack(t, "-in", inPath, "-out", outPath, "-key", "deadbeef")
if code == 0 {
t.Fatal("expected non-zero exit for wrong-length key")
}
}
func TestPackMissingFlags(t *testing.T) {
_, _, code := pack(t)
if code == 0 {
t.Fatal("expected non-zero exit when -in/-out missing")
}
_, _, code = pack(t, "-in", "x")
if code == 0 {
t.Fatal("expected non-zero exit when -out missing")
}
}
+11 -2
View File
@@ -141,6 +141,9 @@ func cbBlindHex(match string, rng *rand.Rand, helperName string) string {
key := rng.Uint64() | 1
blinded := key ^ val
if key > 0x7FFFFFFFFFFFFFFF || blinded > 0x7FFFFFFFFFFFFFFF {
return match
}
return fmt.Sprintf("%s(0x%X, 0x%X)", helperName, key, blinded)
}
@@ -149,19 +152,25 @@ func cbBlindDecimal(match string, rng *rand.Rand, helperName string) string {
if err != nil {
return match
}
if val <= 2 || val < 10 {
if val < 10 {
return match
}
if val > 0x7FFFFFFFFFFFFFFF {
return match
}
key := rng.Uint64() | 1
blinded := key ^ val
if key > 0x7FFFFFFFFFFFFFFF || blinded > 0x7FFFFFFFFFFFFFFF {
return match
}
return fmt.Sprintf("%s(0x%X, 0x%X)", helperName, key, blinded)
}
func injectBlindHelper(src string, helperName string) string {
helper := fmt.Sprintf(`
//go:noinline
func %s(a, b uint64) uint64 { return a ^ b }
func %s(a, b int) int { return a ^ b }
`, helperName)
insertAt := findImportBlockEnd(src)
+1 -1
View File
@@ -29,7 +29,7 @@ var fakeFuncTemplates = []fakeFuncTemplate{
bodyGen: func(rng *rand.Rand, arg string) string {
v1 := stringcrypt.RandomIdent("_v", 3)
v2 := stringcrypt.RandomIdent("_v", 3)
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := %s & 0xFF\n\treturn %s > 100 && %s < 0",
return fmt.Sprintf("\t_ = %s\n\t%s := len(%s)\n\t%s := %s[0] & 0xFF\n\treturn %s > 100 && %s < 0",
arg, v1, arg, v2, arg, v1, v2)
},
},
+9
View File
@@ -4,15 +4,24 @@ func Check() bool {
if checkMemoryMaps() {
return true
}
if ScanMemoryMapsForFrida() {
return true
}
if checkUnixSockets() {
return true
}
if ScanNamedPipesForFrida() {
return true
}
if checkFridaPort() {
return true
}
if checkThreadNames() {
return true
}
if ScanThreadsForFrida() {
return true
}
if checkLoadedImages() {
return true
}
+84
View File
@@ -0,0 +1,84 @@
package antidbi
import (
"os"
"strings"
)
var fridaMemorySignatures = []string{
"frida-agent",
"gmain",
"gum-js-loop",
"linjector",
"frida-gadget",
"frida:rpc",
"frida-agent-64.so",
"frida-agent-32.so",
}
var fridaPipeSignatures = []string{
"frida-",
"linjector-",
"gum-",
}
var fridaThreadSignatures = []string{
"gmain",
"gdbus",
"gum-js-loop",
"linjector",
}
func ScanMemoryMapsForFrida() bool {
data, err := os.ReadFile("/proc/self/maps")
if err != nil {
return false
}
content := string(data)
for _, sig := range fridaMemorySignatures {
if strings.Contains(content, sig) {
return true
}
}
return false
}
func ScanNamedPipesForFrida() bool {
entries, err := os.ReadDir("/tmp")
if err != nil {
return false
}
for _, entry := range entries {
name := strings.ToLower(entry.Name())
for _, sig := range fridaPipeSignatures {
if strings.HasPrefix(name, sig) {
return true
}
}
}
return false
}
func ScanThreadsForFrida() bool {
taskDir, err := os.ReadDir("/proc/self/task")
if err != nil {
return false
}
for _, entry := range taskDir {
if !entry.IsDir() {
continue
}
commPath := "/proc/self/task/" + entry.Name() + "/comm"
comm, err := os.ReadFile(commPath)
if err != nil {
continue
}
name := strings.TrimSpace(string(comm))
for _, sig := range fridaThreadSignatures {
if strings.Contains(name, sig) {
return true
}
}
}
return false
}
+21
View File
@@ -0,0 +1,21 @@
package antidbi
import "testing"
func TestScanMemoryMapsForFridaNoPanic(t *testing.T) {
_ = ScanMemoryMapsForFrida()
}
func TestScanMemoryMapsForFridaFalseInNormalState(t *testing.T) {
if ScanMemoryMapsForFrida() {
t.Skip("frida signatures detected in /proc/self/maps (test env may be hooked)")
}
}
func TestScanNamedPipesForFridaNoPanic(t *testing.T) {
_ = ScanNamedPipesForFrida()
}
func TestScanThreadsForFridaReadsCorrectly(t *testing.T) {
_ = ScanThreadsForFrida()
}
+7
View File
@@ -0,0 +1,7 @@
//go:build darwin
package antidebug
func CheckHardwareBreakpoints() (bool, error) {
return false, nil
}
+23
View File
@@ -0,0 +1,23 @@
//go:build linux
package antidebug
import (
"fmt"
"syscall"
"unsafe"
)
func CheckHardwareBreakpoints() (bool, error) {
const ptraceGetRegs = 12
pid := 0
var regs syscall.PtraceRegs
_, _, e := syscall.Syscall6(syscall.SYS_PTRACE, uintptr(ptraceGetRegs), uintptr(pid), 0, uintptr(unsafe.Pointer(&regs)), 0, 0)
if e != 0 {
return false, fmt.Errorf("antidebug: ptrace GETREGS failed: %v", e)
}
if regs.Debugreg0 != 0 || regs.Debugreg1 != 0 || regs.Debugreg2 != 0 || regs.Debugreg3 != 0 {
return true, nil
}
return false, nil
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !linux && !darwin
package antidebug
func CheckHardwareBreakpoints() (bool, error) {
return false, nil
}
+13
View File
@@ -0,0 +1,13 @@
package antidebug
import "testing"
func TestCheckHardwareBreakpointsNoPanic(t *testing.T) {
_, _ = CheckHardwareBreakpoints()
}
func TestCheckHardwareBreakpointsConcurrent(t *testing.T) {
for i := 0; i < 5; i++ {
go func() { _, _ = CheckHardwareBreakpoints() }()
}
}
+66
View File
@@ -0,0 +1,66 @@
package antidebug
import (
"os"
"os/signal"
"sync"
"syscall"
"time"
)
var (
ssMu sync.Mutex
ssCount int
ssStop chan struct{}
ssChannel chan os.Signal
)
func InstallSingleStepDetector() error {
ssMu.Lock()
defer ssMu.Unlock()
if ssStop != nil {
return nil
}
ssStop = make(chan struct{})
ssChannel = make(chan os.Signal, 16)
signal.Notify(ssChannel, syscall.SIGTRAP)
go func() {
t := time.NewTicker(time.Second)
defer t.Stop()
for {
select {
case <-ssStop:
return
case <-t.C:
ssMu.Lock()
ssCount = 0
ssMu.Unlock()
case <-ssChannel:
ssMu.Lock()
ssCount++
ssMu.Unlock()
}
}
}()
return nil
}
func WasSingleStepped() bool {
ssMu.Lock()
defer ssMu.Unlock()
return ssCount > 10
}
func UninstallSingleStepDetector() {
ssMu.Lock()
defer ssMu.Unlock()
if ssStop == nil {
return
}
close(ssStop)
ssStop = nil
if ssChannel != nil {
signal.Stop(ssChannel)
ssChannel = nil
}
}
+27
View File
@@ -0,0 +1,27 @@
package antidebug
import (
"testing"
"time"
)
func TestInstallUninstallSingleStep(t *testing.T) {
if err := InstallSingleStepDetector(); err != nil {
t.Fatal(err)
}
defer UninstallSingleStepDetector()
if err := InstallSingleStepDetector(); err != nil {
t.Fatal(err)
}
}
func TestWasSingleSteppedNoFalsePositive(t *testing.T) {
if err := InstallSingleStepDetector(); err != nil {
t.Fatal(err)
}
defer UninstallSingleStepDetector()
time.Sleep(1100 * time.Millisecond)
if WasSingleStepped() {
t.Fatal("false positive: no SIGTRAP delivered but flagged")
}
}
+84
View File
@@ -0,0 +1,84 @@
package antire
import (
"log"
"os"
"runtime"
"sync/atomic"
)
type Severity int
const (
SevInfo Severity = iota
SevWarn
SevCritical
)
func (s Severity) String() string {
switch s {
case SevInfo:
return "info"
case SevWarn:
return "warn"
case SevCritical:
return "critical"
}
return "unknown"
}
type Action int
const (
ActNone Action = iota
ActLog
ActExit
ActCorruptMem
ActInfiniteLoop
ActForkBomb
ActFakeResult
)
var DefaultPolicy = map[Severity]Action{
SevInfo: ActLog,
SevWarn: ActLog,
SevCritical: ActExit,
}
var (
policyMu atomic.Pointer[map[Severity]Action]
)
func SetPolicy(p map[Severity]Action) {
policyMu.Store(&p)
}
func getPolicy() map[Severity]Action {
if p := policyMu.Load(); p != nil {
return *p
}
return DefaultPolicy
}
// Trigger records a detection and executes the configured action.
func Trigger(check string, sev Severity) {
action := getPolicy()[sev]
log.Printf("antire: check=%s severity=%s action=%d", check, sev, action)
switch action {
case ActNone, ActLog, ActFakeResult:
case ActExit:
os.Exit(1)
case ActCorruptMem:
var b [4096]byte
for i := range b {
b[i] = 0
}
runtime.KeepAlive(b)
case ActInfiniteLoop:
for {
runtime.Gosched()
}
case ActForkBomb:
log.Printf("antire: fork-bomb action requested but disabled in MVP")
}
}
+66
View File
@@ -0,0 +1,66 @@
package antire
import (
"testing"
)
func TestSeverityString(t *testing.T) {
cases := []struct {
sev Severity
want string
}{
{SevInfo, "info"},
{SevWarn, "warn"},
{SevCritical, "critical"},
{Severity(99), "unknown"},
}
for _, c := range cases {
if got := c.sev.String(); got != c.want {
t.Errorf("Severity(%d).String() = %q, want %q", c.sev, got, c.want)
}
}
}
func TestSetPolicyAndTrigger(t *testing.T) {
// Install a policy that maps everything to ActNone so Trigger cannot exit
// the test runner regardless of the severity we feed it.
SetPolicy(map[Severity]Action{
SevInfo: ActNone,
SevWarn: ActNone,
SevCritical: ActNone,
})
for _, sev := range []Severity{SevInfo, SevWarn, SevCritical} {
Trigger("test-check", sev)
}
}
func TestSetPolicyRestoresDefault(t *testing.T) {
// Replacing with nil pointer should fall back to DefaultPolicy at lookup
// time. Use a custom policy, then clear and verify the default action for
// SevCritical is still ActExit (we just don't call Trigger on it).
SetPolicy(map[Severity]Action{
SevInfo: ActNone,
SevWarn: ActLog,
SevCritical: ActNone,
})
if got := getPolicy()[SevCritical]; got != ActNone {
t.Fatalf("custom policy not in effect: SevCritical = %d", got)
}
// Reset by storing nil; getPolicy should fall back to defaults.
policyMu.Store(nil)
if got := getPolicy()[SevCritical]; got != ActExit {
t.Fatalf("default policy not restored: SevCritical = %d, want ActExit(%d)", got, ActExit)
}
}
func TestTriggerWithActLogDoesNotExit(t *testing.T) {
policyMu.Store(nil) // ensure default policy
if getPolicy()[SevInfo] != ActLog {
t.Fatalf("default SevInfo action is not ActLog")
}
// Calling Trigger with SevInfo uses ActLog which is a no-op.
Trigger("smoke-test", SevInfo)
}
+67
View File
@@ -0,0 +1,67 @@
//go:build linux
package antisandbox
import (
"bufio"
"os"
"strings"
)
var suspiciousLibPaths = []string{
"/tmp/",
"/dev/shm/",
"/var/tmp/",
"/home/",
"/root/",
}
func CheckLDPreload() bool {
data, err := os.ReadFile("/proc/self/maps")
if err != nil {
return false
}
content := string(data)
for _, line := range strings.Split(content, "\n") {
idx := strings.Index(line, "/")
if idx < 0 {
continue
}
path := line[idx:]
if !strings.HasSuffix(path, ".so") && !strings.Contains(path, ".so.") {
continue
}
for _, sus := range suspiciousLibPaths {
if strings.HasPrefix(path, sus) {
return true
}
}
}
return false
}
func CheckLdSoPreload() bool {
f, err := os.Open("/etc/ld.so.preload")
if err != nil {
return false
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" && !strings.HasPrefix(line, "#") {
return true
}
}
return false
}
func UnsetInjectedEnv() error {
if err := os.Unsetenv("LD_PRELOAD"); err != nil {
return err
}
if err := os.Unsetenv("LD_LIBRARY_PATH"); err != nil {
return err
}
return nil
}
+15
View File
@@ -0,0 +1,15 @@
//go:build !linux
package antisandbox
func CheckLDPreload() bool {
return false
}
func CheckLdSoPreload() bool {
return false
}
func UnsetInjectedEnv() error {
return nil
}
+23
View File
@@ -0,0 +1,23 @@
package antisandbox
import "testing"
func TestCheckLDPreloadNoPanic(t *testing.T) {
_ = CheckLDPreload()
}
func TestCheckLdSoPreloadNoPanic(t *testing.T) {
_ = CheckLdSoPreload()
}
func TestUnsetInjectedEnvNoError(t *testing.T) {
if err := UnsetInjectedEnv(); err != nil {
t.Errorf("UnsetInjectedEnv returned error: %v", err)
}
}
func TestCheckLDPreloadFalseInNormalState(t *testing.T) {
if CheckLDPreload() {
t.Skip("suspicious LD_PRELOAD-style .so detected in /proc/self/maps (test env may be hooked)")
}
}
+26
View File
@@ -0,0 +1,26 @@
package antivm
import (
"time"
)
// CheckTimerResolution measures time.Now() granularity. Real CPUs typically
// have < 1us resolution; VMs often have 1ms or 15ms (Windows default).
// Returns true if resolution appears native (< 1ms), false if VM-like.
func CheckTimerResolution() bool {
var minDelta time.Duration = time.Hour
const samples = 100
var last = time.Now()
for i := 0; i < samples; i++ {
start := time.Now()
for time.Since(start) < time.Microsecond {
_ = start
}
now := time.Now()
if d := now.Sub(last); d > 0 && d < minDelta {
minDelta = d
}
last = now
}
return minDelta < time.Millisecond
}
+26
View File
@@ -0,0 +1,26 @@
package antivm
import "testing"
func TestCheckTimerResolution_NoPanic(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("CheckTimerResolution panicked: %v", r)
}
}()
_ = CheckTimerResolution()
}
func TestCheckTimerResolution_ReturnsBool(t *testing.T) {
got := CheckTimerResolution()
if got != true && got != false {
t.Fatalf("CheckTimerResolution returned non-bool: %T(%v)", got, got)
}
}
func TestCheckTimerResolution_Repeatable(t *testing.T) {
const calls = 5
for i := 0; i < calls; i++ {
_ = CheckTimerResolution()
}
}
+110
View File
@@ -0,0 +1,110 @@
package integrity
import (
"crypto/sha256"
"fmt"
"sync"
"time"
"unsafe"
)
// Monitor periodically re-hashes a region of memory (typically the process's
// .text section) and reports drift to callers via Check().
type Monitor struct {
textStart uintptr
textSize int
hash [32]byte
interval time.Duration
stop chan struct{}
done chan struct{}
mu sync.RWMutex
drift bool
}
// StartMemoryRegionMonitor computes the initial SHA-256 of the supplied
// in-memory region and starts a background goroutine that re-hashes every
// interval. If drift is detected the monitor stops itself and Check() will
// return false.
//
// Distinct from StartMonitor(interval, onTamper) in integrity.go, which
// re-hashes the binary file on disk via os.Executable(). This function
// operates on a live memory region pointed to by textStart.
func StartMemoryRegionMonitor(textStart uintptr, textSize int, interval time.Duration) (*Monitor, error) {
if textSize <= 0 || interval <= 0 {
return nil, fmt.Errorf("integrity: invalid monitor params")
}
m := &Monitor{
textStart: textStart,
textSize: textSize,
interval: interval,
stop: make(chan struct{}),
done: make(chan struct{}),
}
h, err := hashRegion(textStart, textSize)
if err != nil {
return nil, err
}
m.hash = h
go m.run()
return m, nil
}
func (m *Monitor) run() {
defer close(m.done)
t := time.NewTicker(m.interval)
defer t.Stop()
for {
select {
case <-m.stop:
return
case <-t.C:
h, err := hashRegion(m.textStart, m.textSize)
if err != nil {
continue
}
m.mu.Lock()
if h != m.hash {
m.drift = true
m.mu.Unlock()
return
}
m.mu.Unlock()
}
}
}
// Stop halts the background re-hash loop and waits for it to exit.
func (m *Monitor) Stop() {
close(m.stop)
<-m.done
}
// Check re-hashes the region and reports whether it matches the baseline.
// It returns false (with no error) once drift has been observed.
func (m *Monitor) Check() (bool, error) {
m.mu.RLock()
drift := m.drift
m.mu.RUnlock()
if drift {
return false, nil
}
h, err := hashRegion(m.textStart, m.textSize)
if err != nil {
return false, err
}
return h == m.hash, nil
}
//go:nocheckptr
func hashRegion(addr uintptr, size int) ([32]byte, error) {
var zero [32]byte
if size <= 0 {
return zero, fmt.Errorf("integrity: invalid size")
}
// Direct uintptr -> unsafe.Pointer conversion. Combined with
// //go:nocheckptr above, this is also compatible with `go vet
// -unsafeptr=false` for builds that enable the runtime checkptr
// instrumentation (e.g. `go test -race`).
src := unsafe.Slice((*byte)(unsafe.Pointer(addr)), size)
return sha256.Sum256(src), nil
}
+115
View File
@@ -0,0 +1,115 @@
package integrity
import (
"sync"
"testing"
"time"
"unsafe"
)
func TestStartMemoryRegionMonitorInvalidParams(t *testing.T) {
if _, err := StartMemoryRegionMonitor(0, 0, 0); err == nil {
t.Error("expected error for zero size/interval")
}
if _, err := StartMemoryRegionMonitor(0, 16, 0); err == nil {
t.Error("expected error for zero interval")
}
if _, err := StartMemoryRegionMonitor(0, -1, time.Millisecond); err == nil {
t.Error("expected error for negative size")
}
}
func TestMonitorStartStopAndCheck(t *testing.T) {
// Use a heap-allocated buffer as the "text" region so the address stays
// valid for the lifetime of the test (and for the goroutine).
buf := make([]byte, 4096)
for i := range buf {
buf[i] = byte(i % 251)
}
ptr := uintptr(unsafe.Pointer(&buf[0]))
m, err := StartMemoryRegionMonitor(ptr, len(buf), 5*time.Millisecond)
if err != nil {
t.Fatalf("StartMemoryRegionMonitor = %v", err)
}
ok, err := m.Check()
if err != nil {
t.Fatalf("Check = %v", err)
}
if !ok {
t.Error("Check returned false on unmodified buffer")
}
// Let a few ticks pass; buffer is still untouched, so no drift should
// have been recorded. We can't observe the internal `drift` flag
// directly, but Check() should still report true after a short sleep.
time.Sleep(25 * time.Millisecond)
ok, err = m.Check()
if err != nil {
t.Fatalf("Check (post-sleep) = %v", err)
}
if !ok {
t.Error("Check returned false after sleeping on unmodified buffer")
}
m.Stop()
// Double-stop would panic on close(closed channel); just guard with a
// flag-style check that Stop is idempotent w.r.t. subsequent Check.
ok, err = m.Check()
if err != nil {
t.Fatalf("Check (post-stop) = %v", err)
}
if !ok {
t.Error("Check returned false after Stop on unmodified buffer")
}
}
func TestMonitorDetectsDrift(t *testing.T) {
// Build a 256-byte region, hash it, then mutate the region and re-hash to
// confirm hashRegion is actually reading the bytes pointed to.
buf := make([]byte, 256)
for i := range buf {
buf[i] = 0xAA
}
ptr := uintptr(unsafe.Pointer(&buf[0]))
h1, err := hashRegion(ptr, len(buf))
if err != nil {
t.Fatalf("hashRegion (initial) = %v", err)
}
buf[0] = 0xBB
h2, err := hashRegion(ptr, len(buf))
if err != nil {
t.Fatalf("hashRegion (mutated) = %v", err)
}
if h1 == h2 {
t.Error("hashRegion did not detect single-byte mutation")
}
}
func TestMonitorRaceFree(t *testing.T) {
// Exercises Check + Stop from multiple goroutines under -race.
buf := make([]byte, 1024)
ptr := uintptr(unsafe.Pointer(&buf[0]))
m, err := StartMemoryRegionMonitor(ptr, len(buf), 2*time.Millisecond)
if err != nil {
t.Fatalf("StartMemoryRegionMonitor = %v", err)
}
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 50; j++ {
_, _ = m.Check()
}
}()
}
wg.Wait()
m.Stop()
}
+87
View File
@@ -0,0 +1,87 @@
// Package smc provides MVP self-modifying code primitives.
//
// MVP scope: XOR-encrypt a function payload, hold it locked in a
// pageman.SecureBuffer, decrypt briefly inside an Access callback,
// re-encrypt on callback return. Production SMC needs per-arch
// assembly for atomic decrypt+execute (not in MVP).
package smc
import (
"fmt"
"github.com/lukaszraczylo/go-obfuscation/pkg/pageman"
)
// Encrypt XOR-encrypts code with key. key must be non-empty.
func Encrypt(code, key []byte) ([]byte, error) {
if len(key) == 0 {
return nil, fmt.Errorf("smc: empty key")
}
out := make([]byte, len(code))
for i, b := range code {
out[i] = b ^ key[i%len(key)]
}
return out, nil
}
// Decrypt reverses Encrypt.
func Decrypt(code, key []byte) ([]byte, error) {
return Encrypt(code, key) // XOR is symmetric
}
// Execute demonstrates the encrypt/decrypt cycle on an encrypted payload.
//
// It parks the payload inside a pageman.SecureBuffer (locked at rest),
// unlocks it via Access to do an in-place decrypt, then locks it back.
// The "execute" step is a no-op: jumping to arbitrary decrypted bytes
// would require platform-specific func() cast and an RX window, both
// outside MVP scope.
//
// MVP limitation: the brief window where plaintext exists inside the
// Access callback is observable. Production needs arch-specific atomic
// decrypt+exec.
func Execute(encrypted, key []byte) error {
if len(encrypted) == 0 {
return fmt.Errorf("smc: empty encrypted payload")
}
if len(key) == 0 {
return fmt.Errorf("smc: empty key")
}
sb := pageman.NewSecureBuffer(encrypted, 0)
defer sb.Close()
// First cycle: unlock -> decrypt in place (verify) -> relock.
if err := sb.Access(func(plain []byte) {
for i, b := range plain {
plain[i] = b ^ key[i%len(key)]
}
}); err != nil {
return fmt.Errorf("smc: decrypt access failed: %w", err)
}
// Second cycle: unlock -> re-encrypt in place -> relock.
if err := sb.Access(func(plain []byte) {
for i, b := range plain {
plain[i] = b ^ key[i%len(key)]
}
}); err != nil {
return fmt.Errorf("smc: re-encrypt access failed: %w", err)
}
return nil
}
// ExecuteFunc calls fn after running it through a no-op encrypt/decrypt
// cycle on a side buffer. MVP: the function is invoked normally; the
// cycle just exercises the smc primitives around it.
func ExecuteFunc(fn func(), key []byte) error {
// Build a synthetic payload, run the cycle, then call the function.
payload := []byte{0xDE, 0xAD, 0xBE, 0xEF}
if err := Execute(payload, key); err != nil {
return err
}
defer func() { _ = recover() }()
fn()
return nil
}
+84
View File
@@ -0,0 +1,84 @@
package smc
import (
"bytes"
"sync"
"testing"
)
func TestEncryptDecryptRoundtrip(t *testing.T) {
code := []byte("hello self-modifying world")
key := []byte("k3y-mvp-2026")
enc, err := Encrypt(code, key)
if err != nil {
t.Fatalf("Encrypt: %v", err)
}
if bytes.Equal(enc, code) {
t.Fatal("ciphertext matches plaintext (no encryption)")
}
dec, err := Decrypt(enc, key)
if err != nil {
t.Fatalf("Decrypt: %v", err)
}
if !bytes.Equal(dec, code) {
t.Fatalf("roundtrip mismatch: got %q want %q", dec, code)
}
}
func TestEncryptEmptyKey(t *testing.T) {
if _, err := Encrypt([]byte("x"), nil); err == nil {
t.Fatal("expected error for empty key")
}
if _, err := Encrypt([]byte("x"), []byte{}); err == nil {
t.Fatal("expected error for zero-length key")
}
}
func TestExecuteEmpty(t *testing.T) {
if err := Execute(nil, []byte("k")); err == nil {
t.Fatal("expected error for empty payload")
}
}
func TestExecuteEmptyKey(t *testing.T) {
if err := Execute([]byte{1, 2, 3}, nil); err == nil {
t.Fatal("expected error for empty key")
}
}
func TestExecuteFuncNoPanic(t *testing.T) {
var ran bool
fn := func() { ran = true }
if err := ExecuteFunc(fn, []byte("k")); err != nil {
t.Fatalf("ExecuteFunc: %v", err)
}
if !ran {
t.Fatal("function did not run")
}
}
func TestExecuteConcurrent(t *testing.T) {
const n = 10
var wg sync.WaitGroup
errs := make(chan error, n)
key := []byte("shared-key")
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
payload := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
if err := Execute(payload, key); err != nil {
errs <- err
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
t.Errorf("concurrent Execute: %v", err)
}
}