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

8.5 KiB

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:

analysis\scripts\run_loopback_capture.ps1

It starts dumpcap on the Npcap loopback adapter and runs the x86 MXAccess harness:

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:

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:

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:

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:

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:

::1:55840 <-> ::1:49704
803 frames, about 84 kB

It also has a short additional 49704 DCE/RPC conversation:

::1:49768 <-> ::1:49704
192 frames, about 19 kB

The harness write timestamps were:

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:

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:

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:

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:

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:

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:

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:

127.0.0.1:57415 <-> 127.0.0.1:57433

The analyzer output is:

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:

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:

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:

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.