Files
mxaccess/docs/Capture-Run-2026-04-25.md
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

54 KiB

MXAccess capture run - 2026-04-25

This run used the primary installed MXAccess interop assembly:

C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll

The harness is in src\MxTraceHarness and builds as net481 x86 because MXAccess activates the 32-bit LMXProxy.LMXProxyServer COM server.

Galaxy repository inputs

The tag selector is:

analysis\sql\select_capture_tags.sql

It connects to the Galaxy repository described in C:\Users\dohertj2\Desktop\lmxopcua\gr\connectioninfo.md:

Server=localhost; Database=ZB; Integrated Security=SSPI

The output from this run is saved at:

analysis\db\capture-tag-candidates.tsv

Selected test tags:

Runtime reference Type Security Notes
TestChildObject.TestBool Boolean Operate / 1 scalar read/subscription
TestChildObject.TestInt Integer Operate / 1 scalar read/subscription/write
TestChildObject.TestString String Operate / 1 scalar read/subscription
TestChildObject.TestStringArray[] String array, length 10 Operate / 1 array read/subscription
NoSuchObject_999.NoSuchAttr invalid n/a negative resolution path

Important naming result: array attributes must be passed to MXAccess with the [] suffix. TestChildObject.TestStringArray produced MxCategoryConfigurationError, detail 1003; TestChildObject.TestStringArray[] returned System.String[] with quality 192.

Harness behavior captured

Built command:

dotnet build src\MxTraceHarness\MxTraceHarness.csproj -c Release

Executable:

src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe

Scenarios now implemented:

Scenario High-level sequence
register Register, wait, Unregister
add-remove Register, AddItem, wait, RemoveItem, Unregister
subscribe Register, AddItem, AdviseSupervisory, wait callbacks, UnAdvise, RemoveItem, Unregister
write Register, AddItem, AdviseSupervisory, wait initial callback, Write, wait OnWriteComplete, cleanup

Write precondition found in this run:

  • Write immediately throws E_INVALIDARG if called after AddItem only.
  • Write succeeds after AdviseSupervisory has established the item connection.
  • The fourth Write argument should follow the existing bridge convention: pass the Galaxy security_classification value for the attribute. For TestChildObject.TestInt, that value is 1.

Successful same-value write:

captures\010-write-test-int-advised-same-value\harness.log

Observed sequence:

mx.event.data-change: TestChildObject.TestInt = 99, quality 192
mx.write.begin: value 99, UserId/security classification 1
mx.write.end
mx.event.write-complete: MxCategoryOk, MxSourceRespondingAutomationObject, detail 0

Captures produced

Every capture folder contains harness.log, netsh.etl, network.pcapng, and tool stdout/stderr files unless noted.

Folder Result
captures\001-register register/unregister success
captures\002-add-remove-scalar scalar AddItem/RemoveItem success
captures\003-subscribe-scalars bool/int/string initial data changes, quality 192
captures\004-subscribe-array-runtime-name array without [] returns configuration error detail 1003
captures\005-subscribe-array-bracketed-name array with [] returns System.String[], length 10, quality 192
captures\006-add-invalid invalid AddItem still returns an item handle; no validation until advise
captures\007-subscribe-invalid invalid subscribe returns configuration error detail 6
captures\008-write-test-int-same-value write without advise throws E_INVALIDARG using fourth arg 0
captures\009-write-test-int-same-value-security-1 write without advise still throws E_INVALIDARG using fourth arg 1
captures\010-write-test-int-advised-same-value advised same-value write succeeds
captures\011-pktmon-subscribe-scalar-loopback-probe pktmon probe; still did not expose ::1 NMX loopback traffic

Converted pcap summaries:

analysis\network\pcap-summary.txt
captures\011-pktmon-subscribe-scalar-loopback-probe\pcap-summary.txt

Network capture status

netsh trace and pktmon both captured external/background traffic and can be converted to pcapng. They did not expose the local loopback session that NmxSvc.exe keeps open on IPv6 loopback.

Current NmxSvc.exe socket evidence during the run:

TCP 10.100.0.48:5026 LISTEN
UDP 10.100.0.48:5026
TCP ::1:49829 <-> ::1:49704 ESTABLISHED

Interpretation: the payload needed for a native managed client is probably on the local ::1 connection between the 32-bit MXAccess stack and NmxSvc.exe, not on the physical NIC path. Capturing that requires one of:

  • working Npcap loopback capture,
  • API Monitor / debugger tracing at Winsock or COM method boundaries,
  • ETW provider capture if the AVEVA NMX components emit enough payload detail,
  • direct lower-level COM tracing around INmx4.PutRequest2 / GetResponse2 and IDataClient methods.

Wireshark 4.6.4 and etl2pcapng 1.11.0 were installed.

Update after interactive install: Npcap 1.87 is now installed and working. dumpcap -D lists \Device\NPF_Loopback (Adapter for loopback traffic capture). The verification capture is:

captures\012-npcap-loopback-subscribe-scalar\

Files:

loopback.pcapng
harness.log
nmx-loopback-frames.tsv

The focused loopback capture includes the active MXAccess/NMX conversation:

::1:59335 <-> ::1:49704
803 frames, about 84 kB, duration 7.6793 s

It also saw the pre-existing service connection:

::1:49829 <-> ::1:49704
4 frames, about 526 bytes

This confirms Npcap loopback capture is the correct mechanism for collecting the actual local NMX payloads needed for protocol reconstruction.

Npcap loopback protocol captures

The repeatable runner is:

analysis\scripts\run_loopback_capture.ps1

Focused captures completed after Npcap verification:

Folder Result
captures\013-loopback-subscribe-scalars good bool/int/string subscribe
captures\014-loopback-subscribe-array-bracketed good string-array subscribe using [] suffix
captures\015-loopback-subscribe-invalid invalid reference subscribe
captures\016-loopback-write-test-int-advised advised same-value write succeeds

Extraction and summary helpers:

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\analyze_write_window.py
analysis\scripts\diff_write_window_records.py
analysis\scripts\run_frida_mx_trace.ps1
analysis\scripts\extract_frida_trace.py
analysis\scripts\summarize_dcerpc.py
analysis\network\dcerpc-loopback-summary.tsv
analysis\network\write-window-tcp-payloads.tsv

