fe2a6db786
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>
268 lines
8.5 KiB
Markdown
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.
|