Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
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>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user