53 Commits
Author SHA1 Message Date
Jonathan Beierle 62c36a2c9a Create build.yml 2026-08-04 14:52:53 -05:00
Jonathan Beierle bf80e4a71f Delete .github/workflows 2026-08-04 14:52:23 -05:00
Jonathan Beierle e4477ec15e Create workflows 2026-08-04 14:52:04 -05:00
HullaBrian 2750eba80c Update CAPA Fork Version v1.0.1 2026-08-01 21:44:25 -05:00
HullaBrian 61d08f628a Clean up README.md 2026-08-01 21:37:28 -05:00
HullaBrian 5563641071 Remove Github build
Doesn't play nice with v145 build tools
2026-08-01 20:55:33 -05:00
HullaBrian ac6151331b Fix crash when no index file present 2026-08-01 20:52:49 -05:00
Jonathan Beierle e66fb999fc Merge pull request #1 from Vector35/master
Merge Updates/Improvements from Vector35/ttd-capa
2026-08-01 19:51:32 -05:00
Xusheng 2888975ff6 Fold the extractor's command-line docs into the format reference
One document now covers both what ttdcapa-extract does and what it writes,
since anyone reading either needs the other. Renamed to match the tool it
documents, which also reads better in the release artifact next to the
executable.

Brings the README's prerequisites and DLL instructions in line with the
toolset and --ttd-dlls changes.
2026-07-30 15:06:07 -04:00
Xusheng 2d98e5e58f Take the TTD DLL location on the command line, and build in CI
TTDReplay.dll and TTDReplayCPU.dll are Microsoft's, shipped with WinDbg and
not ours to redistribute, so requiring them beside the executable made the
extractor awkward to ship. --ttd-dlls points at wherever WinDbg already put
them. TTDReplay.dll is delay-loaded so this can take effect at runtime;
without the flag the normal search order applies, which finds copies beside
the executable exactly as before. Either way the load is explicit, so a
missing engine is a clear message rather than the structured exception a
failed delay-load raises.

The toolset is no longer pinned to v145, which only VS2026 has -- it
defaults to v143 and can be overridden.

CI builds Release x64 on windows-2022, smoke-tests --dump-sig, and uploads
ttdcapa-extract.exe, win32-index.bin and docs/ttdb-format.md. Tags publish
the same set as a release. The replay DLLs are deliberately absent from it.
2026-07-30 15:06:07 -04:00
Xusheng fc4c2ef00a Document the .ttdb format 2026-07-30 15:06:07 -04:00
Xusheng 75cde9091d Put captured buffer contents in the search haystack
A buffer parameter contributed only its pointer to the searchable text --
'lpbuffer=0x4fdf54' -- so searching for a string a program actually wrote
or read found nothing, even though the bytes were sitting in the report.
Append the printable runs from each captured buffer instead, bounded at 96
characters so a 64 KiB buffer cannot inflate every haystack.

The WriteFile that prints hello now reads
  kernelbase!writefile hfile=0xc lpbuffer=0x4fdf54 hello, world! ...
and matches a search for it. No measurable size cost: buffer parameters are
well under 1% of calls.
2026-07-30 15:06:07 -04:00
Xusheng c8c4b56b52 Record each call's return address
The replay engine already hands the fall-through address to the call
callback -- it is what we match the return against -- so recording it is a
single store and no extra memory reads. Measured on the 3.4M-call netac
trace: 34.5s and 32.0s against a 35.0s baseline, i.e. inside run-to-run
noise, with the report write unchanged at ~3.3s. The binary report grows
8 bytes per call, 585MB to 611MB.

Worth having because it names the call site: user32!MsgWaitForMultipleObjectsEx
in that trace returns to 0x00f91edc, in the sample's own range, where the
ntdll!_allmul calls return into ntdll itself. That is the difference between
a call the sample made and one made on its behalf.

Binary format version goes to 2; a reader checking the version will ask for
a re-extract rather than misread the 40-byte records.
2026-07-30 15:06:07 -04:00
Xusheng 64afd8c401 Report how long writing a report took
Measured: the binary write is 3.6s against JSON's ~56s on a 3.4M-call
trace. Emitted both readably and as a [timing] line so the sidebar can put
it in front of the user, since the cost of the format was the thing worth
knowing.
2026-07-30 15:06:07 -04:00
Xusheng bdf0e048c7 Add a memory-mappable binary report format
JSON is the right output for capa and the wrong one for a tool that loads a
whole report and browses it. Measured on a 3.4M-call trace: of an 86s run,
the ~30s sweep was dwarfed by ~56s of JSON serialisation, and loading the
654MB result back cost 16.6s -- 8.9s of DOM parse plus 7.6s allocating
per-call objects -- of which only 138ms was disk I/O. Two thirds of the
wall clock was the format, and none of it was inherent.