Important packet result: the ::1:<ephemeral> <-> ::1:49704 traffic is DCE/RPC, not a simple tag-string socket protocol. The observed interface UUIDs are:

4e0c90df-e39d-4164-a421-ace89484c602
1981974b-6bf7-46cb-9640-0260bbb551ba

Those UUIDs were not found as direct keys under the checked COM registry interface, CLSID, or TypeLib areas. The likely decode targets are therefore the native proxy/stub binaries:

C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvcps.dll
C:\Program Files (x86)\ArchestrA\Framework\Bin\WWProxyStub.dll

Good scalar subscribe, good array subscribe, and successful advised write share the same main 49704 DCE/RPC shape: 165 ctx-1 opnum-3 request/response pairs, 10 ctx-0/ctx-1 setup groups, and 3 ctx-1 opnum-5 pairs. The invalid subscribe adds one more setup group and 11 more opnum-3 pairs, matching the observed behavior that invalid references fail during advise/resolution, not AddItem.

The successful write has a second important observation. The harness write call occurred at relative time about 10.34s, and the write-complete callback at about 10.55s. No 49704 DCE/RPC frames appear in that exact window. The active payload is primarily on:

127.0.0.1:57415 <-> 127.0.0.1:57433

That stream is compact binary traffic with small control messages and apparent little-endian length prefixes. This means the managed replacement likely needs to reproduce both the DCE/RPC coordination path and a separate local binary callback/request channel.

The write-window stream is 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 has a mixed-record framing:

  • 12-byte control records: int32 code_or_status, int32 token_low, int32 token_high.
  • Data records: uint32 body_length, followed by the data body.
  • Positive control values can announce one or more following data records.
  • -1 appears as a normal acknowledgement/status control.
  • -2 appears around write-window status/control exchanges.

Additional differential write captures:

Folder Result
captures\017-loopback-write-test-int-100 value changed 99 -> 100; pcap usable
captures\018-loopback-write-test-int-101 write succeeded, but pcap is header-only and should not be used
captures\019-loopback-write-test-int-101-rerun pcap usable, but same-value write because value was already 101
captures\020-loopback-write-test-int-102 value changed 101 -> 102; pcap usable
captures\021-loopback-write-test-int-sequence-103-105 same-session sequence changed 102 -> 103 -> 104 -> 105; pcap usable
captures\022-frida-write-test-int-sequence-106-108 Frida hooks validated Ghidra RVAs; first buffer dump helper was incomplete
captures\023-frida-write-test-int-sequence-109-111 Frida hooks captured raw write values in LmxProxy and NmxAdptr buffers
captures\024-frida-write-test-bool-sequence bool write matrix: VT_BOOL, 37-byte PutRequest, value slot at offset 18
captures\025-frida-write-test-float-sequence float write matrix: 40-byte PutRequest, float32 at offset 18
captures\026-frida-write-test-double-sequence double write matrix: 44-byte PutRequest, float64 at offset 18
captures\027-frida-write-test-string-sequence string write matrix: UTF-16LE payload at offset 26
captures\028-frida-write-test-datetime-sequence datetime write matrix: outbound UTF-16LE display string; callback FILETIME
captures\029-frida-write-test-int-array int array write succeeds; packed int32 values at PutRequest offset 28
captures\030-frida-write-test-bool-array bool array write succeeds, but alternating requested values returned as paired values; needs follow-up
captures\031-frida-write-test-float-array float array write succeeds; packed float32 values at PutRequest offset 28
captures\032-frida-write-test-double-array double array write succeeds; packed float64 values at PutRequest offset 28
captures\033-frida-write-test-string-array string array write succeeds; per-element UTF-16LE records
captures\034-frida-write-test-datetime-array datetime array write succeeds, but body dump was truncated by the old 256-byte Frida cap
captures\035-frida-write-test-datetime-array-full datetime array rerun with full 4096-byte Frida dump cap
captures\036-frida-write-secured-test-int WriteSecured against Operate int rejected with 0x80004021 before value-bearing body
captures\037-frida-write-secured2-test-int WriteSecured2 against Operate int rejected with E_INVALIDARG before value-bearing body
captures\038-frida-write-secured-protectedvalue WriteSecured against real SecuredWrite bool rejected with 0x80004021
captures\039-frida-write-secured-verified-protectedvalue1 WriteSecured against real VerifiedWrite bool rejected with 0x80004021
captures\040-frida-write-normal-secured-protectedvalue normal Write with user/security 2 succeeds against SecuredWrite bool
captures\041-frida-write-normal-verified-protectedvalue1 normal Write with user/security 3 succeeds against VerifiedWrite bool
captures\042-frida-write2-test-int-timestamp Write2 succeeds; int at offset 18, FILETIME at offset 24
captures\043-frida-loopback-write-test-int-115 combined Frida plus Npcap loopback capture; exact adapter bodies are not present verbatim in TCP streams
captures\044-frida-loopback-write-test-int-123456789 combined Frida plus Npcap loopback capture with distinctive value; raw scalar also absent from full pcap payload scan

The same-session sequence was captured with:

--scenario=write --tag=TestChildObject.TestInt --type=int --values=103,104,105 --user-id=1 --write-delay-ms=1000 --write-interval-ms=700 --duration=5

Generated write-window analyses:

captures\017-loopback-write-test-int-100\write-window-mixed-records.tsv
captures\020-loopback-write-test-int-102\write-window-mixed-records.tsv
captures\021-loopback-write-test-int-sequence-103-105\write-window-mixed-records.tsv
analysis\network\write-window-body-diff-017-vs-020.tsv
analysis\network\write-window-body-diff-021-w0-vs-w1.tsv
analysis\network\write-window-body-diff-021-w1-vs-w2.tsv

The sequence capture is important because it keeps one MXAccess session alive while changing only the requested int value. The decoded local-stream records around write-complete still do not contain 103, 104, or 105 as plain little-endian int32 payloads. The bytes that move consistently in the visible records are request/session counters and checksummed or opaque body fields. The pcap-only mixed-stream layer did not isolate the scalar value. Headless Ghidra plus Frida then located it one native layer higher:

