gomcp/internal/infrastructure/antitamper/antitamper.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
}