-b/--binary writes the same data in a layout designed to be mapped and read
in place: fixed-size call records indexable by row, positions as two
integers rather than "45905:15A5", raw bytes rather than hex, a
deduplicated string table, and a prebuilt lowercased search haystack per
call so a filter needs nothing built first. -o still writes JSON; either or
both can be given.

Result: the same extraction takes 44s instead of 86s, and the file is
585MB. Loading is a memory map.

The string table is the standout -- every module and API name across 3.4M
calls fits in 100KB. The remaining bulk is parameter records (218MB) and
haystacks (237MB), both of which have obvious trims left if size starts to
matter more than time.
2026-07-30 15:06:07 -04:00
Xusheng e716a83bfd Report sweep progress and allow the sweep to be cancelled
Two new flags, both for a UI driving this as a child process; interactive
use is unchanged.

--progress emits "[progress] <percent> <calls>" on stderr a few times a
second, plus "[phase] sweep" / "[phase] write" markers. The percentage
comes from the callback thread's position against the trace's last
sequence. Positions from different threads are not perfectly ordered, so
roughly 4% of samples read behind the previous one; they are clamped
forward rather than allowed to walk a progress bar backwards. The phases
matter because the sweep is only part of the wall clock -- on a 3.4M-call
trace it takes ~30s while serialising the 655MB report takes comparable
time, so a caller tracking only the sweep would sit at 100% looking wedged.

--cancel-on-stdin starts a watcher thread that calls InterruptReplay when
it reads "cancel", which the SDK documents as safe from any thread. The
report is then written as usual: everything recorded so far is already in
g_report, so a cancelled run yields a complete, valid report of a shorter
prefix of the trace. Verified stopping a netac sweep at 1.54M of 3.38M
calls, 317k of them with decoded parameters.
2026-07-30 15:06:07 -04:00
Xusheng 7954276ad8 Raise the default buffer capture limit to 64 KiB
Measured on a 654 MB report of a 3.4M-call trace: 23k buffer parameters
totalling 1.1 MB, so buffers are nowhere near the dominant cost and the
old 256-byte cap was buying almost nothing. It was, however, cutting off
2014 buffers across 19 APIs -- ReadFile, WriteFile, RegGetValueW,
GetFontData and DeviceIoControl among them -- right where the interesting
content starts.

64 KiB covers typical socket and file reads whole.
2026-07-30 15:06:07 -04:00
Xusheng a5c5d7f240 Record a buffer's real length when --max-buffer cuts it short
A 4096-byte WriteFile showed up in the report as a plain 256-byte buffer,
with nothing to say the other 3840 bytes had been dropped. Emit
bytes_total alongside bytes whenever the cap actually bit, so a consumer
can distinguish a short buffer from the head of a long one.

Only when it really was cut short: a character buffer trimmed at its NUL
is complete, since the count parameter is the caller's capacity rather
than the string's length. Claiming truncation there would send someone
hunting for data that never existed.
2026-07-30 15:06:07 -04:00
Xusheng 8ca0ccdea7 Read pointer-sized pointees at the guest's width on x86
Validated against a real 32-bit trace: NtOpenFile's FileHandle out-param
came back as 0x820077a1cf10, an 8-byte read of a 4-byte HANDLE that
spliced the following dword into the value. The index measures HANDLE at
its x64 width, so on a 32-bit guest an 8-byte pointee is almost always a
pointer-sized type and should be read as 4.

A genuine 64-bit pointee now loses its high half instead, which is still
better than a value mixed with unrelated memory. Distinguishing the two
needs the same index change the README already describes.

x64 output remains byte-identical on the reference trace.
2026-07-30 15:06:07 -04:00
Xusheng 80c39ade4f Support x86 traces, including WoW64
Bitness is decided per call from the PE header of the module owning the
call target, not once per trace. SystemInfo.ProcessorArchitecture comes
from GetSystemInfo() on the recording machine, so it reports AMD64 for a
32-bit process recorded on an x64 box -- it never described the guest.
A WoW64 process runs both widths at once, so per-module is also the only
answer that can be right.