CLMXProxyServer::Write variant A RVA 0x12c0c
CNmxAdapter::PutRequest RVA 0x15169
CNmxAdapter::TransferData RVA 0x10996
CNmxAdapter::ProcessDataReceived RVA 0x112da

In captures\023-frida-write-test-int-sequence-109-111, the raw scalar values are visible as little-endian int32:

Function Body size Value offsets
CLMXProxyServer::Write call args args[5] = 109, 110, 111
CNmxAdapter::PutRequest 40 offset 18
CNmxAdapter::TransferData 86 offset 64
CNmxAdapter::ProcessDataReceived 88 offset 84

The later Frida captures generalize the body format:

Type PutRequest TransferData Callback/update Encoding
bool size 37, offset 18 size 83, offset 64 size 85, offset 84 VT_BOOL; true ff ff ff 00 in write body and ff in data-change body; false 00 ff ff 00 and 00
int size 40, offset 18 size 86, offset 64 size 88, offset 84 little-endian int32
float size 40, offset 18 size 86, offset 64 size 88, offset 84 little-endian float32
double size 44, offset 18 size 90, offset 64 size 92, offset 84 little-endian float64
string size 58 or 60, offset 26 size 104 or 106, offset 72 size 106 or 108, offset 92 UTF-16LE
datetime size 86, offset 26 size 132, offset 72 size 98, offset 88 outbound UTF-16LE display string; callback FILETIME

Full matrix:

analysis\frida\write-body-matrix.tsv

Array matrix:

analysis\frida\write-array-body-matrix.tsv

Write-mode matrix:

analysis\frida\write-mode-matrix.tsv

Frida-to-TCP mapper:

analysis\scripts\run_frida_loopback_capture.ps1
analysis\scripts\map_frida_to_tcp.py
analysis\scripts\parse_dcerpc_streams.py
captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv
captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv

Array body summary:

Type PutRequest TransferData Callback/update Encoding
int[] size 86, first value offset 28 size 132, first value offset 74 size 134, first value offset 94 descriptor kind 0x42, count 10, width 4, packed int32
bool[] size 66, first value offset 28 size 112, first value offset 74 size 114, first value offset 94 descriptor kind 0x41, count 10, width 2; observed value pairing needs follow-up
float[] size 86, first value offset 28 size 132, first value offset 74 size 134, first value offset 94 descriptor kind 0x43, count 10, width 4, packed float32
double[] size 126, first value offset 28 size 172, first value offset 74 size 174, first value offset 94 descriptor kind 0x44, count 10, width 8, packed float64
string[] size 256, first string bytes at 41 size 302, first string bytes at 87 size 304, first string bytes at 107 descriptor kind 0x45, per-element UTF-16LE variable records
datetime[] size 596, first string bytes at 41 size 642, first string bytes at 87 size 214, first FILETIME at 94 outbound per-element display strings; callback/update FILETIME sequence

Secured/verified write result: the public WriteSecured and WriteSecured2 methods are exposed by COM but did not produce value-bearing requests in these captures. Actual SecuredWrite and VerifiedWrite attributes accepted normal Write calls when the fourth argument matched the Galaxy security classification (2 or 3).

Write2 result: the timestamped int write still uses a 40-byte body with the value at offset 18. A FILETIME timestamp is embedded at PutRequest offset 24, TransferData offset 70, and callback/update offset 75.

Transport correlation result: in capture 043, the raw int32 value 115 appears in TCP streams, but the exact Frida PutRequest, TransferData, and callback bodies do not. The ::1:49704 hits around the scalar align with DCE/RPC metadata/call IDs rather than the native adapter body.

Capture 044 uses 123456789 to avoid that ambiguity. The raw scalar is not found in a full pcap payload scan, in parsed ::1:49704 DCE/RPC stubs, or in the mixed local stream. This confirms the wire representation is transformed or encoded before TCP.

Detailed notes are in:

docs\Loopback-Protocol-Findings.md
docs\NMX-COM-Contracts.md

Protocol facts established

  • Register returns session handle 1 for these short-lived runs.
  • AddItem allocates local item handles but does not prove the reference exists.
  • AdviseSupervisory triggers item resolution and initial data/status callback.
  • Good scalar reads return:
    • MXSTATUS_PROXY.success = -1
    • category = MxCategoryOk
    • detectedBy = MxSourceRequestingLmx
    • detail = 0
    • quality 192
  • Invalid subscribe returns:
    • value null
    • quality 0
    • category = MxCategoryConfigurationError
    • detectedBy = MxSourceRequestingLmx
    • detail = 6
  • Array name missing [] returns:
    • value null
    • quality 0
    • category = MxCategoryConfigurationError
    • detectedBy = MxSourceRespondingAutomationObject
    • detail = 1003
  • Successful write completion returns:
    • success = -1
    • category = MxCategoryOk
    • detectedBy = MxSourceRespondingAutomationObject
    • detail = 0

