Initial commit

This commit is contained in:
2026-05-31 01:09:34 +01:00
commit 1d51b29b92
31 changed files with 4874 additions and 0 deletions
+173
View File
@@ -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
}
+11
View File
@@ -0,0 +1,11 @@
//go:build darwin
package antidebug
func checkFunctionPrologues() error {
return nil
}
func scanForBreakpoints() error {
return nil
}
+167
View File
@@ -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
}
+165
View File
@@ -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
}
+114
View File
@@ -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
}
+216
View File
@@ -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<<shift | b>>(8-shift)
}
func unrotateKey(b byte, pos int) byte {
shift := uint((pos*3 + 7) % 8)
return b>>shift | b<<(8-shift)
}
+48
View File
@@ -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)
}
+151
View File
@@ -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,
}
}
+70
View File
@@ -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)
}
+44
View File
@@ -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)
}
+39
View File
@@ -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
}
+100
View File
@@ -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
}
+26
View File
@@ -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,
}
}
+63
View File
@@ -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
}
+122
View File
@@ -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
}
+35
View File
@@ -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")
}
}
+66
View File
@@ -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
}
+146
View File
@@ -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
}
+248
View File
@@ -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 }
+537
View File
@@ -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)
}
}