Files
mxaccess/docs/Loopback-Protocol-Findings.md
T
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
Layout:
- src/                    .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
                          MxAsbClient, probes, tests, harnesses. Executable spec.
- design/                 Architectural plan for the Rust port (M0–M6), error
                          model, protocol invariants, risks (R1–R16), adversarial
                          review log (review.md).
- rust/                   Rust workspace. M0 skeleton + M1 codec parity.
                          mxaccess-codec: 215 unit tests + 2 cross-implementation
                          parity tests (byte-identical against .NET reference).
                          Other crates are M0 stubs awaiting M2+.
- captures/               Frida + netsh + pcap evidence per CLAUDE.md
                          ("captures are evidence, not throwaway logs").
- analysis/               Decompiled C# (frida/proxy/decompiled-*),
                          Ghidra exports for native DLLs (`exports/` only —
                          working state at `projects/` and AVEVA's input
                          binaries at `input/` are gitignored).
- docs/                   Reverse-engineering reference docs.
- tools/                  Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
                          Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/      Rust CI: fmt + build + test + clippy on Windows.
- LICENSE                 MIT (Joseph Doherty, 2026).

Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly

Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 06:21:00 -04:00

268 lines
8.5 KiB
Markdown

# Loopback protocol findings
This note captures the first Npcap loopback decode pass for the MXAccess to
LMX/NMX path.
## Capture set
The repeatable capture runner is:
```text
analysis\scripts\run_loopback_capture.ps1
```
It starts `dumpcap` on the Npcap loopback adapter and runs the x86 MXAccess
harness:
```text
src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
```
Focused loopback captures:
| Folder | Scenario |
| --- | --- |
| `captures\013-loopback-subscribe-scalars` | subscribe `TestBool`, `TestInt`, `TestString` |
| `captures\014-loopback-subscribe-array-bracketed` | subscribe `TestStringArray[]` |
| `captures\015-loopback-subscribe-invalid` | subscribe invalid reference |
| `captures\016-loopback-write-test-int-advised` | advised same-value write to `TestInt` |
Per-capture generated files:
| File | Purpose |
| --- | --- |
| `loopback.pcapng` | raw Npcap loopback capture |
| `harness.log` | timestamped MXAccess method and callback log |
| `dcerpc-49704.tsv` | tshark DCE/RPC frame index for the NMX service port |
| `nmx-conversations.tsv` | extracted IPv6 conversations involving port `49704` |
| `nmx-payload-packets.tsv` | payload packet index for the selected `49704` conversation |
| `nmx-stream-*.bin` | reassembled directional payloads for the selected `49704` conversation |
| `tcp-conversations.tsv` | all TCP payload conversations ranked by byte count |
| `tcp-payload-packets.tsv` | payload packet index for the top TCP conversations |
| `tcp-stream-*.bin` | reassembled directional payloads for the top TCP conversations |
Cross-capture summaries:
```text
analysis\scripts\extract_nmx_loopback.py
analysis\scripts\extract_tcp_conversations.py
analysis\scripts\decode_tcp_payload_packets.py
analysis\scripts\decode_mixed_local_stream.py
analysis\scripts\summarize_dcerpc.py
analysis\network\dcerpc-loopback-summary.tsv
analysis\network\write-window-tcp-payloads.tsv
```
## DCE/RPC service path
Wireshark decodes the main `::1:<ephemeral> <-> ::1:49704` traffic as DCE/RPC.
The captures show these DCE/RPC interface UUIDs:
| UUID | Observed use |
| --- | --- |
| `4e0c90df-e39d-4164-a421-ace89484c602` | initial bind context, opnum `0` calls |
| `1981974b-6bf7-46cb-9640-0260bbb551ba` | altered context, main opnums `0`, `2`, `3`, `5` |
The UUIDs were not present as direct keys under the checked COM registry areas:
```text
HKCR\Interface
HKCR\Wow6432Node\Interface
HKCR\CLSID
HKCR\Wow6432Node\CLSID
HKCR\TypeLib
HKCR\Wow6432Node\TypeLib
```
This makes `NmxSvcps.dll` and `WWProxyStub.dll` high-value next targets. They
are likely proxy/stub components, but their interfaces are not exposed through
the simple COM registry lookup above.
## DCE/RPC shape
For good scalar subscribe, good array subscribe, and successful advised write,
the main `49704` DCE/RPC shape is identical:
```text
165 request/response pairs on ctx 1, opnum 3
10 request/response groups on ctx 0 opnum 0, ctx 1 opnum 0, ctx 1 opnum 2
3 request/response pairs on ctx 1, opnum 5
```
The invalid subscribe adds one extra group:
```text
176 request/response pairs on ctx 1, opnum 3
11 request/response groups on ctx 0 opnum 0, ctx 1 opnum 0, ctx 1 opnum 2
3 request/response pairs on ctx 1, opnum 5
```
Interpretation: invalid item resolution does not fail at `AddItem`; it drives
additional NMX/RPC activity during `AdviseSupervisory`, matching the harness
callbacks where invalid names return configuration errors only after advise.
## Write path split
The successful write capture has the standard main `49704` DCE/RPC conversation:
```text
::1:55840 <-> ::1:49704
803 frames, about 84 kB
```
It also has a short additional `49704` DCE/RPC conversation:
```text
::1:49768 <-> ::1:49704
192 frames, about 19 kB
```
The harness write timestamps were:
```text
2026-04-25T05:13:58.0479762Z mx.write.begin
2026-04-25T05:13:58.0489758Z mx.write.end
2026-04-25T05:13:58.2561934Z mx.event.write-complete
```
With the pcap first frame at epoch `1777094027.708322300`, this places the
write call around relative time `10.34s` and the write-complete callback around
relative time `10.55s`.
No DCE/RPC frames on port `49704` occur in the `10.30s` to `10.62s` window.
Instead, the active payload in that exact window is primarily TCP stream `0`:
```text
127.0.0.1:57415 <-> 127.0.0.1:57433
```
That stream is not decoded by Wireshark as DCE/RPC. Its payload has a compact
binary framing pattern with frequent 12-byte control messages and small
length-prefixed payloads. Example payload prefixes around the write window
include little-endian values such as:
```text
1a 00 00 00 ...
16 00 00 00 ...
22 00 00 00 ...
1e 00 00 00 ...
3f 00 00 00 ...
```
Interpretation: the native implementation probably uses DCE/RPC for service
activation/session coordination and a separate local binary channel for at
least part of the advised update/write-complete path. The managed replacement
must account for both layers.
The stream has been extracted as:
```text
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57415-to-127_0_0_1_57433.bin
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57433-to-127_0_0_1_57415.bin
```
The stream is now decoded with packet-boundary and mixed-record helpers:
```text
analysis\scripts\decode_tcp_payload_packets.py
analysis\scripts\decode_mixed_local_stream.py
analysis\scripts\analyze_write_window.py
analysis\scripts\diff_write_window_records.py
```
The mixed-record model is:
- 12-byte control records: `int32 code_or_status`, `int32 token_low`,
`int32 token_high`.
- Data records: `uint32 body_length`, followed by the data body.
- A positive control value can announce one or more following data records.
- `-1` appears as acknowledgement/status.
- `-2` appears as a bidirectional status/control marker around write windows.
Usable value-change write captures added after this decode pass:
| Folder | Result |
| --- | --- |
| `captures\017-loopback-write-test-int-100` | `TestInt` changed `99 -> 100` |
| `captures\020-loopback-write-test-int-102` | `TestInt` changed `101 -> 102` |
| `captures\021-loopback-write-test-int-sequence-103-105` | same-session writes `103`, `104`, `105` |
The detailed COM contract and managed-client implication note is:
```text
docs\NMX-COM-Contracts.md
```
## Same-session write sequence
`captures\021-loopback-write-test-int-sequence-103-105` writes three int values
through one registered/advised MXAccess session:
```text
103 at 2026-04-25T05:53:06.9746508Z
104 at 2026-04-25T05:53:07.6963047Z
105 at 2026-04-25T05:53:08.4180133Z
```
Each write produced a good data-change callback followed by a good
write-complete callback. The top local payload stream remained:
```text
127.0.0.1:57415 <-> 127.0.0.1:57433
```
The analyzer output is:
```text
captures\021-loopback-write-test-int-sequence-103-105\write-window-mixed-records.tsv
analysis\network\write-window-body-diff-021-w0-vs-w1.tsv
analysis\network\write-window-body-diff-021-w1-vs-w2.tsv
```
Within the `-0.10s` to `+0.12s` write-complete windows, the repeated records are
the same families already seen in the single-write captures:
```text
54 8f 63 40 e2 5e 31 40 ... 26-byte data body
1c 21 18 d0 c4 6f 33 bb ... 34-byte data body
98 04 33 cb 0c b4 7c 38 ... 67-byte data body
44 6b 99 d8 ec 1b bd b5 ... 52-byte data body
55 ce ff 62 b2 1b 3a 50 ... 30-byte data body
```
The raw int values `103`, `104`, and `105` were not isolated from the pcap-only
mixed stream decode. The most consistent byte changes in that layer are
counter-like fields at offset `14` in the 26-byte and 30-byte data bodies,
offset `0` in the 22-byte and 26-byte response bodies, and related token fields
in adjacent 12-byte controls. Those fields advance with the local message
sequence and should not be treated as the application value.
The application value was later isolated one layer higher with Frida hooks
placed using headless Ghidra RVAs. See:
```text
docs\Ghidra-Headless-Analysis.md
captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv
```
## Plaintext result
The selected `49704` stream binaries did not contain the test tag names in
ASCII or UTF-16LE. The captured protocol is therefore not a simple plaintext
tag-reference transport.
## Current reconstruction hypothesis
The implementation path is:
```text
ArchestrA.MXAccess.dll
-> LmxProxy.dll COM in-proc server
-> local DCE/RPC to NmxSvc.exe on ::1:49704
-> local binary channels for request/callback data
-> LMX/NMX runtime and Galaxy-derived security/type metadata
```
The next useful work is to decode the proxy/stub interfaces and the local
binary stream structure, then tie decoded calls back to MXAccess harness events.