Geheugenforensiek voor AI-systemen
Technieken voor geheugenforensiek bij het onderzoeken van gecompromitteerde AI-systemen, waaronder analyse van GPU-geheugen, extractie van modelgewichten en herstel van de runtime-staat.
Overzicht
Geheugenforensiek voor AI-systemen breidt traditionele digitale forensiek uit naar de unieke runtime-omgeving van machine learning-workloads. AI-systemen behouden complexe staat in zowel CPU- als GPU-geheugen tijdens inferentie en training: modelgewichten, optimizer-staten, attention-caches (KV-caches), intermediate activaties, tokenizer-configuraties en dynamisch geladen adapter-gewichten. Wanneer een AI-systeem wordt gecompromitteerd — of dat nu via modelmanipulatie, ongeautoriseerde fine-tuning of runtime-manipulatie is — biedt geheugenforensiek de onderzoeker een momentopname van de werkelijke staat van het systeem op het moment van vastlegging.
Traditionele tools voor geheugenforensiek zoals Volatility zijn ontworpen voor CPU-adresseerbaar geheugen en besturingssysteem-artefacten. AI-workloads verdelen echter kritieke staat over GPU-VRAM, unified memory-architecturen en framework-beheerde memory pools die gespecialiseerde extractietechnieken vereisen. Dit artikel behandelt het end-to-end-proces van geheugenforensiek voor AI-systemen, van vastlegging tot analyse en rapportage.
De belangen zijn aanzienlijk: als een aanvaller modelgewichten in het geheugen heeft gewijzigd zonder de on-disk checkpoint te wijzigen, zal alleen een forensisch geheugenonderzoek de manipulatie onthullen. Evenzo, als een aanvaller kwaadaardige code heeft geïnjecteerd in een model-serving-pijplijn die outputs tijdens runtime wijzigt, kan geheugenanalyse de enige manier zijn om de geïnjecteerde logica te herstellen.
Geheugenarchitectuur van AI-systemen
CPU-geheugenindeling
AI-serving-frameworks (vLLM, Triton Inference Server, TorchServe) behouden verschillende categorieën data in CPU-geheugen:
- Modelconfiguratie: Hyperparameters, tokenizer-vocabulaire, generatieparameters
- Verzoekwachtrijen: Wachtende inferentieverzoeken inclusief volledige prompttekst
- Responsbuffers: Gegenereerde outputs voordat ze aan clients worden geleverd
- Framework-metadata: Schedulingstaat, batchsamenstelling, geheugentoewijzingskaarten
- Loggingbuffers: Circulaire buffers van recente inferentiegebeurtenissen
GPU-geheugenindeling
GPU-VRAM bevat de rekenkundig actieve componenten:
- Modelgewichten: De parametertensors die het modelgedrag definiëren, vaak in gekwantiseerde formaten (FP16, INT8, INT4)
- KV-cache: Key-value attention-cache voor actieve generatiesessies, die het "werkgeheugen" van het model voor lopende gesprekken bevat
- Activatietensors: Intermediate rekenresultaten tijdens forward passes
- CUDA graphs: Vooraf gecompileerde rekengrafen voor geoptimaliseerde inferentiepaden
Unified memory en NVLink
Moderne GPU-architecturen ondersteunen unified virtual addressing (UVA) waarbij CPU- en GPU-geheugen verschijnen als één enkele adresruimte. Multi-GPU-systemen verbonden via NVLink verdelen modelgewichten via tensor parallelism of pipeline parallelism, wat betekent dat een volledige modelstaat over meerdere GPU's verspreid kan zijn.
Technieken voor geheugenvastlegging
Verwerving van CPU-geheugen
Standaardtools voor geheugenverwerving werken voor het CPU-deel van de staat van een AI-systeem. Op Linux-systemen zijn de primaire methoden:
# Methode 1: Vastlegging via het /proc-bestandssysteem (vereist root)
# Leg het geheugen van een draaiend AI-serving-proces vast
AI_PID=$(pgrep -f "vllm.entrypoints")
cp /proc/${AI_PID}/maps /evidence/proc_maps_$(date +%s).txt
# Dump specifieke geheugengebieden geïdentificeerd uit het maps-bestand
# Focus op heap-gebieden waar modelconfigs en verzoekdata zich bevinden
grep "heap" /proc/${AI_PID}/maps
# Methode 2: gcore gebruiken voor een volledige core dump van het proces
# Dit pauzeert het proces kort -- stem dit af met operations
gcore -o /evidence/ai_server_core ${AI_PID}
# Methode 3: LiME (Linux Memory Extractor) gebruiken voor volledig systeemgeheugen
# Bouw de LiME-kernelmodule voor de draaiende kernel
# insmod lime.ko "path=/evidence/full_memory.lime format=lime"Verwerving van GPU-geheugen
Het verwerven van GPU-geheugen is complexer omdat GPU-VRAM niet rechtstreeks adresseerbaar is vanuit host-CPU-code. De primaire benaderingen zijn:
"""
Module voor forensische vastlegging van GPU-geheugen.
Legt modelgewichten, KV-cache en activatiestaat vast uit GPU-geheugen
voor forensische analyse. Vereist dat het modelproces toegankelijk is
(ofwel draaiend, ofwel via een opgeslagen CUDA-context).
"""
import torch
import json
import hashlib
import time
from pathlib import Path
from dataclasses import dataclass
@dataclass
class GPUMemoryCapture:
"""Container voor een forensische vastlegging van GPU-geheugen."""
capture_time: float
gpu_device: int
tensors: dict[str, dict] # naam -> {shape, dtype, hash, data_path}
gpu_info: dict
cuda_memory_stats: dict
def capture_gpu_state(
model: torch.nn.Module,
output_dir: str,
device_id: int = 0,
) -> GPUMemoryCapture:
"""
Leg de volledige GPU-resident staat van een model vast voor forensische analyse.
Deze functie itereert over alle parameters en buffers in het model,
berekent integriteitshashes en slaat tensors op naar schijf. De vastlegging
behoudt de exacte binaire representatie van elke tensor.
"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
torch.cuda.synchronize(device_id)
tensors = {}
for name, param in model.named_parameters():
cpu_copy = param.detach().cpu()
tensor_bytes = cpu_copy.numpy().tobytes()
tensor_hash = hashlib.sha256(tensor_bytes).hexdigest()
tensor_path = output_path / f"{name.replace('.', '_')}.pt"
torch.save(cpu_copy, tensor_path)
tensors[name] = {
"shape": list(param.shape),
"dtype": str(param.dtype),
"device": str(param.device),
"hash_sha256": tensor_hash,
"data_path": str(tensor_path),
"requires_grad": param.requires_grad,
"size_bytes": len(tensor_bytes),
}
# Leg bufferstaat vast (running means, varianties, enz.)
for name, buf in model.named_buffers():
cpu_copy = buf.detach().cpu()
tensor_bytes = cpu_copy.numpy().tobytes()
tensor_hash = hashlib.sha256(tensor_bytes).hexdigest()
tensor_path = output_path / f"buffer_{name.replace('.', '_')}.pt"
torch.save(cpu_copy, tensor_path)
tensors[f"buffer:{name}"] = {
"shape": list(buf.shape),
"dtype": str(buf.dtype),
"hash_sha256": tensor_hash,
"data_path": str(tensor_path),
}
gpu_info = {
"name": torch.cuda.get_device_name(device_id),
"total_memory_bytes": torch.cuda.get_device_properties(device_id).total_mem,
"capability": list(torch.cuda.get_device_capability(device_id)),
}
memory_stats = torch.cuda.memory_stats(device_id)
capture = GPUMemoryCapture(
capture_time=time.time(),
gpu_device=device_id,
tensors=tensors,
gpu_info=gpu_info,
cuda_memory_stats={
k: v for k, v in memory_stats.items()
if isinstance(v, (int, float))
},
)
manifest_path = output_path / "capture_manifest.json"
manifest_path.write_text(json.dumps({
"capture_time": capture.capture_time,
"gpu_device": capture.gpu_device,
"gpu_info": capture.gpu_info,
"tensor_count": len(capture.tensors),
"tensors": capture.tensors,
}, indent=2))
return captureExtractie van de KV-cache
De KV (Key-Value) attention-cache is forensisch bijzonder waardevol, omdat deze de berekende representaties van het model bevat van alle tokens die in actieve sessies zijn verwerkt. Het extraheren van de KV-cache kan onthullen welke prompts werden verwerkt en met welke gesprekscontext het model opereerde.
def extract_kv_cache_forensics(
kv_cache: list[tuple[torch.Tensor, torch.Tensor]],
output_dir: str,
) -> dict:
"""
Extraheer en analyseer de KV-cachestaat voor forensische doeleinden.
De KV-cache bevat key- en value-tensors voor elke attention-laag,
die de berekende context van het model voor actieve sessies representeren.
Args:
kv_cache: Lijst van (key, value) tensor-paren, één per laag.
output_dir: Directory om de geëxtraheerde cachedata weg te schrijven.
"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
analysis = {"layers": [], "total_tokens_cached": 0}
for layer_idx, (keys, values) in enumerate(kv_cache):
# keys shape: (batch, num_heads, seq_len, head_dim)
seq_len = keys.shape[2] if keys.dim() == 4 else keys.shape[1]
analysis["total_tokens_cached"] = max(
analysis["total_tokens_cached"], seq_len
)
layer_info = {
"layer": layer_idx,
"key_shape": list(keys.shape),
"value_shape": list(values.shape),
"key_hash": hashlib.sha256(
keys.detach().cpu().numpy().tobytes()
).hexdigest(),
"value_hash": hashlib.sha256(
values.detach().cpu().numpy().tobytes()
).hexdigest(),
"cached_sequence_length": seq_len,
}
# Sla tensors op voor gedetailleerde analyse
torch.save(keys.detach().cpu(), output_path / f"layer_{layer_idx}_keys.pt")
torch.save(values.detach().cpu(), output_path / f"layer_{layer_idx}_values.pt")
analysis["layers"].append(layer_info)
return analysisGeheugenvastleggingen analyseren
Verificatie van gewichtsintegriteit
De meest kritieke analyse is het vergelijken van vastgelegde gewichten met bekende goede referentiechecksums. Elke discrepantie wijst op ofwel modelmanipulatie ofwel een ongeautoriseerde update.
def verify_weight_integrity(
capture_manifest: dict,
reference_hashes: dict[str, str],
) -> dict:
"""
Vergelijk vastgelegde hashes van modelgewichten met referentiechecksums.
Args:
capture_manifest: Het manifest van een vastlegging van GPU-geheugen.
reference_hashes: Dict die parameternamen koppelt aan verwachte SHA-256-hashes.
Returns:
Analyseresultaten inclusief eventuele mismatches.
"""
results = {
"total_parameters": len(capture_manifest["tensors"]),
"verified_matching": 0,
"mismatches": [],
"missing_from_reference": [],
"extra_in_capture": [],
}
captured_names = set(capture_manifest["tensors"].keys())
reference_names = set(reference_hashes.keys())
results["missing_from_reference"] = list(captured_names - reference_names)
results["extra_in_capture"] = list(reference_names - captured_names)
for name, tensor_info in capture_manifest["tensors"].items():
if name in reference_hashes:
if tensor_info["hash_sha256"] == reference_hashes[name]:
results["verified_matching"] += 1
else:
results["mismatches"].append({
"parameter": name,
"expected_hash": reference_hashes[name],
"captured_hash": tensor_info["hash_sha256"],
"shape": tensor_info["shape"],
"dtype": tensor_info["dtype"],
})
results["integrity_status"] = (
"VERIFIED" if not results["mismatches"]
and not results["missing_from_reference"]
else "COMPROMISED"
)
return resultsGeïnjecteerde adapter-gewichten detecteren
Een aanvaller die toegang heeft gekregen tot een model-serving-systeem kan LoRA-adapter-gewichten injecteren om het modelgedrag te wijzigen zonder de basis-modelgewichten te veranderen. Dit is forensisch onopvallend, omdat de hashes van het basismodel nog steeds overeenkomen met de referentie.
def detect_unexpected_adapters(
model: torch.nn.Module,
expected_adapter_names: set[str] | None = None,
) -> dict:
"""
Scan een model op onverwachte LoRA- of adapter-modules.
Aanvallers kunnen adapter-gewichten injecteren om gedrag te wijzigen zonder
de basis-modelgewichten te veranderen. Deze functie identificeert eventuele adapter-
modules die geen deel uitmaakten van de verwachte configuratie.
"""
expected = expected_adapter_names or set()
findings = {"expected_adapters": [], "unexpected_adapters": [], "suspicious_modules": []}
for name, module in model.named_modules():
module_type = type(module).__name__
# Controleer op veelvoorkomende adapter-moduletypen
is_adapter = any(keyword in module_type.lower() for keyword in [
"lora", "adapter", "prefix", "prompt_tuning", "ia3",
])
if is_adapter:
info = {
"name": name,
"type": module_type,
"param_count": sum(p.numel() for p in module.parameters()),
}
if name in expected:
findings["expected_adapters"].append(info)
else:
findings["unexpected_adapters"].append(info)
# Controleer ook op verdacht benoemde parameters
for pname, param in module.named_parameters(recurse=False):
if any(kw in pname.lower() for kw in ["inject", "hook", "patch", "backdoor"]):
findings["suspicious_modules"].append({
"module": name,
"parameter": pname,
"shape": list(param.shape),
})
return findingsDetectie van runtime-hooks
Het hook-mechanisme van PyTorch maakt het mogelijk dat code forward en backward passes onderschept. Een aanvaller kan hooks registreren die modeloutputs wijzigen zonder de gewichten te veranderen. Forensische analyse moet alle geregistreerde hooks opsommen.
def enumerate_model_hooks(model: torch.nn.Module) -> dict:
"""
Som alle geregistreerde forward- en backward-hooks op een model op.
PyTorch-hooks kunnen modelgedrag tijdens runtime wijzigen zonder
de gewichten te veranderen. Een aanvaller zou hooks kunnen gebruiken om:
- Specifieke outputs te wijzigen op basis van trigger-inputs
- Data te exfiltreren via zijkanalen
- Veiligheidsfilters selectief te omzeilen
"""
findings = {"forward_hooks": [], "backward_hooks": [], "forward_pre_hooks": []}
for name, module in model.named_modules():
# Controleer forward-hooks
if hasattr(module, '_forward_hooks') and module._forward_hooks:
for hook_id, hook in module._forward_hooks.items():
findings["forward_hooks"].append({
"module": name,
"hook_id": hook_id,
"hook_function": str(hook),
"source_file": getattr(hook, '__module__', 'unknown'),
})
# Controleer backward-hooks
if hasattr(module, '_backward_hooks') and module._backward_hooks:
for hook_id, hook in module._backward_hooks.items():
findings["backward_hooks"].append({
"module": name,
"hook_id": hook_id,
"hook_function": str(hook),
})
# Controleer forward pre-hooks
if hasattr(module, '_forward_pre_hooks') and module._forward_pre_hooks:
for hook_id, hook in module._forward_pre_hooks.items():
findings["forward_pre_hooks"].append({
"module": name,
"hook_id": hook_id,
"hook_function": str(hook),
})
findings["total_hooks"] = (
len(findings["forward_hooks"])
+ len(findings["backward_hooks"])
+ len(findings["forward_pre_hooks"])
)
return findingsProcesgeheugenanalyse voor AI-frameworks
Herstel van Python-objecten
AI-serving-systemen draaien doorgaans in Python-processen. Traditionele geheugenforensiek kan worden aangevuld met Python-specifieke analyse om objecten uit de heap te herstellen.
# Gebruik py-spy om een momentopname van de Python-processtaat te krijgen
# Dit legt de call stack van alle threads vast zonder het proces te stoppen
py-spy dump --pid ${AI_PID} > /evidence/python_state.txt
# Gebruik voor diepere analyse gdb met Python-extensies
gdb -batch -ex "source /usr/lib/python3.11/gdb_helpers.py" \
-ex "py-bt" -ex "quit" -p ${AI_PID} > /evidence/python_backtrace.txtVerzoekdata uit het geheugen herstellen
Inferentieverzoeken die door de serving-pijplijn gaan, laten sporen achter in het geheugen die kunnen worden hersteld, zelfs nadat het verzoek is verwerkt. Deze sporen bevinden zich in:
- Python-string-objecten in de door de garbage collector getrackte objecten
- Datastructuren van de verzoekwachtrij van het framework
- HTTP-serverbuffers (bij gebruik van HTTP-gebaseerde serving)
- Encode/decode-buffers van de tokenizer
Onderzoeksworkflow
Fase 1: Behoud van de plaats delict
- Documenteer de huidige systeemstaat (draaiende processen, netwerkverbindingen, GPU-gebruik)
- Leg vluchtig bewijs vast in volgorde van vluchtigheid: GPU-VRAM eerst, dan CPU-geheugen, dan schijf
- Registreer alle systeemtijdstempels en synchroniseer met NTP
Fase 2: Geheugenverwerving
- Voer GPU-geheugenvastlegging uit met de hierboven beschreven technieken
- Verwerf CPU-geheugen met LiME of het /proc-bestandssysteem
- Leg processpecifiek geheugen vast voor elk AI-serving-proces
- Verifieer de vastleggingsintegriteit met checksums
Fase 3: Analyse
- Vergelijk gewichtshashes met referentiechecksums
- Scan op onverwachte adapter-modules of hooks
- Analyseer de KV-cache op bewijs van specifieke interacties
- Doorzoek CPU-geheugen op artefacten van aanvalleractiviteit
Fase 4: Correlatie
- Kruisverwijs geheugenbevindingen met loganalyse uit andere forensische werkstromen
- Map bevindingen naar MITRE ATLAS-technieken
- Stel een tijdlijn van de compromittering vast met behulp van geheugenartefacten
Uitdagingen en beperkingen
Geheugenforensiek voor AI-systemen kent verschillende unieke uitdagingen:
- Geheugenvluchtigheid: GPU-geheugen is uiterst vluchtig. De inhoud van VRAM verandert bij elk inferentieverzoek, en er is geen persistente opslag of swap voor GPU-geheugen.
- Schaal: Grote taalmodellen kunnen 100+ GB aan VRAM over meerdere GPU's innemen. Het vastleggen en analyseren van dit datavolume vereist aanzienlijke opslag- en rekenmiddelen.
- Encryptie: Sommige GPU-architecturen ondersteunen geheugenencryptie (AMD SEV-SNP, NVIDIA Confidential Computing) die directe geheugenuitlezingen vanaf de host voorkomt.
- Frameworkondoorzichtigheid: Deep learning-frameworks beheren hun eigen memory pools, waardoor het moeilijk is om ruwe geheugenadressen te koppelen aan betekenisvolle datastructuren zonder framework-specifieke kennis.
- Kwantisatie-artefacten: Modellen die in gekwantiseerde formaten (INT4, INT8, FP8) worden geserveerd, vereisen kennis van het kwantisatieschema om gewichtswaarden correct te interpreteren.
Referenties
- Ligh, M. H., Case, A., Levy, J., & Walters, A. (2014). The Art of Memory Forensics: Detecting Malware and Threats in Windows, Linux, and Mac Memory. Wiley.
- MITRE ATLAS. (2024). Adversarial Threat Landscape for Artificial Intelligence Systems. https://atlas.mitre.org/
- NVIDIA. (2024). CUDA C++ Programming Guide: Unified Memory. https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#unified-memory-programming
- NIST. (2023). Artificial Intelligence Risk Management Framework (AI RMF 1.0). NIST AI 100-1. https://doi.org/10.6028/NIST.AI.100-1