Files
Xusheng 19dc1581ce Sync metadata-driven parameter decoding build from author
Win32 metadata index (win32json) drives per-call signature lookup so
arguments are captured at their true arity and type, [Out] params are
re-read at the call's return position, and buffers are dumped as bytes.
2026-07-30 15:06:07 -04:00

142 lines
5.0 KiB
Python

"""
One-command CAPA-over-TTD wrapper.
Runs the native ttdcapa-extract.exe to turn a .run trace into a neutral TTD report
JSON, then invokes capa with `-f ttd` against that report and a rules directory.
python ttd-capa.py <trace.run> <rules-dir> [--sample sample.exe]
[--extractor path\\to\\ttdcapa-extract.exe]
[--keep-json] [-- capa args...]
Anything after a bare `--` (or any unrecognized flags) is forwarded to capa, e.g.:
python ttd-capa.py trace.run ./rules -- -vv
"""
import os
import sys
import shutil
import argparse
import tempfile
import subprocess
from pathlib import Path
HERE = Path(__file__).resolve().parent
def find_extractor(explicit: str | None) -> Path:
if explicit:
p = Path(explicit)
if not p.is_file():
sys.exit(f"extractor not found: {p}")
return p
# search common build output locations relative to this script
candidates = [
HERE / "ttdcapa-extract.exe",
HERE / "ttd" / "bin" / "x64" / "Release" / "ttdcapa-extract.exe",
HERE / "ttd" / "bin" / "x64" / "Debug" / "ttdcapa-extract.exe"
]
for c in candidates:
if c.is_file():
return c
found = shutil.which("ttdcapa-extract")
if found:
return Path(found)
sys.exit(
"could not locate ttdcapa-extract.exe; build the ttd/ project or pass "
"--extractor <path>"
)
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description="Run CAPA capability detection over a TTD .run trace.",
epilog="Arguments after `--` are passed through to capa.",
)
parser.add_argument("trace", help="path to the TTD .run trace file")
parser.add_argument("rules", help="path to a directory of capa rules")
parser.add_argument("--sample", help="optional on-disk sample for accurate hashes")
parser.add_argument("--extractor", help="path to ttdcapa-extract.exe")
parser.add_argument("--max-calls", type=int, help="cap recorded API calls (for huge traces)")
parser.add_argument(
"--with-stack-args",
action="store_true",
help="for calls with no Win32 metadata, also grab four stack slots past the "
"register args (calls we have a signature for always capture their true arity)",
)
parser.add_argument("--win32-index", help="path to win32-index.bin (default: next to the extractor)")
parser.add_argument(
"--no-metadata",
action="store_true",
help="disable metadata-driven argument decoding entirely",
)
parser.add_argument("--max-buffer", type=int, help="bytes kept from any one captured buffer (default 256)")
parser.add_argument("--keep-json", action="store_true", help="keep the intermediate TTD report")
parser.add_argument("--python", default=sys.executable, help="python interpreter to run capa")
args, capa_extra = parser.parse_known_args(argv)
# argparse leaves a leading "--" in capa_extra if present; drop it
if capa_extra and capa_extra[0] == "--":
capa_extra = capa_extra[1:]
trace = Path(args.trace)
if not trace.is_file():
sys.exit(f"trace not found: {trace}")
# absolute, because capa is invoked with cwd set to the report's directory
rules = Path(args.rules).resolve()
if not rules.exists():
sys.exit(f"rules path not found: {args.rules}")
extractor = find_extractor(args.extractor)
fd, json_path = tempfile.mkstemp(suffix=".ttd.json")
os.close(fd)
json_path = Path(json_path)
try:
extract_cmd = [str(extractor), str(trace), "-o", str(json_path)]
if args.sample:
extract_cmd += ["--sample", args.sample]
if args.max_calls:
extract_cmd += ["--max-calls", str(args.max_calls)]
if args.with_stack_args:
extract_cmd += ["--with-stack-args"]
if args.win32_index:
extract_cmd += ["--win32-index", args.win32_index]
if args.no_metadata:
extract_cmd += ["--no-metadata"]
if args.max_buffer:
extract_cmd += ["--max-buffer", str(args.max_buffer)]
print(f"[ttd-capa] extracting: {' '.join(extract_cmd)}", file=sys.stderr)
rc = subprocess.call(extract_cmd)
if rc != 0:
sys.exit(f"ttdcapa-extract failed (exit {rc})")
if json_path.stat().st_size == 0:
sys.exit("ttdcapa-extract produced an empty report")
capa_cmd = [
args.python,
"-m",
"capa.main",
"-f",
"ttd",
"-r",
str(rules),
*capa_extra,
str(json_path),
]
print(f"[ttd-capa] running capa: {' '.join(capa_cmd)}", file=sys.stderr)
return subprocess.call(capa_cmd, cwd=str(json_path.parent))
finally:
if args.keep_json:
print(f"[ttd-capa] kept TTD report: {json_path}", file=sys.stderr)
else:
json_path.unlink(missing_ok=True)
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))