Later targeted non-core type captures:

  • 062-frida-subscribe-intl-shortdesc: TestChildObject.ShortDesc (InternationalizedString) resolves and advises. A callback record used normal string wire kind 0x05 with compact empty payload 04 00 00 00.
  • 063-frida-subscribe-elapsed-time-deadband: TestMachine_001.TestAlarm001.Alarm.TimeDeadband (ElapsedTime) resolves and advises. A callback record used wire kind 0x07 with four-byte zero payload.
  • 064-frida-subscribe-intl-percent and 065-frida-subscribe-intl-mb: non-empty internationalized-string references resolved and advised but did not emit value callbacks during the capture window.
  • 066 through 069: timestamped Write2 captures for bool, float, double, and string. Float, double, and string use the same fixed/variable timestamped suffix shape as int. Timestamped bool uses wire kind 0x01, one value byte, then the normal timestamp suffix.
  • 070 through 072: timestamped Write2 captures for int[], bool[], and string[]. These match the existing managed array timestamp encoder. The bool-array capture preserves the earlier MXAccess marshaling behavior where requested alternating bools arrive as paired true/false values.
  • 073, 074, and 076: timestamped Write2 captures for float[], double[], and datetime[]. These match the managed array timestamp encoder. The initial 075 datetime[] run used the wrong tag name and did not emit the expected 0x37 write body; 076 is the valid capture for TestDateTimeArray[].
  • mxaccess-suspend-* / mxaccess-activate-*: Suspend and Activate throw 0x80070057 before AdviseSupervisory. The advised variants succeed on scan-state targets: suspend returns pending/requesting-LMX, activate returns ok/requesting-LMX. Frida captures 077 and 078 show no additional NMX request body after the public method call, so this behavior appears local to the MXAccess/LMX layer once the item has been advised.
  • 079-frida-add-buffered-advise-testint: AddBufferedItem("TestInt", "TestChildObject") followed by AdviseSupervisory sends an item-control 0x10 reference-registration body for TestInt.property(buffer) in context TestChildObject. It does not send a normal 0x1f advise body for the buffered handle.
  • 080-frida-buffered-external-write-testint: while the buffered handle is advised, adding normal writer handles in the same session sends normal reference-registration bodies, but no OnBufferedDataChange payload was observed. This capture supplied the stable normal and buffered 0x10 registration bodies, plus the matching 0x11 registration-result frames, used by NmxReferenceRegistrationMessage and NmxReferenceRegistrationResultMessage tests.
  • 085-frida-subscribe-property-buffer and 086-frida-write-property-buffer: direct literal TestChildObject.TestInt.property(buffer) add/advise follows normal AdviseSupervisory, not AddBufferedItem; the item-control 0x10 registration uses the full literal item with an empty context and receives a 0x11 registration result containing the same Base Runtime Object internal error text. No OnBufferedDataChange event was fired.
  • 087-frida-authenticate-administrator-empty and 088-frida-authenticate-invalid-empty: password-redacted auth hook shows CLMXProxyServer.AuthenticateUser returning S_OK and user ID 1 for both Administrator and an invalid user name with password length 0. The only NMX traffic observed after the auth call is the normal unregister-time system reference cleanup, not an auth request body.
  • 111-frida-write-secured-auth-protectedvalue and 112-frida-write-secured-auth-verified-protectedvalue1: pre-authenticating with AuthenticateUser returns user handle 1, but public WriteSecured still returns 0x80004021 before any value-bearing NMX write body is emitted.
  • 113-frida-write-secured2-auth-protectedvalue, 114-frida-write-secured2-auth-protectedvalue-false, 115-frida-write-secured2-auth-protectedvalue-true, and 116-frida-write-secured2-auth-verified-protectedvalue1: authenticated public WriteSecured2 succeeds and emits NMX command 0x38 over normal transfer kind Write (3). The decoded boolean body layout is: command and 14-byte reference-handle projection, boolean wire kind 0x01, a four-byte boolean scalar whose first byte carries the value, FILETIME timestamp, a 16-byte current-user authenticator token, UTF-16LE client-name byte length, null-terminated UTF-16LE client name, a 16-byte verifier token, 0xffff, client token, and write index. The verifier token is all zeros when VerifierUserId is 0 and equals the authenticated token when the verifier handle is 1.
  • 117-frida-write-secured2-auth-testint: after pre-authentication, public WriteSecured2 also succeeds against TestChildObject.TestInt and emits command 0x38 with integer wire kind 0x02. This proves the secured2 body is not boolean-only: it reuses the normal timestamped 0x37 value and timestamp prefix, then appends current-user token, client name, verifier token, 0xffff, client token, and write index.
  • 089-frida-write-testint-wrong-type: writing string not_an_int to integer TestChildObject.TestInt sends a normal 0x37 write body using string wire kind 0x05. NMX responds with a length-prefixed completion-only status body whose inner completion byte is 0x41; MXAccess returns S_OK from Write but does not fire OnWriteComplete during the harness wait.
  • 090-frida-write-invalid-reference: invalid reference add/advise produces the expected 0x10 registration and 0x11 registration-result failure. The later public Write call returns S_OK but no value-bearing 0x37 body or write-complete event is observed.
  • 091-frida-write-testint-double-type, 092-frida-write-testbool-string-type, and 093-frida-write-testdatetime-string-type: double-to-int writes emit a length-prefixed completion-only byte 0x00, but MXAccess still does not fire OnWriteComplete; string-to-bool and string-to-time wrong-type writes match the string-to-int failure pattern and emit completion byte 0x41.
  • analysis/ghidra/exports/LmxProxy.dll.buffered-decompile.md: decompile of AddBufferedItem, Fire_OnBufferedDataChange, and SetBufferedUpdateInterval. It confirms the .property(buffer) suffix, the buffered item-record marker, the seven-argument event firing path, and 100 ms tick rounding for buffered update intervals.
  • analysis/ghidra/exports/LmxProxy.dll.buffered-event-xrefs.md and analysis/ghidra/exports/LmxProxy.dll.buffered-event-caller-decompile.md: Fire_OnBufferedDataChange has one direct caller, FUN_1001657f. That function is the same native OnDataChange callback received path used for normal data changes. It looks up the item record and branches on a buffered item flag at offset 0x28: normal items call the _ILMXProxyServerEvents OnDataChange helper, while buffered items convert the value to value, quality, and timestamp SAFEARRAY variants and call _ILMXProxyServerEvents2::Fire_OnBufferedDataChange.
  • analysis/ghidra/exports/LmxProxy.dll.buffered-value-conversion-decompile.md: decompile of the buffered value conversion helper shows the public buffered event value argument is a SAFEARRAY of values, the quality argument is a VT_I2 SAFEARRAY, the timestamp argument is a VT_UI8/FILETIME SAFEARRAY, and the status argument is the normal MXSTATUS_PROXY[] SAFEARRAY. This confirms the event shape even though a live buffered payload has not yet been produced.
  • analysis/ghidra/exports/LmxProxy.dll.auth-decompile.md: decompile of AuthenticateUser and ArchestrAUserToId. Both increment a session-local user counter and store a mapping from that generated handle to a GUID/token identity before returning the generated handle to the public API caller.
  • analysis/ghidra/exports/LmxProxy.dll.events-decompile.md: decompile of Fire_OnWriteComplete and Fire_OperationComplete. Both build the same three-argument COM event payload: server handle, item handle, and one MXSTATUS_PROXY SAFEARRAY. OnWriteComplete dispatches event ID 2; OperationComplete dispatches event ID 3. No capture in this set emitted mx.event.operation-complete.
  • analysis/ghidra/exports/LmxProxy.dll.event-xrefs.md and analysis/ghidra/exports/LmxProxy.dll.event-callers-decompile.md: generated with headless Ghidra to identify the event helper callers. Fire_OnWriteComplete is called only from FUN_10016b50, which logs OnSetAttributeResult callback received. Fire_OperationComplete is called only from FUN_10016d4b, which logs OperationComplete callback received. This confirms that mapping write completion statuses to both public events would be incorrect; OperationComplete needs a distinct native callback capture before the managed compatibility event can fire.
  • analysis/ghidra/exports/LmxProxy.dll.operation-candidates-decompile.md: decompile of public operation candidates. RemoveItem performs local item cleanup, while Suspend and Activate query an IMxScanOnDemand interface and synchronously call vtable offsets 0x0c and 0x10, respectively. These paths did not reveal a call to Fire_OperationComplete, matching captures 077 and 078, which returned status structs but emitted no operation event.
  • 118-frida-suspend-advised-scanstate-long and 119-frida-activate-advised-scanstate-long: reran advised DevAppEngine.ScanState suspend/activate with direct hooks on CUserConnectionCallback.OnSetAttributeResult and CUserConnectionCallback.OperationComplete. The hooks installed, but neither callback entry point was called during the 20 second waits. This strengthens the conclusion that these public scan-on-demand calls return local status and do not trigger the public OperationComplete event on this node.
  • 094-frida-buffered-separate-writer: the harness was adjusted so buffered-external-write registers a separate writer server handle before adding/advising/writing TestChildObject.TestInt. The capture still produced no mx.event.buffered-data-change and no Fire_OnBufferedDataChange Frida entry. It did show the buffered 0x10 registration/0x11 result for TestInt.property(buffer) in context TestChildObject, plus normal writer subscription/data callbacks. This rules out same-server-handle writer reuse as the reason buffered callbacks were absent.
  • 120-frida-buffered-history-testhistoryvalue: first historized-attribute buffered attempt against TestHistoryValue, but the harness argument used --item-context instead of its actual --context switch, so MXAccess registered TestHistoryValue.property(buffer) with an empty context. This is retained only as a harness-option correction.
  • 121-frida-buffered-history-testhistoryvalue-context: repeated the capture with --context=TestMachine_001. GR identifies TestMachine_001.TestHistoryValue as a deployed, historized integer dynamic attribute. Native MXAccess emitted the expected buffered 0x10 registration and 0x11 result for TestHistoryValue.property(buffer) in context TestMachine_001, and the separate writer session successfully wrote 201, 202, and 203 through TestMachine_001.TestHistoryValue. No public mx.event.buffered-data-change and no Fire_OnBufferedDataChange Frida entry were observed.
  • 122-frida-buffered-history-testhistoryvalue-plainadvise: added a harness --plain-advise probe switch and repeated the same historized buffered scenario using public Advise instead of AdviseSupervisory. The registration/result bodies matched the context-bearing buffered shape, writer writes succeeded, and writer-session normal 0x32 data callbacks were seen, but the buffered subscriber still did not enter Fire_OnBufferedDataChange. This makes the remaining buffered gap a runtime/source-delivery condition, not a plain-versus-supervisory advise mismatch.
  • 095-frida-write-elapsed-int: writing 1000 as an Int32 to TestMachine_001.TestAlarm001.Alarm.TimeDeadband emitted a normal 0x37 write body with integer wire kind 0x02; MXAccess did not emit a special elapsed write kind for an integer caller value.
  • 096-frida-write-intl-string: writing "hello-native" as a string to TestChildObject.ShortDesc emitted a normal 0x37 write body with string wire kind 0x05. The runtime returned a completion-only status byte 0xef, so success semantics still need more captures, but outbound encoding is now defined for the caller-variant path.
  • 097-frida-write-bool-array-pattern: first attempted a bool-array pattern with --values, which the harness interprets as ten separate writes of one-element arrays. This capture is useful only as a harness argument reminder.
  • 098-frida-write-bool-array-pattern-10: writing one ten-element bool[] value with the requested pattern true,false,false,true,true,false,true,false,false,true confirmed the x86 MXAccess COM automation path still emits a paired/shifted VARIANT_BOOL-style wire payload: true,true,false,false,false,false,true,true,true,true. The NMX array descriptor remains 0x41, count 10, width 2. The managed encoder keeps direct per-element bool[] encoding for native .NET callers, while the observed x86 COM projection is preserved as a golden compatibility capture.
  • 099-frida-plain-advise-testint: public CLMXProxyServer.Advise for TestChildObject.TestInt emitted the same item-control 0x1f body shape as the earlier AdviseSupervisory scalar subscription capture, including the trailing option value 3. The wrapper's shared Advise/AdviseSupervisory path now has capture support for this scalar case.
  • 100-frida-subscribe-string-array: subscribing to TestChildObject.TestStringArray[] produced a subscription-status callback with wire kind 0x45, count 10, width code 4, and string-array element records, but the observed callback buffer stopped inside the final "JJ10" element after "JJ1". MXAccess did not raise a public OnDataChange event.
  • 101-frida-write-string-array-update: writing KA1;KB2;KC3;KD4;KE5;KF6;KG7;KH8;KI9;KJ10 emitted a complete outbound 0x37 string-array body. The following callback again carried a 0x45 string-array record but stopped inside the final "KJ10" element after "KJ1", and no public data-change event was observed. Current managed decoding treats this malformed callback value as incomplete rather than fabricating a string array.
  • 102-frida-subscribe-intl-shortdesc-after-write: subscribing to TestChildObject.ShortDesc after the earlier string write still produced the compact empty string callback form (wire kind 0x05, record length 4) and no public data-change event. The earlier 096 write returned completion-only 0xef, so it did not establish a non-empty InternationalizedString callback value.
  • 103-frida-subscribe-elapsed-after-write: subscribing to TestMachine_001.TestAlarm001.Alarm.TimeDeadband produced a non-zero ElapsedTime callback value using wire kind 0x07 followed by a 4-byte little-endian millisecond count (00 e4 0b 54, 0x540be400). This confirms the existing elapsed-time callback decoder works for non-zero values as well as the earlier zero capture.