- ttd_pe_utils parses PE32 alongside PE32+, and reports each image's
  bitness. Previously readNTHeaders rejected anything that was not
  PE32+, so on a 32-bit trace the export map came back empty and no
  calls were recorded at all.
- abi_x64 becomes abi, parameterised over the calling convention. x86
  passes everything on the stack, so each parameter's offset depends on
  the sizes of the ones before it; computeStackOffsets recovers those.
  __stdcall and __cdecl are indistinguishable from the callee's entry,
  so one layout serves both.
- Pointer-width dereferences (T** hops, untyped pointees) follow the
  guest's width rather than a hardcoded 8.
- decodeArgs now declines signatures it cannot lay out, and the caller
  falls back to the heuristic. That covers pointer-sized scalars, whose
  x86 footprint the index cannot express, and by-value aggregates. See
  the README for what this costs and how to fix it properly.
- The x86 heuristic fallback reads the stack, since nothing arrives in
  registers there.
- --dump-sig shows the computed x86 offsets, so a layout can be checked
  without a trace.

Also drops a duplicated thread-list collection that made every trace
report its threads twice.

x64 output is unchanged: byte-identical call records on a 4741-call
reference trace. The x86 path has not yet been run against a real
32-bit trace.
2026-07-30 15:06:07 -04:00
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
Jonathan Beierle f9db02e59e Update README.md 2026-06-18 20:10:33 -05:00
Jonathan Beierle e6bb901599 Update README.md 2026-06-18 20:08:30 -05:00
Jonathan Beierle 40842e1c50 Tiny improvements 2026-06-18 20:04:02 -05:00
Jonathan Beierle a9ac90da8a Update ttd-timeline.py 2026-06-18 19:57:10 -05:00
Jonathan Beierle 2c642a7ed8 Update README.md 2026-06-18 19:55:49 -05:00
Jonathan Beierle 0b8a582cf1 Update README.md 2026-06-18 19:54:39 -05:00
Jonathan Beierle 4d107384b5 Update README.md 2026-06-18 19:51:48 -05:00
Jonathan Beierle 6cb97d6cb1 Update README.md 2026-06-18 19:48:23 -05:00
Jonathan Beierle a044cd9b42 Update README.md 2026-06-04 13:17:37 -05:00
Jonathan Beierle 9f0c73dced Update README.md 2026-06-04 13:15:10 -05:00
Jonathan Beierle 2aca37a548 Create LICENSE 2026-06-02 21:40:39 -05:00
Jonathan Beierle affc14b111 Update ttd-capa.py 2026-06-02 20:52:06 -05:00
Jonathan Beierle 4d3cbeda7d Update README.md v1.0.0 2026-06-02 20:47:08 -05:00
Jonathan Beierle aed6399248 Updated images 2026-06-02 20:41:21 -05:00
Jonathan Beierle 1134102cef Added images 2026-06-02 20:36:49 -05:00
Jonathan Beierle d80047671a Formatting changes 2026-06-02 20:23:23 -05:00
Jonathan Beierle d1abd96a70 Fix wrapper script issues 2026-06-02 20:09:57 -05:00
Jonathan Beierle c6e1f181b4 Update README.md 2026-06-02 19:27:16 -05:00
Jonathan Beierle bd35ce53bd Update README.md 2026-06-02 19:23:12 -05:00
Jonathan Beierle d6f3d4f733 Update README.md 2026-06-02 19:21:19 -05:00
Jonathan Beierle 29897c03e0 Update README.md 2026-06-02 19:12:50 -05:00
Jonathan Beierle 04190f8f0e Update README.md 2026-06-02 17:45:35 -05:00
Jonathan Beierle 858118b9ef Script updates 2026-05-31 21:15:50 -05:00
Jonathan Beierle 3608bf17c1 Update capa 2026-05-31 21:05:26 -05:00
Jonathan Beierle c8f0d2e9ce Make TTD CAPA fork a submodule 2026-05-31 20:48:24 -05:00
Jonathan Beierle ad58a8f0d9 Script modifications 2026-05-31 20:26:32 -05:00
Jonathan Beierle 5d5f7791ec Project restructuring 2026-05-31 20:22:20 -05:00
Jonathan Beierle 95906d75e4 Project restructuring 2026-05-31 20:04:07 -05:00
Jonathan Beierle f6f75539a6 Timeline script and documentation 2026-05-29 18:59:26 -05:00