mirror of
https://github.com/Kowntaewook/MCPTrace.git
synced 2026-11-02 04:45:09 +01:00
111 lines
5.0 KiB
Python
111 lines
5.0 KiB
Python
"""Re-read Linux JSONL artifacts through native Volatility process layers.
|
|
|
|
This checks exported bytes, hashes, and VMA bounds. It does not establish that
|
|
the JSON was transmitted or measure message recovery against ground truth.
|
|
No MCPTrace scanner or adapter is used to perform the verification.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import volatility3.plugins
|
|
import volatility3.symbols
|
|
from volatility3 import framework
|
|
from volatility3.framework import automagic, constants, contexts, plugins
|
|
from volatility3.framework.automagic import stacker
|
|
from volatility3.plugins.linux import pslist
|
|
|
|
|
|
def verify_file(path, context, tasks):
|
|
with path.open(encoding="utf-8") as stream:
|
|
rows = [json.loads(line) for line in stream if line.strip()]
|
|
if len(rows) < 2 or rows[0].get("type") != "metadata":
|
|
raise ValueError(f"{path.name}: missing export metadata")
|
|
header, *records, summary = rows
|
|
pid = header["pid"]
|
|
if header.get("target_os") != "linux" or header.get("region_kind") != "vma":
|
|
raise ValueError(f"{path.name}: not a Linux VMA export")
|
|
if header.get("address_space") != "process-virtual":
|
|
raise ValueError(f"{path.name}: not a process virtual address export")
|
|
if summary.get("type") != "scan-summary" or summary.get("displayed_records") != len(records):
|
|
raise ValueError(f"{path.name}: summary/count mismatch")
|
|
if pid not in tasks:
|
|
raise ValueError(f"{path.name}: PID {pid} is not in native linux.pslist")
|
|
task = tasks[pid]
|
|
total_bytes = 0
|
|
if records:
|
|
if not task.mm:
|
|
raise ValueError(f"{path.name}: records attributed to a kernel thread")
|
|
layer_name = task.add_process_layer()
|
|
if not layer_name:
|
|
raise ValueError(f"{path.name}: process layer unavailable")
|
|
layer = context.layers[layer_name]
|
|
regions = {int(v.vm_start): int(v.vm_end) for v in task.mm.get_vma_iter()}
|
|
for index, record in enumerate(records, 2):
|
|
label = f"{path.name}:{index}"
|
|
if (
|
|
record.get("type") != "artifact"
|
|
or record.get("pid") != pid
|
|
or record.get("target_os") != "linux"
|
|
or record.get("region_kind") != "vma"
|
|
):
|
|
raise ValueError(f"{label}: artifact metadata mismatch")
|
|
raw = base64.b64decode(record["raw_base64"], validate=True)
|
|
if not raw or len(raw) != record["byte_length"]:
|
|
raise ValueError(f"{label}: byte length mismatch")
|
|
if hashlib.sha256(raw).hexdigest() != record["raw_sha256"]:
|
|
raise ValueError(f"{label}: SHA-256 mismatch")
|
|
start, address = record["region_start"], record["virtual_address"]
|
|
if (
|
|
record.get("vma_start") != start
|
|
or start not in regions
|
|
or not start <= address < address + len(raw) <= regions[start]
|
|
):
|
|
raise ValueError(f"{label}: VMA bounds mismatch")
|
|
if layer.read(address, len(raw), pad=False) != raw:
|
|
raise ValueError(f"{label}: process memory bytes differ")
|
|
total_bytes += len(raw)
|
|
return {"pid": pid, "artifacts_verified": len(records), "bytes_verified": total_bytes}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--image", type=Path, required=True)
|
|
parser.add_argument("--symbols", type=Path, required=True)
|
|
parser.add_argument("--cache", type=Path, required=True)
|
|
parser.add_argument("--require-artifacts", action="store_true")
|
|
parser.add_argument("exports", type=Path, nargs="+")
|
|
args = parser.parse_args()
|
|
args.cache.mkdir(parents=True, exist_ok=True)
|
|
constants.CACHE_PATH = str(args.cache.resolve())
|
|
constants.OFFLINE = True
|
|
volatility3.symbols.__path__ = [str(args.symbols.resolve()), *constants.SYMBOL_BASEPATHS]
|
|
context = contexts.Context()
|
|
framework.import_files(volatility3.plugins, True)
|
|
# Import automagic modules before selecting stackers: the Linux stackers
|
|
# live there, not in framework.layers.
|
|
magics = automagic.choose_automagic(automagic.available(context), pslist.PsList)
|
|
context.config["automagic.LayerStacker.single_location"] = args.image.resolve().as_uri()
|
|
context.config["automagic.LayerStacker.stackers"] = stacker.choose_os_stackers(pslist.PsList)
|
|
plugin = plugins.construct_plugin(context, magics, pslist.PsList, "plugins", None, None)
|
|
tasks = {
|
|
int(task.tgid): task
|
|
for task in pslist.PsList.list_tasks(
|
|
context, plugin.config["kernel"], include_threads=False
|
|
)
|
|
}
|
|
results = [verify_file(path, context, tasks) for path in args.exports]
|
|
count = sum(result["artifacts_verified"] for result in results)
|
|
if args.require_artifacts and not count:
|
|
raise ValueError("No artifacts to verify")
|
|
print(json.dumps({"artifacts_verified": count, "exports": results}, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|