Managed x64 live probes

  • probe-remqi-managed: a .NET 10 x64 process using managed NTLM activation successfully resolved INmxService2, completed RemQueryInterface, and returned partner version 6.
  • probe-session-write: direct managed WriteAsync to TestChildObject.TestInt returns success through INmxService2.TransferData. The managed sender now uses the captured transfer kind for normal writes (Write, value 3) rather than the earlier item-control kind.
  • probe-session-subscribe: fixed local engine IDs caused stale registration collisions and UnAdvise failures (0x80041101). The managed defaults now derive the local engine ID from the current process ID; unique engine IDs receive both 0x32 subscription-status and 0x33 data-update callbacks and clean up without that failure.
  • The live managed subscriber receives status-only scalar callbacks for TestChildObject.TestInt: the raw 0x33 body contains status, quality, timestamp, and wire kind 0x02, but no four-byte integer payload. The same status-only result occurs when an x86 MXAccess writer changes the tag while the managed subscriber is active. This narrows the missing value-delivery piece to the pre-advise AddItem/metadata registration path that MXAccess emits as 0x17, not to the scalar callback decoder.
  • A managed encoder for the observed 0x17 metadata body reproduces the capture exactly and NmxSvc accepts it before advise, but it still does not turn the managed scalar callback into a value-bearing record. The 0x17 body is therefore documented as a captured metadata primitive, not a complete AddItem/value-subscription registration.
  • Replaying the observed pre-advise 0x17 metadata body from the managed NTLM/DCOM path now reproduces the MXAccess-like callback sequence in x64: a 706-byte 0x40 metadata response containing DevPlatform.GR.TimeOfLastDeploy, DevPlatform.GR.TimeOfLastConfigChange, and the text An internal error occurred in the Base Runtime Object; a 151-byte 0x32 metadata status callback with two datetime records; the 92-byte operation status frame; then the normal 108-byte scalar 0x32 status-only callback for TestChildObject.TestInt. This proves the pre-advise metadata primitive is live and decoded enough to inspect, but its runtime result is an internal base-object error rather than successful value-subscription state.
  • A same-session managed subscribe-then-write probe also remains status-only: after SubscribeAsync(TestChildObject.TestInt), managed WriteAsync(331) returns success, then the callback is 0x33 with wire kind 0x02 but no integer payload. This rules out cross-client delivery as the only cause; the native library still needs additional LMX item/write state beyond the current GR handle, NMX advise, and direct NMX write bodies.
  • Ghidra review of CReferenceStringResolutionService explains why replaying only the wire-visible metadata request is insufficient. The resolver compares TimeOfLastDeploy and TimeOfLastConfigChange against pending reference status details, and on a usable GR subscription status it calls an in-process OnSetAttributeResult path directly before removing the pending request. That state mutation is not produced by the current managed x64 replay, which only sends the NmxSvc request and receives callbacks.
  • Fixed Frida captures 104 and 105 corrected the stack argument mapping for PrebindReference and UserRegisterPreboundReference. Native TestChildObject.ShortDesc returns public prebound/reference handles of 1, sends 0x1f advise with transfer kind ItemControl (2), and its IMxReference.GetMxHandle value uses property id 10 even though the GR attribute category is 11.
  • After changing the managed sender to use transfer kind 2 for 0x1f advise and changing the GR resolver to synthesize value handles with property id 10, .NET 10 x64 SubscribeAsync(TestChildObject.ShortDesc) receives the native-equivalent 112-byte callback: command 0x32, status/detail 3/0, quality 0x00C0, wire kind 0x05, and empty string value. This proves the primary value-subscription path can be reproduced in full managed code for at least one x86 value-bearing capture.
  • A managed-client regression check now asserts the distinct transfer kinds: normal 0x37 writes use transfer kind Write (3), while 0x1f advise uses transfer kind ItemControl (2). The generated ShortDesc "hello-native" write body matches capture 096 byte-for-byte, including the value handle with property id 10.
  • Live x64 subscribe-write probes after that correction matched the native outcomes: TestChildObject.ShortDesc still returns completion-only byte 0xef, which is therefore native-equivalent for the current InternationalizedString caller path rather than a managed-only transport failure; TestChildObject.TestInt returns completion-only byte 0x00 and a status-only 0x33 update callback with wire kind 0x02.
  • Fresh x86 MXAccess baseline captures on the current VM state: 106-native-subscribe-testint-current and 107-native-write-testint-current show public MXAccess also raises no mx.event.data-change for TestChildObject.TestInt subscribe-only or subscribe-then-write, even though the write call returns success. Older captures 003, 011, 012, 017, and 018 prove this tag previously emitted value-bearing public events, so the current status-only managed result is not by itself a managed transport failure; it reflects the current runtime/Galaxy state or deployment value-delivery state.
  • Fresh current-state capture 108-native-subscribe-scalar-current subscribed to TestBool, TestInt, TestFloat, TestString, and ShortDesc through native x86 MXAccess and raised no public mx.event.data-change for any of them. Because native x86 also suppresses the decoded empty ShortDesc adapter callback at the public event layer, MxNativeCompatibilityServer now suppresses empty InternationalizedString DataChanged promotion while leaving the low-level MxNativeSession.CallbackReceived event intact.
  • The new .NET 10 x64 compatibility probe --probe-compatibility-subscribe validates that public-facade behavior: TestChildObject.ShortDesc and TestChildObject.TestInt both reported compat_data_changes=0, matching fresh native x86 public-event behavior on the current VM state.
  • The companion --probe-compatibility-subscribe-write probe validates public write-path facade behavior: TestChildObject.TestInt = 793 and TestChildObject.ShortDesc = hello-compat both completed the managed write call but reported compat_subscribe_write_data_changes=0 and compat_subscribe_write_completes=0, matching the current native public behavior where completion-only NMX statuses do not surface as OnWriteComplete.
  • Invalid-reference parity is now modeled in the compatibility facade: NoSuchObject_999.NoSuchAttr returns an item handle from AddItem, and AdviseSupervisory emits one public data-change with value=null, quality=0, status_success=0, ConfigurationError, RequestingLmx, and detail 6, matching captures 007 and 015. A subsequent compatibility write to that invalid handle returns without adding a public write-complete event, matching capture 090.
  • A mixed multi-item .NET 10 x64 compatibility probe with ShortDesc, TestInt, and NoSuchObject_999.NoSuchAttr validated handle routing in one server session: items 1 and 2 produced zero public changes on the current VM state, while item 3 produced exactly one configuration-error data-change.
  • Compatibility write argument validation now mirrors two x86 error captures: normal Write after AddItem but before advise returns 0x80070057, as in captures 008/009, and normal Write against an AddBufferedItem handle returns 0x80070057, as in mxaccess-add-buffered-write-testint-context.log.
  • captures\109-native-post-remove-errors records stale item-handle parity after RemoveItem. Native x86 MXAccess returns ArgumentException 0x80070057 for Advise, AdviseSupervisory, UnAdvise, Write, Write2, Suspend, Activate, and a second RemoveItem against the removed handle. The .NET 10 x64 --probe-compatibility-post-remove probe now reports the same 0x80070057 result for all of those operations.
  • captures\110-native-invalid-handle-errors records invalid server-handle and cross-server item-handle parity. Native x86 MXAccess returns ArgumentException 0x80070057 for AddItem, AddItem2, RemoveItem, Advise, AdviseSupervisory, UnAdvise, Write, Write2, Suspend, Activate, and Unregister when the server handle is invalid, and also for RemoveItem, Advise, and Write when the item handle belongs to another server. The .NET 10 x64 --probe-compatibility-invalid-handles probe now matches those HRESULTs.
  • Literal property-reference parity is now covered for the observed buffer property. Captures 085/086 show TestChildObject.TestInt.property(buffer) resolving to base TestInt with property id 0x32 and native handle 01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 32 00 3e da 00 00. GalaxyRepositoryTagResolver now recognizes that suffix, and the .NET 10 x64 compatibility subscribe/write probe for the literal reference reports zero public data-changes and zero public write-completes, matching the native public behavior in those captures.
  • AddItem2 context resolution has been broadened in the compatibility layer. The live .NET 10 x64 probe now covers the captured simple context form TestInt + TestChildObject, a dotted primitive attribute Alarm.TimeDeadband + TestMachine_001.TestAlarm001, and a context-relative property reference TestInt.property(buffer) + TestChildObject. All three add and advise successfully; no public data-change is promoted on the current VM state.
  • A mixed .NET 10 x64 compatibility write probe now validates per-item routing for TestChildObject.TestInt, TestChildObject.ShortDesc, TestChildObject.TestInt.property(buffer), and NoSuchObject_999.NoSuchAttr in one server session. The three valid/literal items report data_changes=0 and write_completes=0 on the current VM state, while the invalid reference reports exactly one configuration-error data-change and no write-complete. This confirms the compatibility facade does not misattribute suppressed completion-only writes or invalid-reference callbacks across item handles.
  • A first post-fix scalar matrix shows more decoder/runtime work remains: TestChildObject.TestString receives a 0x05 string record but the current body ends before the declared string payload is complete, so the decoder reports value=null; TestChildObject.TestFloat receives wire kind 0x03 without enough bytes for a float payload; TestChildObject.TestBool receives a 105-byte frame that stops inside the timestamp/kind area and is surfaced as UnparsedCallbackReceived. These look like status/incomplete initial-value callbacks on this node rather than handle-generation failures, because ShortDesc now matches the native value-bearing path.
  • Managed recovery lifecycle probe: MxNativeClient.Probe --probe-session-recover --recover-attempts=2 --recover-delay-ms=100 --tag=TestChildObject.TestInt --value=323 ran from .NET 10 x64. It subscribed successfully, reported one recovery-started event, zero failed-attempt events, one recovery-completed event, preserved one active subscription after recovery, and wrote through the recovered session.
  • Recovery callback policy is now explicit in the managed API: callbacks are passed through during reconnect/replay and marked with IsDuringRecovery instead of being suppressed or buffered. More live evidence is still needed to catch actual replay-window callback delivery on larger subscription sets.
  • Managed multi-subscription recovery probe: MxNativeClient.Probe --probe-session-recover-multi --recover-attempts=2 --recover-delay-ms=100 subscribed to TestChildObject.TestInt, TestChildObject.ShortDesc, TestMachine_001.ProtectedValue, and TestMachine_001.ProtectedValue1. Recovery replay preserved all four subscriptions. The run observed two data callbacks and four unparsed callbacks overall, but zero data, operation-status, reference-registration, or unparsed callbacks with IsDuringRecovery=true.
  • Managed multi-subscription recovery churn probe: MxNativeClient.Probe --probe-session-recover-multi --recover-attempts=2 --recover-delay-ms=100 --recover-concurrent-writes --recover-write-start=330 --recover-write-count=5 --recover-write-delay-ms=10 used a separate writer session to write TestChildObject.TestInt values 330-334 during recovery. Two writes landed before the recovery-completed event. The recovering session preserved all four subscriptions, observed four data callbacks overall, and marked two data callbacks with IsDuringRecovery=true; operation-status, reference-registration, and unparsed recovery-window counts remained zero.
  • OperationComplete trigger analysis update: decompiled interop exposes IMxCallback2.OperationComplete(int lCallbackId, ref MxStatus, string) plus the IDataConsumer.ActivateSuspend / ProcessActivateSuspend2 path returning ItemActiveResponse. Public LMXProxyServerClass.Suspend and Activate captures 118/119 used direct IMxScanOnDemand calls and did not enter the callback. The next useful native trigger search should target DataConsumer activate/suspend completion, not another plain public suspend/activate run.
  • aaMxDataConsumer.dll import/probe: registry CLSID {85209FB2-0BA1-4594-BBC4-59D3DDAB823D} maps to MxDataConsumer Class in C:\Program Files (x86)\Common Files\ArchestrA\Services\aaMxDataConsumer.dll. tlbimp generated analysis\interop\Interop.aaMxDataConsumer.dll, and ilspycmd decompiled it into analysis\decompiled-interop\Interop.aaMxDataConsumer. The new MxDataConsumerProbe x86 harness can instantiate DataConsumerClass, call RegisterCallback, resolve namespace strings to ID 1, and call ResolveReference, subscribe, ActivateSuspend, and ProcessActivateSuspend2. Tested namespace strings remain disconnected (IsConnected=0), no registration or subscription records are returned, and ProcessActivateSuspend2 returns 0x8007139F (ERROR_INVALID_STATE). This confirms the COM surface is callable but still missing the bootstrap needed to attach it to the live namespace.
  • MxDataConsumerProbe --probe-dataclient attempted to create DataClientClass from the same imported type library. Creation failed with 0x80040154 (REGDB_E_CLASSNOTREG) because CLSID {73BC4121-FF89-4762-901C-206E2BD9FE87} is not registered on this node. The ASB deployment config shows ServiceHost1 at net.tcp://localhost:4000/ and Default_ZB_MxDataProvider publishing IASBIData, IASBIDataV2, and IDataV3, but a registered client/factory is still needed before endpoint connection probes can run.
  • Headless Ghidra plus ILSpy on C:\Program Files (x86)\Common Files\ArchestrA\Services\aaMxDataConsumer.dll shows that its DataClient side is a mixed-mode wrapper around managed ASB proxies. CDataClientCLI.CreateConnection sets the namespace string on a DataClientProxy and starts the auto-connect worker; DataClientProxy calls CIDataVersionAdapterFactory.GetIDataAdapter(accessName), which calls IDataProxySelector.SelectProxyForLatestEndpoint(accessName, new AsbMxDataSettings(), out error).
  • ASBIDataV2Adapter.dll from the GAC contains IDataProxySelector. It first checks ASBDataV2Proxy.FindIDataEndpoint(accessName, DiscoveryScope.Global); if any endpoints are discovered it returns ASBDataV2Proxy, otherwise it falls back to ASBDataProxy V1. ASBDataV2Proxy searches LDS using scope domainname/<accessName>/global.
  • New x64 AsbProxyProbe results: access name ZB discovers one IASBIDataV2 endpoint, net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2, with listen URIs on the host name and local IPs. Access names Default_ZB_MxDataProvider, Galaxy, localhost, and ZB2 discover no IDataV2/IData endpoints. AsbProxyProbe --access=ZB --connect successfully opens the ASB proxy from an x64 managed process; Connect returns true, channel state is Opened, and PublishWriteComplete returns success with zero pending writes.
  • AsbProxyProbe --access=ZB --connect --register --read --write-int=401 --tag=TestChildObject.TestInt proves the direct ASB value path. The tag registers with item id 18446462598732840961, reads as ASB type 4 (Int32) value 334, accepts a write of 401, and reads back 401. The immediate write result is global success with per-item 0x0000001F (OperationWouldBlock), then PublishWriteComplete returns 0x00000020 (PublishComplete) with the submitted write handle 0xA5B20001 and final per-item success.
  • A follow-up x86 MxDataConsumerProbe --namespace=ZB run hung inside the COM wrapper and was stopped. Direct ASB proxy probing is now the preferred path for validating data-service functionality and any future OperationComplete trigger without relying on the standalone mixed-mode DataConsumer COM object.
  • Pure .NET 10 x64 ASB port update: MxAsbClient.Probe --tag=TestChildObject.TestInt --write-int=412 now completes the core register/read/write/complete data-service flow without AVEVA assembly references. It connects/authenticates through ASB system auth, retries the observed one-way AuthenticateMe startup race in RegisterItems, reads the tag, accepts the write with immediate per-item 0x0000001F, reads back 412, and decodes PublishWriteComplete result 0x00000020, count 1, the submitted handle, and final per-item success. The saved evidence is analysis\proxy\mxasbclient-probe-stage21-register-retry.txt.
  • Pure .NET 10 ASB unregister update: MxAsbClient.Probe --tag=TestChildObject.TestInt now calls UnregisterItems with the registered item identity. The pure client and installed ASBDataV2Proxy compare run both return global success 0x00000000 and per-item 0x0000000B (OperationFailed) for this provider. Evidence: analysis\proxy\mxasbclient-probe-stage23-unregister-id.txt and analysis\proxy\asbproxyprobe-unregister-compare.txt.
  • Pure .NET 10 ASB multi-item update: MxAsbClient.Probe --tag=TestChildObject.TestInt --tag=TestMachine_001.TestHistoryValue registers and reads both tags in two-item requests. Both tags return per-item register/read success; values decode as ASB TypeInt32 previews 412 and 303. Evidence: analysis\proxy\mxasbclient-probe-stage24-multi-read.txt.

Next capture steps

  1. Decode NmxSvcps.dll and WWProxyStub.dll to recover interface names, opnum signatures, and NDR stub layouts for the observed DCE/RPC UUIDs.
  2. Extract all localhost binary streams, not just port 49704, and correlate them to harness method/callback timestamps.
  3. Trace LmxProxy.dll, Lmx.dll, NmxAdptr.dll, and NmxSvc.exe with API Monitor around: send, recv, WSASend, WSARecv, INmx4.PutRequest2, INmx4.GetResponse2, IDataClient.RegisterItems2, IDataClient.Write2, and IDataClient.PublishWriteComplete2.
  4. Decide whether a future strict MXAccess COM-compatibility mode should intentionally reproduce the x86 SAFEARRAY VT_BOOL value-pairing behavior from capture 098; the native managed path currently uses direct per-element bool encoding.
  5. Build managed encoder/decoder tests from the scalar, array, and write-mode matrix TSVs.
  6. Decode DCE/RPC/NDR and mixed-stream records structurally; raw byte searching has confirmed that adapter bodies are not copied verbatim to TCP.