mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-28 05:46:22 +02:00
299 lines
7.3 KiB
Go
299 lines
7.3 KiB
Go
// Package antitamper implements SEC-005 Anti-Tamper Protection.
|
|
//
|
|
// Provides runtime protection against:
|
|
// - ptrace/debugger attachment to SOC processes
|
|
// - memory dump (process_vm_readv)
|
|
// - binary modification detection via SHA-256 integrity checks
|
|
// - environment variable tampering
|
|
//
|
|
// On Linux: uses prctl(PR_SET_DUMPABLE, 0) and self-ptrace detection.
|
|
// On Windows: uses IsDebuggerPresent() and NtQueryInformationProcess.
|
|
// Cross-platform: binary hash verification and env integrity checks.
|
|
package antitamper
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// TamperType classifies the tampering attempt.
|
|
type TamperType string
|
|
|
|
const (
|
|
TamperDebugger TamperType = "debugger_attached"
|
|
TamperPtrace TamperType = "ptrace_attempt"
|
|
TamperBinaryMod TamperType = "binary_modified"
|
|
TamperEnvTamper TamperType = "env_tampering"
|
|
TamperMemoryDump TamperType = "memory_dump"
|
|
|
|
// CheckInterval for periodic integrity verification.
|
|
DefaultCheckInterval = 5 * time.Minute
|
|
)
|
|
|
|
// TamperEvent records a detected tampering attempt.
|
|
type TamperEvent struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Type TamperType `json:"type"`
|
|
Detail string `json:"detail"`
|
|
Severity string `json:"severity"`
|
|
PID int `json:"pid"`
|
|
Binary string `json:"binary,omitempty"`
|
|
}
|
|
|
|
// TamperHandler is called when tampering is detected.
|
|
type TamperHandler func(event TamperEvent)
|
|
|
|
// Shield provides anti-tamper protection for SOC processes.
|
|
type Shield struct {
|
|
mu sync.RWMutex
|
|
binaryPath string
|
|
binaryHash string // SHA-256 at startup
|
|
envSnapshot map[string]string
|
|
handlers []TamperHandler
|
|
logger *slog.Logger
|
|
stats ShieldStats
|
|
}
|
|
|
|
// ShieldStats tracks anti-tamper metrics.
|
|
type ShieldStats struct {
|
|
mu sync.Mutex
|
|
TotalChecks int64 `json:"total_checks"`
|
|
TamperDetected int64 `json:"tamper_detected"`
|
|
DebuggerBlocked int64 `json:"debugger_blocked"`
|
|
BinaryIntegrity bool `json:"binary_integrity"`
|
|
LastCheck time.Time `json:"last_check"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
}
|
|
|
|
// NewShield creates a new anti-tamper shield.
|
|
// Takes a snapshot of the binary hash and critical env vars at startup.
|
|
func NewShield() (*Shield, error) {
|
|
binaryPath, err := os.Executable()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("antitamper: get executable: %w", err)
|
|
}
|
|
|
|
hash, err := hashFile(binaryPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("antitamper: hash binary: %w", err)
|
|
}
|
|
|
|
// Snapshot critical environment variables.
|
|
criticalEnvs := []string{
|
|
"SOC_DB_PATH", "SOC_JWT_SECRET", "SOC_GUARD_POLICY",
|
|
"GOMEMLIMIT", "SOC_AUDIT_DIR", "SOC_PORT",
|
|
}
|
|
envSnap := make(map[string]string)
|
|
for _, key := range criticalEnvs {
|
|
envSnap[key] = os.Getenv(key)
|
|
}
|
|
|
|
shield := &Shield{
|
|
binaryPath: binaryPath,
|
|
binaryHash: hash,
|
|
envSnapshot: envSnap,
|
|
logger: slog.Default().With("component", "sec-005-antitamper"),
|
|
stats: ShieldStats{
|
|
BinaryIntegrity: true,
|
|
StartedAt: time.Now(),
|
|
},
|
|
}
|
|
|
|
// Platform-specific initialization (disable core dumps, set non-dumpable).
|
|
shield.platformInit()
|
|
|
|
shield.logger.Info("anti-tamper shield initialized",
|
|
"binary", binaryPath,
|
|
"hash", hash[:16]+"...",
|
|
"env_keys", len(envSnap),
|
|
)
|
|
|
|
return shield, nil
|
|
}
|
|
|
|
// OnTamper registers a handler for tampering events.
|
|
func (s *Shield) OnTamper(h TamperHandler) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.handlers = append(s.handlers, h)
|
|
}
|
|
|
|
// CheckBinaryIntegrity verifies the running binary hasn't been modified.
|
|
func (s *Shield) CheckBinaryIntegrity() *TamperEvent {
|
|
s.stats.mu.Lock()
|
|
s.stats.TotalChecks++
|
|
s.stats.LastCheck = time.Now()
|
|
s.stats.mu.Unlock()
|
|
|
|
currentHash, err := hashFile(s.binaryPath)
|
|
if err != nil {
|
|
event := TamperEvent{
|
|
Timestamp: time.Now(),
|
|
Type: TamperBinaryMod,
|
|
Detail: fmt.Sprintf("cannot read binary for hash check: %v", err),
|
|
Severity: "HIGH",
|
|
PID: os.Getpid(),
|
|
Binary: s.binaryPath,
|
|
}
|
|
s.recordTamper(event)
|
|
return &event
|
|
}
|
|
|
|
if currentHash != s.binaryHash {
|
|
s.stats.mu.Lock()
|
|
s.stats.BinaryIntegrity = false
|
|
s.stats.mu.Unlock()
|
|
|
|
event := TamperEvent{
|
|
Timestamp: time.Now(),
|
|
Type: TamperBinaryMod,
|
|
Detail: fmt.Sprintf("binary modified! expected=%s got=%s",
|
|
truncHash(s.binaryHash), truncHash(currentHash)),
|
|
Severity: "CRITICAL",
|
|
PID: os.Getpid(),
|
|
Binary: s.binaryPath,
|
|
}
|
|
s.recordTamper(event)
|
|
return &event
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckEnvIntegrity verifies critical environment variables haven't changed.
|
|
func (s *Shield) CheckEnvIntegrity() *TamperEvent {
|
|
s.stats.mu.Lock()
|
|
s.stats.TotalChecks++
|
|
s.stats.mu.Unlock()
|
|
|
|
for key, originalValue := range s.envSnapshot {
|
|
current := os.Getenv(key)
|
|
if current != originalValue {
|
|
event := TamperEvent{
|
|
Timestamp: time.Now(),
|
|
Type: TamperEnvTamper,
|
|
Detail: fmt.Sprintf("env %s changed: original=%q current=%q",
|
|
key, originalValue, current),
|
|
Severity: "HIGH",
|
|
PID: os.Getpid(),
|
|
}
|
|
s.recordTamper(event)
|
|
return &event
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CheckDebugger checks if a debugger is attached.
|
|
// Platform-specific implementation in antitamper_*.go.
|
|
func (s *Shield) CheckDebugger() *TamperEvent {
|
|
s.stats.mu.Lock()
|
|
s.stats.TotalChecks++
|
|
s.stats.mu.Unlock()
|
|
|
|
if s.isDebuggerAttached() {
|
|
s.stats.mu.Lock()
|
|
s.stats.DebuggerBlocked++
|
|
s.stats.mu.Unlock()
|
|
|
|
event := TamperEvent{
|
|
Timestamp: time.Now(),
|
|
Type: TamperDebugger,
|
|
Detail: "debugger detected attached to SOC process",
|
|
Severity: "CRITICAL",
|
|
PID: os.Getpid(),
|
|
Binary: s.binaryPath,
|
|
}
|
|
s.recordTamper(event)
|
|
return &event
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunAllChecks performs all anti-tamper checks at once.
|
|
func (s *Shield) RunAllChecks() []TamperEvent {
|
|
var events []TamperEvent
|
|
|
|
if e := s.CheckDebugger(); e != nil {
|
|
events = append(events, *e)
|
|
}
|
|
if e := s.CheckBinaryIntegrity(); e != nil {
|
|
events = append(events, *e)
|
|
}
|
|
if e := s.CheckEnvIntegrity(); e != nil {
|
|
events = append(events, *e)
|
|
}
|
|
|
|
return events
|
|
}
|
|
|
|
// BinaryHash returns the expected binary hash (taken at startup).
|
|
func (s *Shield) BinaryHash() string {
|
|
return s.binaryHash
|
|
}
|
|
|
|
// Stats returns current shield metrics.
|
|
func (s *Shield) Stats() ShieldStats {
|
|
s.stats.mu.Lock()
|
|
defer s.stats.mu.Unlock()
|
|
return ShieldStats{
|
|
TotalChecks: s.stats.TotalChecks,
|
|
TamperDetected: s.stats.TamperDetected,
|
|
DebuggerBlocked: s.stats.DebuggerBlocked,
|
|
BinaryIntegrity: s.stats.BinaryIntegrity,
|
|
LastCheck: s.stats.LastCheck,
|
|
StartedAt: s.stats.StartedAt,
|
|
}
|
|
}
|
|
|
|
// recordTamper updates stats and notifies handlers.
|
|
func (s *Shield) recordTamper(event TamperEvent) {
|
|
s.stats.mu.Lock()
|
|
s.stats.TamperDetected++
|
|
s.stats.mu.Unlock()
|
|
|
|
s.logger.Error("TAMPER DETECTED",
|
|
"type", event.Type,
|
|
"detail", event.Detail,
|
|
"severity", event.Severity,
|
|
"pid", event.PID,
|
|
)
|
|
|
|
s.mu.RLock()
|
|
handlers := s.handlers
|
|
s.mu.RUnlock()
|
|
|
|
for _, h := range handlers {
|
|
h(event)
|
|
}
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func hashFile(path string) (string, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
|
|
h := sha256.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return hex.EncodeToString(h.Sum(nil)), nil
|
|
}
|
|
|
|
func truncHash(h string) string {
|
|
if len(h) > 16 {
|
|
return h[:16]
|
|
}
|
|
return h
|
|
}
|