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

976 lines
54 KiB
Markdown

# MXAccess capture run - 2026-04-25
This run used the primary installed MXAccess interop assembly:
```text
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:
```text
analysis\sql\select_capture_tags.sql
```
It connects to the Galaxy repository described in
`C:\Users\dohertj2\Desktop\lmxopcua\gr\connectioninfo.md`:
```text
Server=localhost; Database=ZB; Integrated Security=SSPI
```
The output from this run is saved at:
```text
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:
```text
dotnet build src\MxTraceHarness\MxTraceHarness.csproj -c Release
```
Executable:
```text
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:
```text
captures\010-write-test-int-advised-same-value\harness.log
```
Observed sequence:
```text
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:
```text
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:
```text
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:
```text
captures\012-npcap-loopback-subscribe-scalar\
```
Files:
```text
loopback.pcapng
harness.log
nmx-loopback-frames.tsv
```
The focused loopback capture includes the active MXAccess/NMX conversation:
```text
::1:59335 <-> ::1:49704
803 frames, about 84 kB, duration 7.6793 s
```
It also saw the pre-existing service connection:
```text
::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:
```text
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:
```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\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:
```text
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:
```text
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:
```text
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:
```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 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:
```text
--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:
```text
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:
```text
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:
```text
analysis\frida\write-body-matrix.tsv
```
Array matrix:
```text
analysis\frida\write-array-body-matrix.tsv
```
Write-mode matrix:
```text
analysis\frida\write-mode-matrix.tsv
```
Frida-to-TCP mapper:
```text
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:
```text
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.