Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# ASB / Native Integration Decision
|
||||
|
||||
## Decision
|
||||
|
||||
Use `MxAsbClient` as the preferred implementation for the regular tag data
|
||||
plane. Do not embed ASB inside the current `MxNativeSession` transport. Instead,
|
||||
introduce a higher-level compatibility facade that can route each MXAccess-style
|
||||
operation to either ASB or native NMX behind a common server/item-handle model.
|
||||
|
||||
The boundary should be operation-based, not inheritance-based:
|
||||
|
||||
- ASB owns direct `IASBIDataV2` connect, register, unregister, read, write,
|
||||
write-completion polling, publish subscriptions, cleanup, and explicit
|
||||
reconnect.
|
||||
- Native NMX owns COM/DCOM activation, NMX service registration, callback export,
|
||||
Galaxy repository metadata resolution, native subscription callbacks, native
|
||||
recovery replay, and MXAccess behaviors whose ASB equivalent is not proven.
|
||||
- The existing `MxNativeClient` should remain available as the native fallback
|
||||
and diagnostic path. ASB can replace its basic scalar/array read/write and
|
||||
simple data-change usage only after a shared facade makes routing explicit.
|
||||
|
||||
This avoids coupling the WCF ASB channel lifecycle to the DCE/RPC callback
|
||||
lifecycle while still letting callers move to the simpler pure-managed x64 ASB
|
||||
path where evidence is strongest.
|
||||
|
||||
## Evidence
|
||||
|
||||
Current ASB evidence is strong for the normal data path:
|
||||
|
||||
- The pure .NET 10 x64 ASB client connects to the live `IASBIDataV2` net.tcp
|
||||
endpoint, authenticates from local DPAPI-protected solution material, and
|
||||
performs register/read/write without AVEVA assembly references.
|
||||
- ASB register/read/write is live-proven for deployed scalar and array tags, and
|
||||
`PublishWriteComplete` supplies write-handle completion records for basic
|
||||
writes.
|
||||
- ASB subscriptions can create subscriptions, add monitored items, publish
|
||||
values, map quality/status payloads, and raise an MXAccess-like data-change
|
||||
event through `MxAsbCompatibilityServer`.
|
||||
- ASB now has public option objects for connection, write, write completion,
|
||||
subscriptions, monitored items, cleanup, and reconnect.
|
||||
|
||||
Current native evidence is still required for legacy semantics:
|
||||
|
||||
- `MxNativeSession` owns NMX service activation, DCE/RPC transport, callback sink
|
||||
registration, heartbeat/recovery, Galaxy metadata resolution, NMX write
|
||||
message projection, `Write2`, `WriteSecured2`, supervisory advise, buffered
|
||||
registration, and callback parsing.
|
||||
- `MxNativeCompatibilityServer` models the fuller MXAccess surface:
|
||||
`AddItem2`, `AuthenticateUser`, `ArchestrAUserToId`, `Suspend`, `Activate`,
|
||||
`AdviseSupervisory`, `OnWriteComplete`, `OperationComplete`, and
|
||||
`OnBufferedDataChange`.
|
||||
- Direct `IASBIDataV2` exposes no activate, suspend, or generic
|
||||
`OperationComplete` operation. It only exposes write-completion polling.
|
||||
- ASB buffered monitored items are accepted by the observed provider, but publish
|
||||
still returns one current value rather than a native buffered sample batch.
|
||||
- Direct system-auth ASB writes did not reproduce the public MXAccess secured
|
||||
write rejection path on this VM.
|
||||
|
||||
## Recommended Boundary
|
||||
|
||||
Create a routing layer above both clients. It should hold compatibility
|
||||
server/item handles and choose a backend per operation:
|
||||
|
||||
- `Register`, `Unregister`, `AddItem`, `RemoveItem`, `Read`, basic `Write`,
|
||||
simple `Advise`, `UnAdvise`, and write-completion polling should default to
|
||||
ASB when an ASB endpoint is configured and the tag/value shape is supported.
|
||||
- `Write2`, `WriteSecured2`, `AuthenticateUser`, `ArchestrAUserToId`, `Suspend`,
|
||||
`Activate`, `AdviseSupervisory`, buffered items, and native callback-only
|
||||
events should route to native NMX until direct ASB evidence exists.
|
||||
- `OperationComplete` should not be synthesized from ASB write completion. ASB
|
||||
write completion can raise a write-complete event, but generic operation
|
||||
completion remains native-only until a native trigger and payload mapping are
|
||||
proven.
|
||||
- The facade should expose which backend handled an item or operation so tests,
|
||||
probes, logs, and callers can distinguish ASB behavior from native fallback.
|
||||
|
||||
## What ASB Should Own
|
||||
|
||||
- Endpoint discovery/configuration inputs for the direct `IASBIDataV2` route.
|
||||
- Connection/authentication, request signing, channel cleanup, reconnect, and
|
||||
connection-failure cleanup for ASB.
|
||||
- ASB item identity creation, register/unregister/read, variant
|
||||
encode/decode, and supported scalar/array writes.
|
||||
- `AsbWriteOptions` and `AsbWriteCompletionOptions` driven write completion,
|
||||
including delayed readback when requested.
|
||||
- ASB subscription creation, monitored item creation/deletion, publish polling,
|
||||
quality/status mapping, and basic MXAccess-compatible data-change adaptation.
|
||||
- ASB-specific result/status summaries that preserve raw provider status while
|
||||
exposing named categories for known cases.
|
||||
|
||||
## What Native NMX Should Still Own
|
||||
|
||||
- DCE/RPC, COM object reference handling, NTLM/SSPI authentication, callback
|
||||
export, and NMX service registration.
|
||||
- Galaxy repository tag resolution and metadata needed for NMX reference handles,
|
||||
value-kind projection, and native buffered registration.
|
||||
- `Write2` and `WriteSecured2` until ASB timestamped and secured-write parity is
|
||||
explicitly proven.
|
||||
- `Suspend`, `Activate`, supervisory advise, native recovery replay, heartbeat
|
||||
policy, and callback-derived event surfaces.
|
||||
- `OnBufferedDataChange` and generic `OperationComplete` until ASB produces
|
||||
equivalent runtime evidence.
|
||||
- Native diagnostic/probe coverage for payloads that ASB does not expose.
|
||||
|
||||
## Risks And Unknowns
|
||||
|
||||
- ASB endpoint discovery/configuration is environment-specific; fallback to NMX
|
||||
must be explicit when no usable endpoint is configured.
|
||||
- ASB access-denied, no-communication, and other live fault status cases still
|
||||
need safe source conditions before they can be treated as complete parity.
|
||||
- The exact native completion-only byte-to-status mapping is not fully proven.
|
||||
- ASB buffered publish behavior may vary by provider or configuration; the
|
||||
observed provider did not produce multi-sample batches.
|
||||
- Dual routing can create semantic drift if the facade hides backend choice.
|
||||
Backend selection must be visible in diagnostics and test assertions.
|
||||
- Mixing ASB and native subscriptions for the same logical item can duplicate
|
||||
callbacks or produce ordering differences; route each item to one backend at a
|
||||
time.
|
||||
|
||||
## Staged Migration
|
||||
|
||||
1. Add a small shared facade project or class that exposes the MXAccess-style
|
||||
handle API and delegates to `MxAsbCompatibilityServer` or
|
||||
`MxNativeCompatibilityServer`.
|
||||
2. Start with opt-in ASB routing for basic register/read/write/simple-advise
|
||||
flows. Keep native as the default fallback for unsupported operations.
|
||||
3. Add route annotations to events and probe output so every data-change,
|
||||
write-complete, and fallback decision identifies `asb` or `native`.
|
||||
4. Port basic compatibility tests to run against the facade with ASB-for-data
|
||||
enabled and native fallback enabled. Preserve existing ASB-only and
|
||||
native-only tests.
|
||||
5. Gate any additional replacement on evidence: timestamped ASB write behavior,
|
||||
secured-write behavior, buffered sample batches, and native
|
||||
operation-complete parity should each require a focused capture or live probe
|
||||
before moving from native to ASB.
|
||||
6. After the facade is stable, treat `MxNativeClient` as the legacy/native
|
||||
adapter and `MxAsbClient` as the primary data adapter. Avoid merging their
|
||||
transport internals.
|
||||
@@ -0,0 +1,222 @@
|
||||
# ASB Variant Wire Format
|
||||
|
||||
This note documents the ASB `Variant` and related value/status payloads that
|
||||
`MxAsbClient` currently understands. It is based on
|
||||
`src/MxAsbClient/AsbContracts.cs`, `MxAsbDataClient.DecodeVariant`,
|
||||
`MxAsbDataClient.FormatVariant`, `src/MxAsbClient.Tests/Program.cs`, and the
|
||||
remaining-work notes.
|
||||
|
||||
## ASBIData Framing
|
||||
|
||||
ASB custom data-contract values are serialized as an `ASBIData` XML element
|
||||
containing base64-encoded binary. The binary uses the .NET `BinaryWriter` /
|
||||
`BinaryReader` primitive layout used by the client on Windows:
|
||||
|
||||
| Field kind | Binary layout |
|
||||
| --- | --- |
|
||||
| `ushort` | 2-byte little-endian unsigned integer |
|
||||
| `int` / `uint` | 4-byte little-endian integer |
|
||||
| `long` / `ulong` | 8-byte little-endian integer |
|
||||
| `float` / `double` | 4-byte / 8-byte IEEE payload from `BitConverter` |
|
||||
| `bool` fields in custom structs | one `BinaryWriter.Write(bool)` byte |
|
||||
| UTF-16 strings in custom structs | 4-byte byte length, then UTF-16LE bytes |
|
||||
| custom arrays | 4-byte element count, then each element's custom binary body |
|
||||
|
||||
The `Variant` body itself is:
|
||||
|
||||
| Offset | Size | Field | Notes |
|
||||
| --- | ---: | --- | --- |
|
||||
| 0 | 2 | `Type` | `AsbDataType` numeric ID. |
|
||||
| 2 | 4 | `Length` | Logical payload length. Factory-created variants set this to `Payload.Length`. |
|
||||
| 6 | 4 | `PayloadLength` | Actual byte count emitted before `Payload`. |
|
||||
| 10 | `PayloadLength` | `Payload` | Type-specific bytes. Empty or missing payload is treated as empty. |
|
||||
|
||||
`DecodeVariant` trusts the actual payload bytes for parsing. Unsupported type IDs
|
||||
with non-empty payloads are returned as raw `byte[]`; unsupported empty variants
|
||||
return `null`, except known empty string/array types, which return empty values.
|
||||
|
||||
## Declared Type IDs
|
||||
|
||||
These IDs are declared by `AsbDataType`. Only the layouts listed as
|
||||
decode-supported below should be considered implemented behavior.
|
||||
|
||||
| ID | Name | Current handling |
|
||||
| ---: | --- | --- |
|
||||
| 0 | `TypeByte` | Declared only; raw bytes if received. |
|
||||
| 1 | `TypeChar` | Declared only; raw bytes if received. |
|
||||
| 2 | `TypeInt16` | Declared only; raw bytes if received. |
|
||||
| 3 | `TypeUInt16` | Declared only; raw bytes if received. |
|
||||
| 4 | `TypeInt32` | Decode/write supported and live-proven. |
|
||||
| 5 | `TypeUInt32` | Declared only; raw bytes if received. |
|
||||
| 6 | `TypeInt64` | Declared only; raw bytes if received. |
|
||||
| 7 | `TypeUInt64` | Declared only; raw bytes if received. |
|
||||
| 8 | `TypeFloat` | Decode/write supported and live-proven. |
|
||||
| 9 | `TypeDouble` | Decode/write supported and live-proven. |
|
||||
| 10 | `TypeString` | Decode/write supported and live-proven. |
|
||||
| 11 | `TypeDateTime` | Decode/write supported and live-proven. |
|
||||
| 12 | `TypeDuration` | Decode/write supported and live-proven. |
|
||||
| 13 | `TypeGuid` | Declared only; raw bytes if received. |
|
||||
| 14 | `TypeByteString` | Declared only; raw bytes if received. |
|
||||
| 15 | `TypeLocaleId` | Declared only; raw bytes if received. |
|
||||
| 16 | `TypeLocalizedText` | Declared only; raw bytes if received. |
|
||||
| 17 | `TypeBool` | Decode/write supported and live-proven. |
|
||||
| 18 | `TypeSByte` | Declared only; raw bytes if received. |
|
||||
| 19 | `TypeErrorStatus` | Declared only; raw bytes if received. |
|
||||
| 20 | `TypeEnum` | Declared only; raw bytes if received. |
|
||||
| 21 | `TypeDataType` | Declared only; raw bytes if received. |
|
||||
| 22 | `TypeSecurityClassification` | Declared only; raw bytes if received. |
|
||||
| 23 | `TypeDataQuality` | Declared only; raw bytes if received. |
|
||||
| 40 | `TypeByteArray` | Declared only; raw bytes if received. |
|
||||
| 41 | `TypeCharArray` | Declared only; raw bytes if received. |
|
||||
| 42 | `TypeInt16Array` | Declared only; raw bytes if received. |
|
||||
| 43 | `TypeUInt16Array` | Declared only; raw bytes if received. |
|
||||
| 44 | `TypeInt32Array` | Decode/write supported and live-proven for deployed array tags. |
|
||||
| 45 | `TypeUInt32Array` | Declared only; raw bytes if received. |
|
||||
| 46 | `TypeInt64Array` | Declared only; raw bytes if received. |
|
||||
| 47 | `TypeUInt64Array` | Declared only; raw bytes if received. |
|
||||
| 48 | `TypeFloatArray` | Decode/write supported and live-proven for deployed array tags. |
|
||||
| 49 | `TypeDoubleArray` | Decode/write supported and live-proven for deployed array tags. |
|
||||
| 50 | `TypeStringArray` | Decode/write supported and live-proven for deployed array tags. |
|
||||
| 51 | `TypeDateTimeArray` | Decode/write supported and live-proven for deployed array tags. |
|
||||
| 52 | `TypeDurationArray` | Decode/write supported by tests; live coverage depends on deployed tags. |
|
||||
| 53 | `TypeGuidArray` | Declared only; raw bytes if received. |
|
||||
| 54 | `TypeByteStringArray` | Declared only; raw bytes if received. |
|
||||
| 55 | `TypeLocaleIdArray` | Declared only; raw bytes if received. |
|
||||
| 56 | `TypeLocalizedTextArray` | Declared only; raw bytes if received. |
|
||||
| 57 | `TypeBoolArray` | Decode/write supported and live-proven for deployed array tags. |
|
||||
| 58 | `TypeSByteArray` | Declared only; raw bytes if received. |
|
||||
| 60 | `TypeEnumArray` | Declared only; raw bytes if received. |
|
||||
| 61 | `TypeDataTypeArray` | Declared only; raw bytes if received. |
|
||||
| 62 | `TypeSecurityClassificationArray` | Declared only; raw bytes if received. |
|
||||
| 63 | `TypeDataQualityArray` | Declared only; raw bytes if received. |
|
||||
| 65535 | `TypeUnknown` | Used for empty/user-data placeholders. |
|
||||
|
||||
## Supported Scalar Payloads
|
||||
|
||||
| Type | ID | Payload layout | Empty-payload decode |
|
||||
| --- | ---: | --- | --- |
|
||||
| `TypeBool` | 17 | one byte; non-zero is `true`, zero is `false` | `null` |
|
||||
| `TypeInt32` | 4 | 4-byte little-endian signed integer | `null` |
|
||||
| `TypeFloat` | 8 | 4-byte IEEE single from `BitConverter` | `null` |
|
||||
| `TypeDouble` | 9 | 8-byte IEEE double from `BitConverter` | `null` |
|
||||
| `TypeString` | 10 | UTF-16LE bytes, no per-string length inside the variant payload | empty string |
|
||||
| `TypeDateTime` | 11 | 8-byte signed Windows FILETIME from `DateTime.ToFileTimeUtc()` | `null` |
|
||||
| `TypeDuration` | 12 | 8-byte signed .NET tick count from `TimeSpan.Ticks` | `null` |
|
||||
|
||||
`FormatVariant` renders scalar values as invariant strings. `DateTime` values
|
||||
format with the round-trip `O` pattern after `DateTime.FromFileTimeUtc()`;
|
||||
`Duration` values format with the constant `c` `TimeSpan` pattern.
|
||||
|
||||
## Supported Array Payloads
|
||||
|
||||
Variant arrays do not carry an element count in a common header. The element
|
||||
count is inferred from payload byte length for fixed-width arrays, or by walking
|
||||
length-prefixed records for string arrays.
|
||||
|
||||
| Type | ID | Payload layout | Empty-payload decode |
|
||||
| --- | ---: | --- | --- |
|
||||
| `TypeInt32Array` | 44 | packed 4-byte little-endian signed integers | empty `int[]` |
|
||||
| `TypeBoolArray` | 57 | one byte per element; non-zero is `true` | empty `bool[]` |
|
||||
| `TypeFloatArray` | 48 | packed 4-byte IEEE singles | empty `float[]` |
|
||||
| `TypeDoubleArray` | 49 | packed 8-byte IEEE doubles | empty `double[]` |
|
||||
| `TypeStringArray` | 50 | repeated 4-byte byte length, then UTF-16LE bytes; zero length is empty string | empty `string[]` |
|
||||
| `TypeDateTimeArray` | 51 | packed 8-byte Windows FILETIME values | empty `DateTime[]` |
|
||||
| `TypeDurationArray` | 52 | packed 8-byte .NET tick counts | empty `TimeSpan[]` |
|
||||
|
||||
For malformed string-array payloads, decoding stops when the next declared
|
||||
string byte length is negative or extends beyond the remaining payload. The
|
||||
partial values decoded before that point are preserved.
|
||||
|
||||
## Timestamp and Duration Handling
|
||||
|
||||
There are two different timestamp encodings:
|
||||
|
||||
| Location | Encoding |
|
||||
| --- | --- |
|
||||
| `Variant` `TypeDateTime` / `TypeDateTimeArray` payloads | Windows FILETIME UTC values (`DateTime.ToFileTimeUtc()` / `DateTime.FromFileTimeUtc()`). |
|
||||
| `RuntimeValue.Timestamp` in read/publish wrappers | 8-byte `DateTime.ToBinary()` value followed by a one-byte `TimestampSpecified` flag. Publish mapping normalizes the timestamp to UTC for `AsbPublishedValue.TimestampUtc`. |
|
||||
|
||||
`TypeDuration` and `TypeDurationArray` payloads are 8-byte `TimeSpan.Ticks`
|
||||
values. No alternate duration units are inferred.
|
||||
|
||||
## String Encoding
|
||||
|
||||
Scalar `TypeString` payloads are raw UTF-16LE bytes without an inner length.
|
||||
The variant `Length` and `PayloadLength` fields provide the byte count.
|
||||
|
||||
`TypeStringArray` payloads are a sequence of string records. Each record starts
|
||||
with a 4-byte little-endian byte length followed by that many UTF-16LE bytes.
|
||||
`null` and empty strings are both emitted as a zero-length string by
|
||||
`AsbVariantFactory.FromStringArray`, and decode as `string.Empty`.
|
||||
|
||||
The same 4-byte byte-length plus UTF-16LE convention is also used by custom
|
||||
struct string fields such as `ItemIdentity.Name`, but that is outside the
|
||||
variant payload.
|
||||
|
||||
## Runtime Value, Quality, and Status
|
||||
|
||||
Quality is not part of a `Variant` payload. Reads and publishes wrap the variant
|
||||
inside `RuntimeValue`, whose binary layout is:
|
||||
|
||||
| Order | Field | Binary layout |
|
||||
| ---: | --- | --- |
|
||||
| 1 | `Timestamp` | 8-byte `DateTime.ToBinary()` value |
|
||||
| 2 | `TimestampSpecified` | one boolean byte |
|
||||
| 3 | `Value` | nested `Variant` body |
|
||||
| 4 | `Status` | nested `AsbStatus` body |
|
||||
|
||||
`AsbStatus` is:
|
||||
|
||||
| Order | Field | Binary layout |
|
||||
| ---: | --- | --- |
|
||||
| 1 | `Count` | signed byte |
|
||||
| 2 | payload byte length | 4-byte integer |
|
||||
| 3 | `Payload` | status element bytes |
|
||||
|
||||
`AsbPublishMapper.DecodeStatus` treats `Count` as the number of payload bytes
|
||||
to inspect when it is positive, capped at the actual payload length. If `Count`
|
||||
is zero or negative, it inspects the full payload.
|
||||
|
||||
Status element bytes are parsed as repeated records:
|
||||
|
||||
| Record part | Meaning |
|
||||
| --- | --- |
|
||||
| marker byte bit 0-6 | `AsbStatusElementType` ID |
|
||||
| marker byte bit 7 set | element value is implicitly zero and no value bytes follow |
|
||||
| marker byte bit 7 clear | next two bytes are a little-endian `ushort` value |
|
||||
|
||||
Known status element type IDs are:
|
||||
|
||||
| ID | Name |
|
||||
| ---: | --- |
|
||||
| 1 | `OpcDaStatus` |
|
||||
| 2 | `OpcUaStatus` |
|
||||
| 3 | `OpcUaVendorStatus` |
|
||||
| 4 | `ScadaStatus` |
|
||||
| 5 | `MxStatusCategory` |
|
||||
| 6 | `MxStatusDetail` |
|
||||
| 7 | `MxQuality` |
|
||||
| 125 | `Reserved1Status` |
|
||||
| 126 | `Reserved2Status` |
|
||||
| 127 | `Reserved3Status` |
|
||||
|
||||
`MxQuality` is summarized by masking with `0x00C0`: `0x00C0` is good,
|
||||
`0x0040` is uncertain, and `0x0000` is bad. Unknown quality values and unknown
|
||||
status element types are preserved rather than guessed.
|
||||
|
||||
## Proven Versus Intentionally Open
|
||||
|
||||
Live/read/write coverage is complete for Boolean, Int32, Float, Double, String,
|
||||
DateTime, Duration, and the deployed array tags called out in the remaining-work
|
||||
notes. The non-live test suite also round-trips the supported factories through
|
||||
`DecodeVariant`, including the duration array path.
|
||||
|
||||
The client intentionally does not infer layouts for declared-but-unsupported ASB
|
||||
types such as GUID, byte strings, localized text, enum/data-type/security/data
|
||||
quality variants, and their array forms. Those types stay as raw bytes until a
|
||||
real deployed tag, capture, or contract evidence proves the payload shape.
|
||||
|
||||
Buffered publish batches are also intentionally not described here. The current
|
||||
ASB buffered flag is exposed, but observed publish responses still carry a
|
||||
single current `RuntimeValue` per item rather than a proven multi-sample batch
|
||||
payload.
|
||||
@@ -0,0 +1,975 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,498 @@
|
||||
# Current Sprint State
|
||||
|
||||
Last updated: 2026-04-26 local VM time.
|
||||
|
||||
## Build Status
|
||||
|
||||
- `MxNativeCodec.Tests`: passed.
|
||||
- `MxNativeClient.Tests`: passed, including compatibility surface and recovery
|
||||
lifecycle event checks.
|
||||
- `MxTraceHarness`: Release build passed.
|
||||
- `MxNativeClient.Probe`: Release build passed, including
|
||||
`--probe-session-recover-multi`.
|
||||
- `MxDataConsumerProbe`: Release build passed. It is an x86 .NET Framework
|
||||
probe for `aaMxDataConsumer.dll` / `IDataConsumer`.
|
||||
- `AsbProxyProbe`: Release x64 .NET Framework build passed. It directly loads
|
||||
the MSIL ASB IData proxy/contract assemblies and proves x64 managed
|
||||
discovery, connection, register, read, write, readback, and published write
|
||||
completion against the live MxDataProvider. It also has an unregister compare
|
||||
probe for installed-proxy parity.
|
||||
- `MxAsbClient.Probe`: Release .NET 10 x64 build passed. It is a pure managed
|
||||
ASB client slice with no AVEVA assembly references. It reads the ASB solution
|
||||
shared secret through DPAPI, performs the ASB system-auth handshake, opens the
|
||||
net.tcp `IASBIDataV2` channel, and live validates register, read, write,
|
||||
readback, published write completion, unregister parity, and multi-item
|
||||
register/read for `TestChildObject.TestInt` plus
|
||||
`TestMachine_001.TestHistoryValue`.
|
||||
- `MxAsbClient.Probe`: clean Release build with zero warnings after adding an
|
||||
explicit `System.Security.Cryptography.Xml` `10.0.7` package reference and
|
||||
replacing the obsolete PBKDF2 constructor with `Rfc2898DeriveBytes.Pbkdf2`.
|
||||
- `MxAsbClient.Tests`: passed. The test project covers ASB `Variant` factory
|
||||
and decode/display round trips for bool, int, float, double, string,
|
||||
datetime, duration, and the matching bool/int/float/double/string/
|
||||
datetime/duration array wire payloads, `MonitoredItemValue` binary
|
||||
serializer round trips for publish responses, and ASB status payload decoding
|
||||
for publish quality/status mapping. It also covers the stable ASB connection
|
||||
options overload, endpoint validation, and friend-only debug payload helper
|
||||
visibility, plus write-completion option validation and cancellation-token
|
||||
surface, subscription/monitored-item option overloads, and publish result
|
||||
summary helpers. Latest focused non-live shaping tests also assert exact
|
||||
defaults and validation boundaries for `AsbConnectionOptions`,
|
||||
`AsbWriteOptions`, `AsbWriteCompletionOptions`, `AsbSubscriptionOptions`,
|
||||
and `AsbMonitoredItemOptions`, DTO shape for write-completion/readback
|
||||
results, null collection argument guards, empty/non-empty `AsbPublishResult`
|
||||
helpers, derived status-summary helpers, item-status array mapping, and both
|
||||
compatibility `Advise` overload signatures.
|
||||
- Credential scan over `analysis`, `docs`, `src`, `captures`, and `README.md`:
|
||||
no matches for the protected password patterns.
|
||||
|
||||
## Implemented Surface
|
||||
|
||||
- Public MXAccess method surface is accounted for: `Register`, `Unregister`,
|
||||
`AddItem`, `RemoveItem`, `Advise`, `UnAdvise`, `Write`, `WriteSecured`,
|
||||
`AuthenticateUser`, `ArchestrAUserToId`, `AddItem2`, `Write2`,
|
||||
`WriteSecured2`, `Suspend`, `Activate`, `AdviseSupervisory`,
|
||||
`AddBufferedItem`, and `SetBufferedUpdateInterval`.
|
||||
- Public event families are represented: `OnDataChange`, `OnWriteComplete`,
|
||||
`OperationComplete`, and `OnBufferedDataChange`.
|
||||
- `WriteSecured2` is implemented and live validated from .NET 10 x64 for the
|
||||
observed authenticated secured-write body shape.
|
||||
- `WriteSecured` remains intentionally unsupported because installed MXAccess
|
||||
returns `0x80004021` for the observed secured/verified paths before emitting a
|
||||
value-bearing NMX body.
|
||||
- Object/VARIANT timestamp overloads exist for `Write2` and `WriteSecured2`,
|
||||
matching the installed interop assembly's public API shape.
|
||||
- `SetHeartbeatSendInterval` is exposed and live validated through
|
||||
`MxNativeClient.Probe --probe-session-heartbeat --heartbeat-ticks=5
|
||||
--heartbeat-max-missed=3`.
|
||||
- Explicit recovery is implemented as `MxNativeSession.RecoverConnection()` and
|
||||
`MxNativeCompatibilityServer.RecoverConnection(serverHandle)`. The probe
|
||||
`--probe-session-recover --tag=TestChildObject.TestInt --value=321` live
|
||||
validated subscribe, recover, preserved subscription count, and write-through
|
||||
against local NmxSvc.
|
||||
- `RecoverConnectionAsync` adds a caller-controlled retry loop via
|
||||
`MxNativeRecoveryPolicy`. Automatic write retry remains intentionally absent
|
||||
because it can duplicate writes.
|
||||
- Recovery lifecycle is observable through `RecoveryAttemptStarted`,
|
||||
`RecoveryAttemptFailed`, and `RecoveryCompleted` on both `MxNativeSession`
|
||||
and the managed compatibility facade. A live recovery probe with
|
||||
`--recover-attempts=2 --recover-delay-ms=100 --value=323` observed one
|
||||
started event, zero failed events, and one completed event.
|
||||
- Callback, operation-status, reference-registration, unparsed-callback,
|
||||
compatibility data-change, buffered-data-change, and write-complete payloads
|
||||
now carry `IsDuringRecovery`. The current policy is pass-through with an
|
||||
explicit marker rather than suppression or buffering.
|
||||
- `MxNativeClient.Probe --probe-session-recover-multi` replays several active
|
||||
subscriptions and counts recovery-window callbacks by callback family.
|
||||
|
||||
## Runtime Evidence Added In Latest Sprints
|
||||
|
||||
- GR identified `TestMachine_001.TestHistoryValue` as a deployed, historized
|
||||
integer dynamic attribute.
|
||||
- Captures `121` and `122` proved context-bearing buffered registration/result
|
||||
bodies for `TestHistoryValue.property(buffer)` in context `TestMachine_001`.
|
||||
- Both supervisory and plain advise buffered captures still produced no native
|
||||
`Fire_OnBufferedDataChange` entry on this VM.
|
||||
- Reflection over `ArchestrA.MXAccess.dll` confirmed the 18-method public
|
||||
method surface, four event families, `MxStatus` layout, `MxStatusCategory`,
|
||||
`MxStatusSource`, and full `MxDataType` enum values.
|
||||
- Installed `Lmx.aaDCT` detail text has been folded into `MxStatusDetails` for
|
||||
details `16`-`61`, `541`, `542`, and `8017`.
|
||||
- Explicit recovery event reporting is implemented and live validated. The
|
||||
events report attempt number, maximum attempts, exception, and retry intent
|
||||
for failed attempts.
|
||||
- A live multi-subscription recovery probe replayed four subscriptions and
|
||||
preserved all four. It observed two data callbacks and four unparsed callbacks
|
||||
overall, but zero callbacks with `IsDuringRecovery=true` on this VM.
|
||||
- A follow-up churn run with `--recover-concurrent-writes` wrote
|
||||
`TestChildObject.TestInt` values `330`-`334` from a separate managed session
|
||||
during recovery. It preserved all four subscriptions and observed four data
|
||||
callbacks overall, including two with `IsDuringRecovery=true`. This validates
|
||||
the pass-through-with-marker callback policy under live traffic.
|
||||
- `aaMxDataConsumer.dll` was imported with `tlbimp` into
|
||||
`analysis\interop\Interop.aaMxDataConsumer.dll` and decompiled under
|
||||
`analysis\decompiled-interop\Interop.aaMxDataConsumer`. Registry CLSID
|
||||
`{85209FB2-0BA1-4594-BBC4-59D3DDAB823D}` maps to `MxDataConsumer Class`.
|
||||
- `MxDataConsumerProbe` can instantiate `DataConsumerClass`, register
|
||||
`IDataConsumerCallback`, resolve namespace strings to namespace ID `1`, and
|
||||
call `ResolveReference`, `subscribe`, `ActivateSuspend`, and
|
||||
`ProcessActivateSuspend2`. However, `IsConnected(namespaceId)` remains `0`
|
||||
for tested namespace strings (`Galaxy`, `ArchestrA`, `Lmx`, `localhost`,
|
||||
`ZB`), no registration/subscription responses are produced, and
|
||||
`ProcessActivateSuspend2` returns `0x8007139F` (`ERROR_INVALID_STATE`).
|
||||
- `DataClientClass` appears in the imported `aaMxDataConsumer` type library,
|
||||
but its CLSID `{73BC4121-FF89-4762-901C-206E2BD9FE87}` is not registered on
|
||||
this node; `MxDataConsumerProbe --probe-dataclient` reports
|
||||
`0x80040154` (`REGDB_E_CLASSNOTREG`) before endpoint tests can run.
|
||||
- Headless/ILSpy analysis of `aaMxDataConsumer.dll` shows the COM consumer is a
|
||||
mixed-mode wrapper around managed ASB data proxies. `CDataClientCLI` creates a
|
||||
`DataClientProxy`, stores the namespace string, starts an auto-connect
|
||||
worker, and `DataClientProxy.Initialize` passes that same string as
|
||||
`AccessName` to `IDataProxySelector.SelectProxyForLatestEndpoint`.
|
||||
- `ASBIDataV2Adapter.dll` from the GAC contains `IDataProxySelector`. It first
|
||||
calls `ASBDataV2Proxy.FindIDataEndpoint(accessName, DiscoveryScope.Global)`;
|
||||
if that returns endpoints it constructs `ASBDataV2Proxy`, otherwise it falls
|
||||
back to V1. The discovery scope is
|
||||
`domainname/<accessName>/global`.
|
||||
- `AsbProxyProbe` live x64 discovery found one `IASBIDataV2` endpoint for
|
||||
access name `ZB`:
|
||||
`net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2`.
|
||||
It found no endpoint for `Default_ZB_MxDataProvider`, `Galaxy`,
|
||||
`localhost`, or `ZB2`.
|
||||
- `AsbProxyProbe --access=ZB --connect` opened the ASB data proxy from an x64
|
||||
managed process. `ASBDataV2Proxy.Connect` returned `true`, channel state was
|
||||
`Opened`, and `PublishWriteComplete` returned `0x00000000` with no pending
|
||||
writes. This proves a managed x64 ASB route to LMX/NMX data services exists
|
||||
on this node.
|
||||
- `AsbProxyProbe --access=ZB --connect --register --read --tag=TestChildObject.TestInt`
|
||||
registered and read the test integer through `IASBIDataV2`. `RegisterItems`
|
||||
and `Read` both returned `0x00000000`; the item id was
|
||||
`18446462598732840961`; the read value was ASB `DataType.TypeInt32` (`4`)
|
||||
with payload `4E010000`, decoded as `334`.
|
||||
- `AsbProxyProbe --access=ZB --connect --register --read --write-int=401 --tag=TestChildObject.TestInt`
|
||||
accepted a basic ASB write and proved readback. The immediate `Write` result
|
||||
was globally successful, the per-item synchronous status was `0x0000001F`
|
||||
(`ArchestrAError.OperationWouldBlock`), the next read returned integer value
|
||||
`401`, and `PublishWriteComplete` then returned `0x00000020`
|
||||
(`ArchestrAError.PublishComplete`) with the original write handle
|
||||
`0xA5B20001` and per-item completion status `0x00000000`. This is the first
|
||||
direct evidence that ASB write completion polling can supply the missing
|
||||
operation-completion source without the unstable DataConsumer COM wrapper.
|
||||
- Forcing the x86 `MxDataConsumerProbe` to `--namespace=ZB` hung in the COM
|
||||
wrapper path and was stopped. The direct ASB route is currently the cleaner
|
||||
path for x64 managed implementation and for any future `OperationComplete`
|
||||
trigger testing.
|
||||
- The pure .NET 10 ASB port in `src\MxAsbClient` now proves the live data path
|
||||
without loading AVEVA assemblies. Key auth details recovered during the port:
|
||||
this VM's ASB solution registry key is `Archestra_DESKTOP-6JL3KKO`,
|
||||
`HashAlgorthim=None`, `keySize=256`, and the solution overrides the DH prime
|
||||
with the installed 768-bit value. The remaining compatibility fixes were the
|
||||
AVEVA CLR data-contract namespaces/field ordering for `ConnectionValidator`,
|
||||
`WriteValue`, `ItemStatus`, `ItemIdentity`, and `ItemWriteComplete`, plus
|
||||
mutable-struct-safe binary decode for nested `Variant` and `ASBStatus`
|
||||
values.
|
||||
- `MxAsbClient.Probe --tag=TestChildObject.TestInt --write-int=412` completed
|
||||
from .NET 10 x64. The run read the previous value `411`, accepted write
|
||||
`412` with immediate per-item `0x0000001F` (`OperationWouldBlock`), read back
|
||||
`412`, and decoded `PublishWriteComplete` result `0x00000020`, count `1`,
|
||||
handle `0xA5B21001`, and final per-item status `0x00000000`.
|
||||
- `RegisterItems` parity is resolved for the observed startup race. ASB
|
||||
`AuthenticateMe` is a one-way WCF call; the first immediate `RegisterItems`
|
||||
can arrive before `ASBIDataV2Shim.AuthenticateMe` adds the connection
|
||||
implementation. The client now matches installed proxy signing for normal
|
||||
data calls and retries `RegisterItems` briefly when the immediate result is
|
||||
`0x00000001` (`InvalidConnectionId`). Stage21 evidence shows first register
|
||||
message `2` returned `InvalidConnectionId`, retry message `3` returned
|
||||
`0x00000000` with item id `18446462598732840961`, and later calls on the
|
||||
same connection succeeded.
|
||||
- `UnregisterItems` is implemented in the pure .NET 10 ASB port and compared
|
||||
against the installed AVEVA `ASBDataV2Proxy`. Both clients return global
|
||||
success `0x00000000` and the same per-item `0x0000000B`
|
||||
(`OperationFailed`) for the registered `TestChildObject.TestInt` identity on
|
||||
this provider. Evidence:
|
||||
`analysis\proxy\mxasbclient-probe-stage23-unregister-id.txt` and
|
||||
`analysis\proxy\asbproxyprobe-unregister-compare.txt`.
|
||||
- Multi-item ASB register/read is implemented and live validated in the pure
|
||||
.NET 10 port. `analysis\proxy\mxasbclient-probe-stage24-multi-read.txt`
|
||||
registers and reads `TestChildObject.TestInt` and
|
||||
`TestMachine_001.TestHistoryValue` in single two-item requests. Both register
|
||||
and read return global `0x00000000` and per-item `0x00000000`; the read values
|
||||
decode as ASB `TypeInt32` with previews `412` and `303`.
|
||||
- The pure .NET 10 ASB port now has non-live ASB `Variant` construction and
|
||||
decode/display coverage for the core scalar and array type matrix:
|
||||
`TypeBool`, `TypeInt32`, `TypeFloat`, `TypeDouble`, `TypeString`,
|
||||
`TypeDateTime`, `TypeDuration`, and their matching array forms. The public
|
||||
client also exposes a generic `Write(tag, Variant, writeHandle)` path so the
|
||||
next live write probes can reuse the same encoder instead of being hard-coded
|
||||
to `Int32`.
|
||||
- A live ASB register/read probe validated the deployed scalar and array read
|
||||
type matrix from .NET 10 x64:
|
||||
`TestChildObject.TestBool` -> `TypeBool` (`17`),
|
||||
`TestChildObject.TestInt` -> `TypeInt32` (`4`),
|
||||
`TestChildObject.TestFloat` -> `TypeFloat` (`8`),
|
||||
`TestChildObject.TestDouble` -> `TypeDouble` (`9`),
|
||||
`TestChildObject.TestString` -> `TypeString` (`10`),
|
||||
`TestChildObject.TestDateTime` -> `TypeDateTime` (`11`), and the matching
|
||||
`TestBoolArray[]`, `TestIntArray[]`, `TestFloatArray[]`,
|
||||
`TestDoubleArray[]`, `TestStringArray[]`, and `TestDateTimeArray[]` returning
|
||||
ASB array types `57`, `44`, `48`, `49`, `50`, and `51`. All register/read
|
||||
per-item statuses were `0x00000000`, and previews decoded correctly.
|
||||
- `MxAsbClient.Probe` now keeps SOAP envelopes and custom serializer byte dumps
|
||||
opt-in through `--dump-messages`; normal traced runs still show stages,
|
||||
statuses, type IDs, payload lengths, timestamps, and decoded previews without
|
||||
dumping request/response bodies.
|
||||
- Pure .NET 10 ASB scalar writes are live validated beyond `Int32`:
|
||||
`TestBool` accepted a `TypeBool` write and later read back `False` then
|
||||
`True`; `TestFloat` wrote/read `13.25`; `TestDouble` wrote/read `13.75`;
|
||||
`TestString` accepted `asb-scalar-write` and read it back on a delayed
|
||||
follow-up read; `TestDateTime` wrote/read `2026-04-26T14:33:00Z`.
|
||||
These runs confirm the scalar factory payloads are accepted by the live
|
||||
provider. Immediate `PublishWriteComplete` often returned count `0`, and
|
||||
bool/string immediate readback could be stale or time out, so completion
|
||||
polling/readback timing remains part of the production timeout policy gap.
|
||||
- Pure .NET 10 ASB array writes are live validated for the deployed dynamic
|
||||
array tags. `TestIntArray[]` accepted/read back `11`-`20`;
|
||||
`TestBoolArray[]` accepted/read back alternating false/true values;
|
||||
`TestFloatArray[]` and `TestDoubleArray[]` accepted/read back
|
||||
`11.1`-`20.1`; `TestStringArray[]` accepted/read back `K11`-`T20`; and
|
||||
`TestDateTimeArray[]` accepted/read back ten UTC timestamps from
|
||||
`2026-04-26T14:40:00Z` through `2026-04-26T14:49:00Z`. Int, bool, and string
|
||||
array readback needed a delayed follow-up read, matching the scalar timing
|
||||
behavior noted above.
|
||||
- Production cleanup removed the active ASB build warnings: the vulnerable
|
||||
transitive `System.Security.Cryptography.Xml` `10.0.0` package is overridden
|
||||
with `10.0.7`, `dotnet list package --include-transitive --vulnerable` now
|
||||
reports no vulnerable packages for `MxAsbClient`, and the system-auth
|
||||
PBKDF2 derivation uses the non-obsolete static API while preserving the
|
||||
installed proxy's 1000-iteration SHA1 derivation inputs. A live ASB read of
|
||||
`TestChildObject.TestInt` still succeeds after the authenticator change.
|
||||
- Pure .NET 10 ASB subscription/publish is now live-proven for the basic
|
||||
lifecycle. `MxAsbClient.Probe --subscribe --publish-count=3` created
|
||||
subscription id `3`, added monitored items for `TestChildObject.TestInt` and
|
||||
`TestChildObject.TestString` with per-item status `0x00000000`, received both
|
||||
values on the second publish poll (`TypeInt32` preview `412` and `TypeString`
|
||||
preview `asb-scalar-write`), deleted the subscription with global success,
|
||||
and then completed the existing unregister cleanup path. Publish responses
|
||||
returned global `0x00000020`, matching the previously observed completion
|
||||
queue success signal, so exact result/status mapping remains open.
|
||||
- The ASB publish path now maps raw `MonitoredItemValue` responses into
|
||||
callback-ready values with item-id-to-tag-name resolution, decoded value,
|
||||
UTC timestamp, `MxQuality`, and parsed ASB status elements. A live publish
|
||||
run for `TestChildObject.TestInt` and `TestChildObject.TestString` decoded
|
||||
quality `0x00C0` and status elements
|
||||
`OpcUaStatus:0|OpcUaVendorStatus:0|MxStatusCategory:0|MxStatusDetail:0|MxQuality:192`
|
||||
for both values. The same run added `DeleteMonitoredItems`, returned global
|
||||
success and per-item `0x00000000` for both monitored identities, then deleted
|
||||
the subscription successfully.
|
||||
- `MxAsbDataClient` now exposes `PublishedValueReceived` plus `PublishValues`.
|
||||
`PublishValues` keeps the raw `PublishResponse`, maps values through the
|
||||
client's monitored-item id cache, and raises one event per mapped value. A
|
||||
live probe observed `published_event[0]` for `TestChildObject.TestInt` and
|
||||
`published_event[1]` for `TestChildObject.TestString`, both with quality
|
||||
`0x00C0` and expected previews.
|
||||
- `MxAsbCompatibilityServer` now adapts the pure ASB stream into an MXAccess-like
|
||||
server/item-handle data-change facade. A live
|
||||
`MxAsbClient.Probe --compat-subscribe --publish-count=3` run registered ASB
|
||||
server handle `1`, added/advised item handles `1` and `2`, polled publish,
|
||||
raised two `compat_data_change` events with values `412` and
|
||||
`asb-scalar-write`, quality `0x00C0`, decoded ASB status elements, removed
|
||||
both monitored items, and unregistered cleanly.
|
||||
- The compatibility facade now has an `Advise` overload that accepts
|
||||
`AsbSubscriptionOptions` plus `AsbMonitoredItemOptions`, matching the public
|
||||
ASB subscription shaping instead of forcing positional primitives. The probe
|
||||
compatibility subscribe path now exercises this route, and a live
|
||||
compatibility subscribe run through the options overload registered/advised
|
||||
item handles, published mapped `compat_data_change` events, removed monitored
|
||||
items, and unregistered cleanly.
|
||||
- Initial non-success ASB item-status mapping is implemented. A live probe for
|
||||
`DefinitelyMissingObject.DefinitelyMissingAttribute` showed global register
|
||||
and read success but per-item read error `0x000A`
|
||||
(`InvalidMonitoredItems`), an empty value with value-status payload decoded
|
||||
as `OpcUaStatus:32905|OpcUaVendorStatus:0|MxStatusCategory:750|MxStatusDetail:0|MxQuality:0`,
|
||||
and unregister per-item error `0x000B` (`OperationFailed`). Unknown payloads
|
||||
are still preserved rather than guessed.
|
||||
- `MxAsbClient.Probe --probe-error-cases` now keeps broader live ASB
|
||||
non-success evidence opt-in without changing production client behavior.
|
||||
Evidence in `analysis\proxy\mxasbclient-probe-error-cases-v2.txt` covers:
|
||||
invalid read target global success with per-item `0x000A`
|
||||
(`InvalidMonitoredItems`) plus bad value status; invalid-target async write
|
||||
immediate per-item `0x001F` (`OperationWouldBlock`) followed by
|
||||
`PublishWriteComplete` global `0x0020` (`PublishComplete`) and completion
|
||||
per-item `0x0006` (`MonitoredItemsNotFound`) with status payload
|
||||
`OpcUaStatus:6|OpcUaVendorStatus:0|MxStatusCategory:4|MxStatusDetail:6`;
|
||||
wrong-type string write to `TestChildObject.TestInt` immediate per-item
|
||||
`0x001F`, completion per-item `0x0013`
|
||||
(`WriteFailedBadTypeMismatch`) with status payload
|
||||
`OpcUaStatus:19|OpcUaVendorStatus:0|MxStatusCategory:4|MxStatusDetail:8001`,
|
||||
and unchanged readback value `412`; invalid monitored-item cleanup global
|
||||
success with per-item `0x000B` (`OperationFailed`); invalid subscription
|
||||
delete global `0x000C`, status `0x0020`, specific `0x80020000`; and empty
|
||||
subscription publish returning global `0x0020` with no values.
|
||||
- ASB result/status summaries now name the full installed `ArchestrAError`
|
||||
code set, classify publish `0x0020` (`PublishComplete`) as success-like for
|
||||
publish polling, summarize `MxQuality` as good/uncertain/bad/unknown, and map
|
||||
known MX status details `16` and `17` to `RequestTimedOut` and
|
||||
`BadNoCommunication` while preserving raw category/detail/quality values.
|
||||
- Async ASB write completion now has a production-oriented polling helper.
|
||||
`WaitForWriteComplete` polls `PublishWriteComplete` for a target write
|
||||
handle until completion or timeout, preserving every raw poll response and
|
||||
completion record. `WaitForWriteCompleteAndRead` adds optional delayed
|
||||
readback without retrying or duplicating the write. A live probe using
|
||||
`--write-int=412 --wait-write-complete --write-complete-timeout-ms=5000
|
||||
--write-complete-poll-ms=250 --write-readback-delay-ms=500` saw the first
|
||||
completion poll return count `0`, the second poll return the matching
|
||||
handle `0xA5B21001`, and readback return `412`.
|
||||
- ASB client disposal now has explicit cleanup reporting. `Dispose()` delegates
|
||||
to idempotent `Cleanup()`, sends signed `Disconnect` best-effort, closes the
|
||||
channel and factory independently, aborts safely after close/fault failures,
|
||||
and records `LastCleanupResult` with `Completed`, `Succeeded`,
|
||||
`UsedAbortFallback`, and `RequiresNewConnection`. A live read probe ended
|
||||
with `asb.stage=cleanup`, `asb.stage=disconnect`, and
|
||||
`asb.cleanup.succeeded=True`.
|
||||
- ASB reconnect now has an explicit opt-in API. `MxAsbDataClient.Reconnect`
|
||||
validates caller retry options, optionally cleans up the current connection,
|
||||
returns a new client on success, and preserves attempt plus cleanup evidence
|
||||
without silently replaying reads or writes. A live
|
||||
`MxAsbClient.Probe --probe-reconnect --reconnect-attempts=2
|
||||
--reconnect-delay-ms=250 --cleanup-disconnect-timeout-ms=5000
|
||||
--cleanup-close-timeout-ms=5000` run read `TestChildObject.TestInt` as
|
||||
`412`, cleaned up and disconnected the original connection with caller
|
||||
cleanup timeouts, connected successfully on attempt `1`, re-registered the
|
||||
tag on the new client, read back `412`, and completed final cleanup
|
||||
successfully.
|
||||
- ASB cleanup partial-failure behavior is now covered in focused local tests
|
||||
without forcing live provider faults. The close-or-abort policy is factored
|
||||
into `AsbCommunicationCleanup`, and tests cover already-closed objects,
|
||||
faulted objects that skip close and abort directly, close failure followed by
|
||||
abort fallback, abort failure reporting, and propagation of the configured
|
||||
close timeout.
|
||||
- ASB cleanup cancellation now has explicit policy. `AsbClientCleanupOptions`
|
||||
accepts a `CancellationToken`; when cancellation is already requested, cleanup
|
||||
skips graceful disconnect/close attempts and uses local abort cleanup for
|
||||
non-closed channel/factory objects. Existing disconnect and close timeouts
|
||||
still bound synchronous WCF calls that have already started. A live
|
||||
`MxAsbClient.Probe --probe-canceled-cleanup` run connected, registered, and
|
||||
read `TestChildObject.TestInt` as `412`, then skipped disconnect and aborted
|
||||
both opened communication objects locally. The cleanup result completed with
|
||||
`succeeded=False`, `abort=True`, `requires_new=True`, and an
|
||||
`OperationCanceledException` disconnect marker.
|
||||
- Safe connection-failure evidence is now captured without destabilizing the
|
||||
live ASB provider. `MxAsbClient.Probe --probe-connect-failure
|
||||
--endpoint=net.tcp://localhost:1/ASBService/Default_ZB_MxDataProvider/IDataV2`
|
||||
reports `connect_failure_observed=True`, an `EndpointNotFoundException`, and
|
||||
inner `SocketException` `10061` for a refused local TCP endpoint. `Connect`
|
||||
now also closes or aborts any WCF channel/factory objects created before a
|
||||
connection setup failure; the refused-endpoint probe reports
|
||||
`asb.stage=connect-cleanup` before surfacing the exception.
|
||||
- OperationComplete candidate evidence was narrowed on the direct ASB route.
|
||||
The `IASBIDataV2` contract exposed by the live endpoint has no activate,
|
||||
suspend, or generic operation-complete operation; only `PublishWriteComplete`
|
||||
exists, and it is write-specific. A live
|
||||
`MxAsbClient.Probe --probe-operation-complete-candidates` run against
|
||||
`TestChildObject.TestInt` and `TestChildObject.TestString` polled
|
||||
`PublishWriteComplete` before and after create-subscription, add-monitored,
|
||||
publish, delete-monitored, and delete-subscription operations. Every poll
|
||||
returned count `0`, while the non-write operations themselves succeeded.
|
||||
- ASB buffered-subscription probing is now explicit. `MxAsbDataClient` exposes
|
||||
the `MonitoredItem.Buffered` flag through `AddMonitoredItems`, and
|
||||
`MxAsbClient.Probe --subscribe-buffered` sets it. A live buffered ASB publish
|
||||
probe against the historized `TestMachine_001.TestHistoryValue` succeeded,
|
||||
but returned the same single current `MonitoredItemValue` shape as the normal
|
||||
publish path: value `303`, quality `0x00C0`, timestamp
|
||||
`2026-04-26T02:33:45.4260000Z`, and no multi-sample buffered batch.
|
||||
- ASB status mapping now includes installed MX detail `33`
|
||||
(`WriteAccessDenied`) and maps it to `WriteFailedAccessDenied`. Tests cover
|
||||
success, invalid monitored item, wrong-type write completion, invalid cleanup
|
||||
evidence inputs, request timeout detail `16`, platform/no-communication
|
||||
detail `17`, and access-denied detail `33`.
|
||||
- Safe access-denied/no-communication candidate probes did not produce those
|
||||
source conditions on this VM. Same-value direct ASB writes to secured
|
||||
`TestMachine_001.ProtectedValue` and verified
|
||||
`TestMachine_001.ProtectedValue1` both completed successfully and read back
|
||||
unchanged, so direct system-auth ASB writes do not reproduce the MXAccess
|
||||
public secured-write rejection path here. A read-only sweep of
|
||||
`TestMachine_001.ProtectedValue` through `TestMachine_020.ProtectedValue`
|
||||
registered and read all 20 values with good quality `0x00C0`; no
|
||||
no-communication status was observed.
|
||||
- Native NMX completion-only operation-status handling is now stricter. One-byte
|
||||
completion-only frames remain preserved as raw, unpromoted completion statuses
|
||||
even when the byte is `0x00`; only the observed 5-byte status-word frame
|
||||
`00 00 50 80 00` maps to `MxStatus.WriteCompleteOk` and can raise the
|
||||
MXAccess-compatible write-complete event. Tests cover completion-only
|
||||
`0x00`, `0x41`, and `0xEF`, plus the promoted `0x8050/0x00` status-word
|
||||
frame. A search of the available Ghidra/decompiled outputs did not expose a
|
||||
completion-byte-to-`MXSTATUS_PROXY[]` mapping table beyond this public-event
|
||||
evidence.
|
||||
- Public ASB connection shaping has started. `AsbConnectionOptions` is now the
|
||||
stable options object for `MxAsbDataClient.Connect`, the previous string
|
||||
overload delegates to it, and `MxAsbCompatibilityServer.Register` has a
|
||||
matching options overload. Endpoint validation now happens before registry or
|
||||
WCF setup work. `AsbPayloadDebug` is no longer part of the public surface and
|
||||
is friend-only for the test/probe assemblies. A clean Release probe build,
|
||||
non-live ASB contract test run, and live read of `TestChildObject.TestInt`
|
||||
all passed after this change.
|
||||
- ASB write-completion options now validate their own polling/readback policy
|
||||
and expose a `CancellationToken`. Cancellation is checked before each
|
||||
`PublishWriteComplete` poll and during poll/readback delays; already-running
|
||||
synchronous ASB calls remain bounded by the existing WCF operation behavior.
|
||||
A live same-value write-completion probe still completed on the second poll
|
||||
for handle `0xA5B21001` and read back `412`.
|
||||
- ASB subscription API shaping now has `AsbSubscriptionOptions` and
|
||||
`AsbMonitoredItemOptions` overloads while preserving the existing positional
|
||||
overloads. A live subscribe/publish probe created subscription `15`, added
|
||||
monitored items for `TestChildObject.TestInt` and
|
||||
`TestChildObject.TestString`, published both mapped values/events, deleted
|
||||
the monitored items, deleted the subscription, and cleaned up the ASB
|
||||
connection successfully.
|
||||
- `AsbPublishResult` now exposes a derived `Result` summary and `HasValues`
|
||||
helper so callers do not have to remap raw `PublishResponse.Result` on every
|
||||
publish poll. The existing raw response and mapped values remain preserved.
|
||||
- Public API/request-shaping tests were expanded without live ASB dependency.
|
||||
They now pin defaults, validation, overload visibility, and helper DTO shape
|
||||
for connection, write completion/readback, subscription, monitored-item,
|
||||
compatibility `Advise`, and publish-result surfaces.
|
||||
- Basic writes now also have an `AsbWriteOptions` overload carrying the write
|
||||
handle and optional comment. The existing positional write overload and
|
||||
`WriteInt32` path delegate through it, preserving behavior; a live same-value
|
||||
write-completion probe still completed on the second poll and read back
|
||||
`412`.
|
||||
- Public multi-item APIs now explicitly reject null collection arguments before
|
||||
touching client state: `RegisterMany`, `UnregisterMany`, `ReadMany`,
|
||||
option-based `AddMonitoredItems`, and `DeleteMonitoredItems`. Empty
|
||||
collections and tag/item contents keep their prior behavior.
|
||||
- Published values and compatibility data-change events now expose derived
|
||||
`StatusSummary` helpers, and `AsbResultMapper.ToItemSummaries` maps nullable
|
||||
item-status arrays into raw-preserving summaries with null/empty as an empty
|
||||
result.
|
||||
- `docs\ASB-Variant-Wire-Format.md` now captures the supported ASB `Variant`
|
||||
wire format: type IDs, scalar and array payload layouts, timestamp/duration
|
||||
handling, string encoding, runtime value wrapping, quality/status payload
|
||||
parsing, and intentionally raw-preserved unsupported types.
|
||||
- `docs\ASB-Native-Integration-Decision.md` now records the recommended
|
||||
integration boundary: prefer `MxAsbClient` for the regular tag data plane
|
||||
behind a higher-level compatibility routing facade, keep `MxNativeClient` for
|
||||
native callback-only semantics and not-yet-proven MXAccess behaviors, and
|
||||
avoid merging ASB WCF lifecycle internals into the NMX DCE/RPC session.
|
||||
|
||||
## Remaining Highest-Value Gaps
|
||||
|
||||
1. Additional live faulted-channel and source-condition evidence that can be
|
||||
gathered safely. Current safe secured/verified-write and protected-value
|
||||
read sweeps did not produce access-denied or no-communication statuses.
|
||||
2. Broaden pure .NET 10 ASB only where safe source conditions expose new
|
||||
behavior. Local ASB `Variant` encode/decode, live reads/writes,
|
||||
subscription/create/add/publish/delete-monitored/delete-subscription,
|
||||
mapped publish events, compatibility data-change facade, options-shaped
|
||||
connection/write/subscription/advise/write-completion APIs, cleanup, and
|
||||
reconnect are now covered. Remaining work is mainly external failure modes
|
||||
such as access denied or no-communication when a safe source condition is
|
||||
available.
|
||||
3. A native runtime trigger for `OperationComplete`. The direct ASB path now
|
||||
proves a write-completion queue (`PublishWriteComplete`) carrying write
|
||||
handles and per-item statuses. Direct `IASBIDataV2` does not expose
|
||||
activate/suspend or a generic operation-complete contract, and subscription
|
||||
create/add/publish/delete operations did not enqueue write-completion
|
||||
records. Map to MXAccess event ID `3` only if a separate native trigger is
|
||||
found.
|
||||
4. A native runtime/source condition that emits `OnBufferedDataChange` sample
|
||||
batches. Direct ASB buffered monitored items are accepted by this provider,
|
||||
but the observed publish response still carries one current value rather
|
||||
than a native MXAccess buffered sample batch.
|
||||
5. Exact mapping for completion-only operation-status bytes into
|
||||
`MXSTATUS_PROXY[]`; current code now preserves all completion-only bytes as
|
||||
unpromoted raw statuses instead of guessing, including completion-only
|
||||
`0x00`.
|
||||
6. Cleanup behavior around live faulted/partial failure cases when a safe
|
||||
provider condition is available.
|
||||
7. Less-common ASB variant types only when real deployed tags expose them; the
|
||||
currently supported/proven variant layout is documented and unsupported
|
||||
declared types remain raw-preserved.
|
||||
|
||||
## Recommended Next Sprint
|
||||
|
||||
Treat the public ASB API-shaping sprint as closed unless a concrete caller gap
|
||||
appears. The remaining local documentation gaps are also closed. Return to exact
|
||||
completion-only operation-status mapping into `MXSTATUS_PROXY[]` only if a new
|
||||
safe native public-event capture or deeper targeted decompile can provide
|
||||
evidence. Keep live faulted-channel, access-denied, and source
|
||||
no-communication probes opportunistic unless a safe provider condition appears.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,328 @@
|
||||
# Ghidra headless analysis
|
||||
|
||||
This note captures the headless Ghidra pass used to identify native MX/LMX/NMX
|
||||
function boundaries and build Frida hook targets.
|
||||
|
||||
## Tooling
|
||||
|
||||
Ghidra was reused from:
|
||||
|
||||
```text
|
||||
C:\Users\dohertj2\Desktop\focas\tools\ghidra_12.0.4_PUBLIC
|
||||
```
|
||||
|
||||
Java was available but not on `PATH`, so headless commands set:
|
||||
|
||||
```text
|
||||
JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot
|
||||
```
|
||||
|
||||
The AVEVA binaries were staged under a path without spaces:
|
||||
|
||||
```text
|
||||
analysis\ghidra\input
|
||||
```
|
||||
|
||||
The headless post-script is:
|
||||
|
||||
```text
|
||||
analysis\ghidra\scripts\MxNmxExport.java
|
||||
```
|
||||
|
||||
It exports metadata, not full decompiled source:
|
||||
|
||||
```text
|
||||
analysis\ghidra\exports\*.ghidra.md
|
||||
analysis\ghidra\exports\*.functions.tsv
|
||||
analysis\ghidra\exports\*.string-refs.tsv
|
||||
analysis\ghidra\exports\*.call-refs.tsv
|
||||
```
|
||||
|
||||
## Static targets
|
||||
|
||||
High-value functions from `LmxProxy.dll`:
|
||||
|
||||
| Function | RVA | Evidence |
|
||||
| --- | ---: | --- |
|
||||
| `CLMXProxyServer::Write` variant A | `0x12c0c` | references `CLMXProxyServer::Write - Server Handle` |
|
||||
| `CLMXProxyServer::Write` variant B | `0x13280` | references `CLMXProxyServer::Write - Server Handle` |
|
||||
| `CLMXProxyServer::WriteSecured` variant A | `0x12f24` | references secured-write strings |
|
||||
| `CLMXProxyServer::WriteSecured` variant B | `0x135fe` | references secured-write strings |
|
||||
| `CLMXProxyServer::AdviseSupervisory` | `0x142b4` | references advise-supervisory strings |
|
||||
|
||||
High-value functions from `NmxAdptr.dll`:
|
||||
|
||||
| Function | RVA | Evidence |
|
||||
| --- | ---: | --- |
|
||||
| `CNmxAdapter::TransferData` | `0x10996` | references `CNmxAdapter::TransferData` strings |
|
||||
| `ProcessDataReceived` | `0x112da` | references invalid NMX request/response strings |
|
||||
| `CNmxAdapter::PutRequest` | `0x15169` | references `CNmxAdapter::PutRequest` strings |
|
||||
| `CNmxAdapter::PutRequestEx` | `0x159c3` | references `CNmxAdapter::PutRequestEx` strings |
|
||||
|
||||
`LmxProxy.dll` has no direct Winsock callsites in the exported call refs. It is
|
||||
COM, `VARIANT`, `BSTR`, and `SAFEARRAY` heavy. `NmxAdptr.dll` and `NmxSvc.exe`
|
||||
contain the relevant NMX transport/body functions.
|
||||
|
||||
`NmxSvcps.dll` is confirmed as a MIDL proxy/stub DLL. Its exports call:
|
||||
|
||||
```text
|
||||
NdrDllGetClassObject
|
||||
NdrDllCanUnloadNow
|
||||
NdrDllRegisterProxy
|
||||
NdrDllUnregisterProxy
|
||||
NdrCStdStubBuffer_Release
|
||||
```
|
||||
|
||||
## Runtime hook result
|
||||
|
||||
Frida hook script:
|
||||
|
||||
```text
|
||||
analysis\frida\mx-nmx-trace.js
|
||||
analysis\scripts\run_frida_mx_trace.ps1
|
||||
analysis\scripts\extract_frida_trace.py
|
||||
```
|
||||
|
||||
Successful traces:
|
||||
|
||||
```text
|
||||
captures\022-frida-write-test-int-sequence-106-108
|
||||
captures\023-frida-write-test-int-sequence-109-111
|
||||
captures\024-frida-write-test-bool-sequence
|
||||
captures\025-frida-write-test-float-sequence
|
||||
captures\026-frida-write-test-double-sequence
|
||||
captures\027-frida-write-test-string-sequence
|
||||
captures\028-frida-write-test-datetime-sequence
|
||||
captures\029-frida-write-test-int-array
|
||||
captures\030-frida-write-test-bool-array
|
||||
captures\031-frida-write-test-float-array
|
||||
captures\032-frida-write-test-double-array
|
||||
captures\033-frida-write-test-string-array
|
||||
captures\035-frida-write-test-datetime-array-full
|
||||
captures\036-frida-write-secured-test-int
|
||||
captures\037-frida-write-secured2-test-int
|
||||
captures\038-frida-write-secured-protectedvalue
|
||||
captures\039-frida-write-secured-verified-protectedvalue1
|
||||
captures\040-frida-write-normal-secured-protectedvalue
|
||||
captures\041-frida-write-normal-verified-protectedvalue1
|
||||
captures\042-frida-write2-test-int-timestamp
|
||||
captures\043-frida-loopback-write-test-int-115
|
||||
captures\044-frida-loopback-write-test-int-123456789
|
||||
```
|
||||
|
||||
Trace `023` proves the scalar write value is visible before the localhost
|
||||
transport. Traces `024` through `028` extend that result across bool, float,
|
||||
double, string, and datetime writes.
|
||||
|
||||
At `CLMXProxyServer::Write` variant A:
|
||||
|
||||
| Field | Observed value |
|
||||
| --- | --- |
|
||||
| `args[1]` | session handle `1` |
|
||||
| `args[2]` | item handle `1` |
|
||||
| `args[3]` | `0x3`, consistent with `VT_I4` |
|
||||
| `args[5]` | requested int value: `0x6d`, `0x6e`, `0x6f` |
|
||||
| `args[7]` | user/security id `1` |
|
||||
|
||||
At `CNmxAdapter::PutRequest`:
|
||||
|
||||
| Field | Observed value |
|
||||
| --- | --- |
|
||||
| `args[6]` | body size `0x28` / `40` |
|
||||
| `args[7]` | body pointer |
|
||||
| body offset `18` | requested int value as little-endian `int32` |
|
||||
|
||||
The repeated 40-byte write body shape is:
|
||||
|
||||
```text
|
||||
37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00
|
||||
00 02 <value:int32-le> ff ff 00 00 00 00 00 00
|
||||
00 00 c9 14 b1 08 <write-index:int32-le>
|
||||
```
|
||||
|
||||
At `CNmxAdapter::TransferData`, the same 40-byte body is wrapped in an 86-byte
|
||||
buffer. The scalar value appears at offset `64`, which is `46 + 18`.
|
||||
|
||||
At `CNmxAdapter::ProcessDataReceived`, the callback/update body for the write
|
||||
value is 88 bytes and carries the scalar value at offset `84`.
|
||||
|
||||
Generated parser output:
|
||||
|
||||
```text
|
||||
captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv
|
||||
```
|
||||
|
||||
Value-hit summary from that TSV:
|
||||
|
||||
| Function | Body size | Hit |
|
||||
| --- | ---: | --- |
|
||||
| `CNmxAdapter::PutRequest` | `40` | `109@18`, `110@18`, `111@18` |
|
||||
| `CNmxAdapter::TransferData` | `86` | `109@64`, `110@64`, `111@64` |
|
||||
| `CNmxAdapter::ProcessDataReceived` | `88` | `109@84`, `110@84`, `111@84` |
|
||||
|
||||
## Write-body matrix
|
||||
|
||||
The machine-readable matrix is:
|
||||
|
||||
```text
|
||||
analysis\frida\write-body-matrix.tsv
|
||||
```
|
||||
|
||||
Observed write body encodings:
|
||||
|
||||
| Type | COM carrier | `PutRequest` | `TransferData` | `ProcessDataReceived` | Encoding notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `int` | `VT_I4` / `0x3`, `args[5]` | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `int32` |
|
||||
| `bool` | `VT_BOOL` / `0xb`, `args[5]` | size `37`, value offset `18` | size `83`, value offset `64` | size `85`, value offset `84` | true is `ff ff ff 00` in the write body and `ff` in the data-change body; false is `00 ff ff 00` and `00` |
|
||||
| `float` | `VT_R4` / `0x4`, `args[5]` | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `float32` |
|
||||
| `double` | `VT_R8` / `0x5`, `args[5]`/`args[6]` | size `44`, value offset `18` | size `90`, value offset `64` | size `92`, value offset `84` | little-endian `float64` |
|
||||
| `string` | `VT_BSTR` / `0x8` | size `58` or `60`, value offset `26` | size `104` or `106`, value offset `72` | size `106` or `108`, value offset `92` | UTF-16LE string payload; body size follows string length |
|
||||
| `datetime` | `VT_DATE` / `0x7`, `args[5]`/`args[6]` | size `86`, value offset `26` | size `132`, value offset `72` | size `98`, value offset `88` | outbound write uses a UTF-16LE display string like `4/25/2026 2:30:00 AM`; callback/update uses FILETIME |
|
||||
|
||||
The repeated numeric write bodies show a stable 18-byte prefix, scalar slot, and
|
||||
trailing status/counter fields. Variable-width types move the value to offset
|
||||
`26`, leaving an 8-byte descriptor before the UTF-16LE data.
|
||||
|
||||
## Array write-body matrix
|
||||
|
||||
The machine-readable array matrix is:
|
||||
|
||||
```text
|
||||
analysis\frida\write-array-body-matrix.tsv
|
||||
```
|
||||
|
||||
Observed array write body encodings:
|
||||
|
||||
| Type | COM carrier | `PutRequest` | `TransferData` | `ProcessDataReceived` | Encoding notes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `int[]` | `SAFEARRAY VT_I4` / `0x2003` | 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 little-endian `int32` |
|
||||
| `bool[]` | `SAFEARRAY VT_BOOL` / `0x200b` | 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`; capture `098` confirmed the x86 COM automation path can project a requested non-repeating .NET `bool[]` into a paired/shifted VARIANT_BOOL wire payload |
|
||||
| `float[]` | `SAFEARRAY VT_R4` / `0x2004` | 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 little-endian `float32` |
|
||||
| `double[]` | `SAFEARRAY VT_R8` / `0x2005` | 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 little-endian `float64` |
|
||||
| `string[]` | `SAFEARRAY VT_BSTR` / `0x2008` | size `256`, first string bytes at offset `41` | size `302`, first string bytes at offset `87` | size `304`, first string bytes at offset `107` | descriptor kind `0x45`; each element is a length-prefixed scalar string-style UTF-16LE record |
|
||||
| `datetime[]` | `SAFEARRAY VT_DATE` / `0x2007` | size `596`, first string bytes at offset `41` | size `642`, first string bytes at offset `87` | size `214`, first FILETIME at offset `94` | outbound values are per-element UTF-16LE display strings; callback/update uses packed FILETIME records |
|
||||
|
||||
Array bodies use an 11-byte descriptor beginning at body offset `17`:
|
||||
|
||||
```text
|
||||
kind_byte 00 00 00 00 element_count:uint16 element_width_or_code:uint32
|
||||
```
|
||||
|
||||
Packed numeric array values begin at body offset `28`. String and datetime
|
||||
arrays also begin their element records at offset `28`, with the first actual
|
||||
UTF-16LE character at offset `41`.
|
||||
|
||||
## Write mode matrix
|
||||
|
||||
The machine-readable write-mode matrix is:
|
||||
|
||||
```text
|
||||
analysis\frida\write-mode-matrix.tsv
|
||||
```
|
||||
|
||||
Findings:
|
||||
|
||||
| Scenario | Result |
|
||||
| --- | --- |
|
||||
| `WriteSecured` against `TestChildObject.TestInt` | rejected before value-bearing `PutRequest` with `0x80004021` |
|
||||
| `WriteSecured2` against `TestChildObject.TestInt` | rejected before value-bearing `PutRequest` with `E_INVALIDARG` |
|
||||
| `WriteSecured` against `TestMachine_001.ProtectedValue` (`SecuredWrite`) | rejected before value-bearing `PutRequest` with `0x80004021` |
|
||||
| `WriteSecured` against `TestMachine_001.ProtectedValue1` (`VerifiedWrite`) | rejected before value-bearing `PutRequest` with `0x80004021` |
|
||||
| normal `Write` against `ProtectedValue` with fourth argument `2` | succeeds; same bool body shape as scalar bool writes |
|
||||
| normal `Write` against `ProtectedValue1` with fourth argument `3` | succeeds; same bool body shape as scalar bool writes |
|
||||
| `Write2` against `TestChildObject.TestInt` | succeeds; `PutRequest` size remains `40`, value stays at offset `18`, FILETIME appears at offset `24` |
|
||||
|
||||
Implication: for MXAccess public automation, the supported secured/verified
|
||||
route is the regular `Write` method with the fourth argument set to the Galaxy
|
||||
security classification. The public `WriteSecured*` methods are present in the
|
||||
type library but did not produce a supported value-bearing request in these
|
||||
captures.
|
||||
|
||||
Later authenticated captures and headless decompile refined this:
|
||||
|
||||
- `analysis\ghidra\exports\LmxProxy.dll.write-secured-decompile.md` decompiles
|
||||
`FUN_10012f24` (`WriteSecured`) and `FUN_100135fe` (`WriteSecured2`).
|
||||
- `WriteSecured` performs an extra item-record byte check at offset `0x0f` and
|
||||
returns `0x80004021` when that byte is nonzero. The secured and verified bool
|
||||
tags on this node hit that branch even after `AuthenticateUser` succeeds.
|
||||
- `WriteSecured2` skips that item-record flag check. It resolves current and
|
||||
verifier user handles to 16-byte authenticator tokens, copies both the value
|
||||
and timestamp variants, and calls the downstream vtable slot that emitted the
|
||||
observed NMX `0x38` body in captures `113`-`116`.
|
||||
- The shared user-token lookup branch returns `0x80070057` when the user handle
|
||||
is not mapped, explaining the earlier unauthenticated `WriteSecured2` failure
|
||||
against `TestChildObject.TestInt`.
|
||||
|
||||
## Transport correlation
|
||||
|
||||
Combined Frida plus loopback capture:
|
||||
|
||||
```text
|
||||
captures\043-frida-loopback-write-test-int-115
|
||||
captures\044-frida-loopback-write-test-int-123456789
|
||||
analysis\scripts\run_frida_loopback_capture.ps1
|
||||
analysis\scripts\map_frida_to_tcp.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
|
||||
```
|
||||
|
||||
For the same write of `TestChildObject.TestInt = 115`, the mapper extracted the
|
||||
exact Frida `PutRequest`, `TransferData`, and callback bodies and searched the
|
||||
reassembled TCP streams. Result:
|
||||
|
||||
| Needle | TCP result |
|
||||
| --- | --- |
|
||||
| raw `int32` value `115` | present in several streams, including `::1:49704`, but surrounding bytes show DCE/RPC call IDs/metadata and unrelated product records |
|
||||
| exact 40-byte `PutRequest` body | not found |
|
||||
| exact 86-byte `TransferData` body | not found |
|
||||
| exact 88-byte callback body | not found |
|
||||
|
||||
Capture `044` repeats the test with `123456789`, which avoids DCE/RPC call-ID
|
||||
ambiguity. That raw scalar is absent from the full pcap payload scan, the
|
||||
`::1:49704` DCE/RPC stubs, and the mixed `127.0.0.1:57415 <-> 57433` stream.
|
||||
|
||||
This means the native adapter bodies are not copied verbatim onto TCP, and the
|
||||
write value is not exposed as a plain little-endian scalar in the transport for
|
||||
the distinctive-value capture. The next transport task is to decode the
|
||||
DCE/RPC/NDR layer and the mixed local stream messages structurally.
|
||||
|
||||
## Managed codec artifact
|
||||
|
||||
The first .NET 10 managed implementation artifact is:
|
||||
|
||||
```text
|
||||
src\MxNativeCodec\MxNativeCodec.csproj
|
||||
src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj
|
||||
```
|
||||
|
||||
This is intentionally template-based. It preserves unknown tag/session/header
|
||||
fields from an observed `PutRequest` body, then rewrites the typed value slot and
|
||||
write index. The tests round-trip the real Frida bytes for bool, int, float,
|
||||
double, string, datetime, timestamped `Write2` int, and the observed
|
||||
int/bool/float/double/string array write bodies.
|
||||
|
||||
Verification command:
|
||||
|
||||
```text
|
||||
dotnet run --project src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj -c Release
|
||||
```
|
||||
|
||||
The harness also now accepts typed array writes:
|
||||
|
||||
```text
|
||||
--type=int[] --value="1;2;3"
|
||||
--type=string[] --value="A;B;C"
|
||||
```
|
||||
|
||||
For multiple whole-array writes in one session, `--values=` can use `|` between
|
||||
array values so element commas are not ambiguous.
|
||||
|
||||
## Implication
|
||||
|
||||
The prior pcap-only decode showed local stream framing but did not isolate the
|
||||
application scalar. Headless Ghidra plus Frida closes that gap: the native NMX
|
||||
adapter receives compact bodies where scalar and string/date values are plainly
|
||||
encoded before they enter localhost transport. The next implementation task is
|
||||
to turn this matrix into encoder/decoder tests, then broaden the same approach
|
||||
to arrays, quality/status responses, tag binding messages, and secured-write
|
||||
variants.
|
||||
@@ -0,0 +1,267 @@
|
||||
# Loopback protocol findings
|
||||
|
||||
This note captures the first Npcap loopback decode pass for the MXAccess to
|
||||
LMX/NMX path.
|
||||
|
||||
## Capture set
|
||||
|
||||
The repeatable capture runner is:
|
||||
|
||||
```text
|
||||
analysis\scripts\run_loopback_capture.ps1
|
||||
```
|
||||
|
||||
It starts `dumpcap` on the Npcap loopback adapter and runs the x86 MXAccess
|
||||
harness:
|
||||
|
||||
```text
|
||||
src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
|
||||
```
|
||||
|
||||
Focused loopback captures:
|
||||
|
||||
| Folder | Scenario |
|
||||
| --- | --- |
|
||||
| `captures\013-loopback-subscribe-scalars` | subscribe `TestBool`, `TestInt`, `TestString` |
|
||||
| `captures\014-loopback-subscribe-array-bracketed` | subscribe `TestStringArray[]` |
|
||||
| `captures\015-loopback-subscribe-invalid` | subscribe invalid reference |
|
||||
| `captures\016-loopback-write-test-int-advised` | advised same-value write to `TestInt` |
|
||||
|
||||
Per-capture generated files:
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `loopback.pcapng` | raw Npcap loopback capture |
|
||||
| `harness.log` | timestamped MXAccess method and callback log |
|
||||
| `dcerpc-49704.tsv` | tshark DCE/RPC frame index for the NMX service port |
|
||||
| `nmx-conversations.tsv` | extracted IPv6 conversations involving port `49704` |
|
||||
| `nmx-payload-packets.tsv` | payload packet index for the selected `49704` conversation |
|
||||
| `nmx-stream-*.bin` | reassembled directional payloads for the selected `49704` conversation |
|
||||
| `tcp-conversations.tsv` | all TCP payload conversations ranked by byte count |
|
||||
| `tcp-payload-packets.tsv` | payload packet index for the top TCP conversations |
|
||||
| `tcp-stream-*.bin` | reassembled directional payloads for the top TCP conversations |
|
||||
|
||||
Cross-capture summaries:
|
||||
|
||||
```text
|
||||
analysis\scripts\extract_nmx_loopback.py
|
||||
analysis\scripts\extract_tcp_conversations.py
|
||||
analysis\scripts\decode_tcp_payload_packets.py
|
||||
analysis\scripts\decode_mixed_local_stream.py
|
||||
analysis\scripts\summarize_dcerpc.py
|
||||
analysis\network\dcerpc-loopback-summary.tsv
|
||||
analysis\network\write-window-tcp-payloads.tsv
|
||||
```
|
||||
|
||||
## DCE/RPC service path
|
||||
|
||||
Wireshark decodes the main `::1:<ephemeral> <-> ::1:49704` traffic as DCE/RPC.
|
||||
The captures show these DCE/RPC interface UUIDs:
|
||||
|
||||
| UUID | Observed use |
|
||||
| --- | --- |
|
||||
| `4e0c90df-e39d-4164-a421-ace89484c602` | initial bind context, opnum `0` calls |
|
||||
| `1981974b-6bf7-46cb-9640-0260bbb551ba` | altered context, main opnums `0`, `2`, `3`, `5` |
|
||||
|
||||
The UUIDs were not present as direct keys under the checked COM registry areas:
|
||||
|
||||
```text
|
||||
HKCR\Interface
|
||||
HKCR\Wow6432Node\Interface
|
||||
HKCR\CLSID
|
||||
HKCR\Wow6432Node\CLSID
|
||||
HKCR\TypeLib
|
||||
HKCR\Wow6432Node\TypeLib
|
||||
```
|
||||
|
||||
This makes `NmxSvcps.dll` and `WWProxyStub.dll` high-value next targets. They
|
||||
are likely proxy/stub components, but their interfaces are not exposed through
|
||||
the simple COM registry lookup above.
|
||||
|
||||
## DCE/RPC shape
|
||||
|
||||
For good scalar subscribe, good array subscribe, and successful advised write,
|
||||
the main `49704` DCE/RPC shape is identical:
|
||||
|
||||
```text
|
||||
165 request/response pairs on ctx 1, opnum 3
|
||||
10 request/response groups on ctx 0 opnum 0, ctx 1 opnum 0, ctx 1 opnum 2
|
||||
3 request/response pairs on ctx 1, opnum 5
|
||||
```
|
||||
|
||||
The invalid subscribe adds one extra group:
|
||||
|
||||
```text
|
||||
176 request/response pairs on ctx 1, opnum 3
|
||||
11 request/response groups on ctx 0 opnum 0, ctx 1 opnum 0, ctx 1 opnum 2
|
||||
3 request/response pairs on ctx 1, opnum 5
|
||||
```
|
||||
|
||||
Interpretation: invalid item resolution does not fail at `AddItem`; it drives
|
||||
additional NMX/RPC activity during `AdviseSupervisory`, matching the harness
|
||||
callbacks where invalid names return configuration errors only after advise.
|
||||
|
||||
## Write path split
|
||||
|
||||
The successful write capture has the standard main `49704` DCE/RPC conversation:
|
||||
|
||||
```text
|
||||
::1:55840 <-> ::1:49704
|
||||
803 frames, about 84 kB
|
||||
```
|
||||
|
||||
It also has a short additional `49704` DCE/RPC conversation:
|
||||
|
||||
```text
|
||||
::1:49768 <-> ::1:49704
|
||||
192 frames, about 19 kB
|
||||
```
|
||||
|
||||
The harness write timestamps were:
|
||||
|
||||
```text
|
||||
2026-04-25T05:13:58.0479762Z mx.write.begin
|
||||
2026-04-25T05:13:58.0489758Z mx.write.end
|
||||
2026-04-25T05:13:58.2561934Z mx.event.write-complete
|
||||
```
|
||||
|
||||
With the pcap first frame at epoch `1777094027.708322300`, this places the
|
||||
write call around relative time `10.34s` and the write-complete callback around
|
||||
relative time `10.55s`.
|
||||
|
||||
No DCE/RPC frames on port `49704` occur in the `10.30s` to `10.62s` window.
|
||||
Instead, the active payload in that exact window is primarily TCP stream `0`:
|
||||
|
||||
```text
|
||||
127.0.0.1:57415 <-> 127.0.0.1:57433
|
||||
```
|
||||
|
||||
That stream is not decoded by Wireshark as DCE/RPC. Its payload has a compact
|
||||
binary framing pattern with frequent 12-byte control messages and small
|
||||
length-prefixed payloads. Example payload prefixes around the write window
|
||||
include little-endian values such as:
|
||||
|
||||
```text
|
||||
1a 00 00 00 ...
|
||||
16 00 00 00 ...
|
||||
22 00 00 00 ...
|
||||
1e 00 00 00 ...
|
||||
3f 00 00 00 ...
|
||||
```
|
||||
|
||||
Interpretation: the native implementation probably uses DCE/RPC for service
|
||||
activation/session coordination and a separate local binary channel for at
|
||||
least part of the advised update/write-complete path. The managed replacement
|
||||
must account for both layers.
|
||||
|
||||
The stream has been extracted as:
|
||||
|
||||
```text
|
||||
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57415-to-127_0_0_1_57433.bin
|
||||
captures\016-loopback-write-test-int-advised\tcp-stream-127_0_0_1_57433-to-127_0_0_1_57415.bin
|
||||
```
|
||||
|
||||
The stream is now decoded with packet-boundary and mixed-record helpers:
|
||||
|
||||
```text
|
||||
analysis\scripts\decode_tcp_payload_packets.py
|
||||
analysis\scripts\decode_mixed_local_stream.py
|
||||
analysis\scripts\analyze_write_window.py
|
||||
analysis\scripts\diff_write_window_records.py
|
||||
```
|
||||
|
||||
The mixed-record model is:
|
||||
|
||||
- 12-byte control records: `int32 code_or_status`, `int32 token_low`,
|
||||
`int32 token_high`.
|
||||
- Data records: `uint32 body_length`, followed by the data body.
|
||||
- A positive control value can announce one or more following data records.
|
||||
- `-1` appears as acknowledgement/status.
|
||||
- `-2` appears as a bidirectional status/control marker around write windows.
|
||||
|
||||
Usable value-change write captures added after this decode pass:
|
||||
|
||||
| Folder | Result |
|
||||
| --- | --- |
|
||||
| `captures\017-loopback-write-test-int-100` | `TestInt` changed `99 -> 100` |
|
||||
| `captures\020-loopback-write-test-int-102` | `TestInt` changed `101 -> 102` |
|
||||
| `captures\021-loopback-write-test-int-sequence-103-105` | same-session writes `103`, `104`, `105` |
|
||||
|
||||
The detailed COM contract and managed-client implication note is:
|
||||
|
||||
```text
|
||||
docs\NMX-COM-Contracts.md
|
||||
```
|
||||
|
||||
## Same-session write sequence
|
||||
|
||||
`captures\021-loopback-write-test-int-sequence-103-105` writes three int values
|
||||
through one registered/advised MXAccess session:
|
||||
|
||||
```text
|
||||
103 at 2026-04-25T05:53:06.9746508Z
|
||||
104 at 2026-04-25T05:53:07.6963047Z
|
||||
105 at 2026-04-25T05:53:08.4180133Z
|
||||
```
|
||||
|
||||
Each write produced a good data-change callback followed by a good
|
||||
write-complete callback. The top local payload stream remained:
|
||||
|
||||
```text
|
||||
127.0.0.1:57415 <-> 127.0.0.1:57433
|
||||
```
|
||||
|
||||
The analyzer output is:
|
||||
|
||||
```text
|
||||
captures\021-loopback-write-test-int-sequence-103-105\write-window-mixed-records.tsv
|
||||
analysis\network\write-window-body-diff-021-w0-vs-w1.tsv
|
||||
analysis\network\write-window-body-diff-021-w1-vs-w2.tsv
|
||||
```
|
||||
|
||||
Within the `-0.10s` to `+0.12s` write-complete windows, the repeated records are
|
||||
the same families already seen in the single-write captures:
|
||||
|
||||
```text
|
||||
54 8f 63 40 e2 5e 31 40 ... 26-byte data body
|
||||
1c 21 18 d0 c4 6f 33 bb ... 34-byte data body
|
||||
98 04 33 cb 0c b4 7c 38 ... 67-byte data body
|
||||
44 6b 99 d8 ec 1b bd b5 ... 52-byte data body
|
||||
55 ce ff 62 b2 1b 3a 50 ... 30-byte data body
|
||||
```
|
||||
|
||||
The raw int values `103`, `104`, and `105` were not isolated from the pcap-only
|
||||
mixed stream decode. The most consistent byte changes in that layer are
|
||||
counter-like fields at offset `14` in the 26-byte and 30-byte data bodies,
|
||||
offset `0` in the 22-byte and 26-byte response bodies, and related token fields
|
||||
in adjacent 12-byte controls. Those fields advance with the local message
|
||||
sequence and should not be treated as the application value.
|
||||
|
||||
The application value was later isolated one layer higher with Frida hooks
|
||||
placed using headless Ghidra RVAs. See:
|
||||
|
||||
```text
|
||||
docs\Ghidra-Headless-Analysis.md
|
||||
captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv
|
||||
```
|
||||
|
||||
## Plaintext result
|
||||
|
||||
The selected `49704` stream binaries did not contain the test tag names in
|
||||
ASCII or UTF-16LE. The captured protocol is therefore not a simple plaintext
|
||||
tag-reference transport.
|
||||
|
||||
## Current reconstruction hypothesis
|
||||
|
||||
The implementation path is:
|
||||
|
||||
```text
|
||||
ArchestrA.MXAccess.dll
|
||||
-> LmxProxy.dll COM in-proc server
|
||||
-> local DCE/RPC to NmxSvc.exe on ::1:49704
|
||||
-> local binary channels for request/callback data
|
||||
-> LMX/NMX runtime and Galaxy-derived security/type metadata
|
||||
```
|
||||
|
||||
The next useful work is to decode the proxy/stub interfaces and the local
|
||||
binary stream structure, then tie decoded calls back to MXAccess harness events.
|
||||
@@ -0,0 +1,523 @@
|
||||
# MXAccess public API surface
|
||||
|
||||
Source of truth: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
|
||||
|
||||
Assembly identity:
|
||||
|
||||
- Name: `ArchestrA.MxAccess`
|
||||
- Version: `3.2.0.0`
|
||||
- Public key token: `23106a86e706d0ae`
|
||||
- File product string: `Assembly imported from type library 'LMXPROXYLib'.`
|
||||
- Processor architecture in metadata: `None`; practical use is still x86 because the COM class is a 32-bit in-proc server.
|
||||
|
||||
## COM class
|
||||
|
||||
`ArchestrA.MxAccess.LMXProxyServerClass`
|
||||
|
||||
- CLSID: `{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
|
||||
- ProgID: `LMXProxy.LMXProxyServer.1`
|
||||
- Version-independent ProgID: `LMXProxy.LMXProxyServer`
|
||||
- Registered server: `C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll`
|
||||
- Registry location: `HKCR\Wow6432Node\CLSID\{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}`
|
||||
- Threading model: `Apartment`
|
||||
|
||||
Because the server is under `Wow6432Node` and registered as an in-process DLL,
|
||||
64-bit callers cannot instantiate it directly.
|
||||
|
||||
## Interface lineage
|
||||
|
||||
`ILMXProxyServer5` extends prior versions without changing the older method
|
||||
signatures:
|
||||
|
||||
- `ILMXProxyServer` - original register/add/advise/write/auth API.
|
||||
- `ILMXProxyServer2` - adds `ArchestrAUserToId`.
|
||||
- `ILMXProxyServer3` - adds `AddItem2`.
|
||||
- `ILMXProxyServer4` - adds `Write2`, `WriteSecured2`, `Suspend`, `Activate`, `AdviseSupervisory`.
|
||||
- `ILMXProxyServer5` - adds buffered item support.
|
||||
|
||||
Interface GUIDs:
|
||||
|
||||
- `ILMXProxyServer`: `{CCE67FB7-EAFD-4367-9212-617043BF126D}`
|
||||
- `ILMXProxyServer2`: `{020A8A87-69C5-497F-A893-B629E669FBFF}`
|
||||
- `ILMXProxyServer3`: `{57D006B6-F25E-4654-A81E-BCBAFD60FE59}`
|
||||
- `ILMXProxyServer4`: `{9DC0D5C1-9371-4E84-86F8-7091D316A66C}`
|
||||
- `ILMXProxyServer5`: `{ECEFF506-A752-46E3-9E31-0A8E257C9926}`
|
||||
|
||||
## Methods
|
||||
|
||||
| Method | Purpose inferred from API and current usage |
|
||||
| --- | --- |
|
||||
| `Register(string pClientName) -> int` | Opens a client session and returns an LMX server handle. |
|
||||
| `Unregister(int hLMXServerHandle)` | Closes the client session. |
|
||||
| `AddItem(int hLMXServerHandle, string strItemDef) -> int` | Resolves/registers an item reference and returns an item handle. |
|
||||
| `RemoveItem(int hLMXServerHandle, int hItem)` | Releases an item handle. |
|
||||
| `Advise(int hLMXServerHandle, int hItem)` | Subscribes to item updates. |
|
||||
| `UnAdvise(int hLMXServerHandle, int hItem)` | Cancels item updates. |
|
||||
| `Write(int hLMXServerHandle, int hItem, object pItemValue, int userId)` | Writes a value. In current code `userId` is used as security classification. |
|
||||
| `WriteSecured(int hLMXServerHandle, int hItem, int currentUserId, int verifierUserId, object pItemValue)` | Secured/verified write path. |
|
||||
| `AuthenticateUser(int hLMXServerHandle, string verifyUser, string verifyUserPsw) -> int` | Authenticates a user and returns a numeric id. |
|
||||
| `ArchestrAUserToId(int hLMXServerHandle, string userIdGuid) -> int` | Maps ArchestrA user GUID to integer id. |
|
||||
| `AddItem2(int hLMXServerHandle, string strItemDef, string strItemCtxt) -> int` | Adds an item with a separate context string. |
|
||||
| `Write2(int hLMXServerHandle, int hItem, object pItemValue, object pItemTime, int userId)` | Timestamped write. |
|
||||
| `WriteSecured2(...)` | Timestamped secured/verified write. |
|
||||
| `Suspend(int hLMXServerHandle, int hItem, out MxStatus status)` | Suspends an item/reference. |
|
||||
| `Activate(int hLMXServerHandle, int hItem, out MxStatus status)` | Activates a suspended item/reference. |
|
||||
| `AdviseSupervisory(int hLMXServerHandle, int hItem)` | Supervisory subscription mode. This is what the existing OPC UA bridge uses. |
|
||||
| `AddBufferedItem(int hLMXServerHandle, string strItemDef, string strItemCtxt) -> int` | Adds a buffered item. |
|
||||
| `SetBufferedUpdateInterval(int hLMXServerHandle, int updateInterval)` | Sets buffered update cadence. |
|
||||
|
||||
Reflection over the installed interop assembly confirms
|
||||
`LMXProxyServerClass` exposes these 18 public methods plus the four public COM
|
||||
event families: `OnDataChange`, `OnWriteComplete`, `OperationComplete`, and
|
||||
`OnBufferedDataChange`. `Write2` and `WriteSecured2` expose their timestamp
|
||||
argument as `object`/VARIANT. The .NET 10 compatibility facade keeps typed
|
||||
`DateTime` overloads and also exposes object timestamp overloads for
|
||||
API-shape parity.
|
||||
|
||||
## Secured write capture status
|
||||
|
||||
The current test node still does not produce a successful dedicated
|
||||
`WriteSecured` wire path:
|
||||
|
||||
- `captures\036-frida-write-secured-test-int`: `WriteSecured` returned
|
||||
`0x80004021` before any NMX `0x37` write body was emitted.
|
||||
- `captures\038-frida-write-secured-protectedvalue`: `WriteSecured` returned
|
||||
`0x80004021` before any NMX `0x37` write body was emitted.
|
||||
- `captures\039-frida-write-secured-verified-protectedvalue1`: `WriteSecured`
|
||||
returned `0x80004021` before any NMX `0x37` write body was emitted.
|
||||
- `captures\111-frida-write-secured-auth-protectedvalue` and
|
||||
`captures\112-frida-write-secured-auth-verified-protectedvalue1`:
|
||||
`AuthenticateUser` succeeded first, but `WriteSecured` still returned
|
||||
`0x80004021` before any value-bearing body.
|
||||
- `captures\037-frida-write-secured2-test-int`: `WriteSecured2` returned
|
||||
`0x80070057` before any NMX write body was emitted.
|
||||
|
||||
Headless Ghidra output in
|
||||
`analysis\ghidra\exports\LmxProxy.dll.write-secured-decompile.md` shows why
|
||||
these paths split: `WriteSecured` has an extra item-record flag check that can
|
||||
return `0x80004021`; `WriteSecured2` skips that check and proceeds through the
|
||||
authenticated timestamped writer when the user handles are mapped.
|
||||
|
||||
Normal `Write` calls to the secured/verified public test attributes did emit
|
||||
ordinary `0x37` write bodies and succeeded in captures `040` and `041`. That
|
||||
means the managed implementation supports the successful observed path through
|
||||
normal `Write`.
|
||||
|
||||
Authenticated `WriteSecured2` now has successful captures and a managed encoder.
|
||||
Captures `113`-`116` show command `0x38` over transfer kind `Write` (`3`) for
|
||||
the boolean secured/verified tags in this GR. The body carries the current-user
|
||||
authenticator token before the client name and the verifier token after the
|
||||
client name. Live .NET 10 x64 probes succeeded for
|
||||
`TestMachine_001.ProtectedValue` with no verifier and
|
||||
`TestMachine_001.ProtectedValue1` with verifier handle `1`, both through the
|
||||
low-level session API and through the handle-based compatibility facade. The
|
||||
facade does not synthesize `OnWriteComplete` for this path because the native
|
||||
successful captures did not show a public write-complete event. Capture `117`
|
||||
adds authenticated `WriteSecured2` for integer `TestChildObject.TestInt`; it
|
||||
uses the same generic timestamped-prefix rule, so the managed encoder is no
|
||||
longer bool-only. Live managed probes also succeeded for float, double, string,
|
||||
datetime, and the scalar-array value kinds. Native captures beyond bool and int
|
||||
would still be useful as additional golden fixtures, but the managed encoder is
|
||||
now generic over the existing timestamped write-body support.
|
||||
|
||||
## User mapping capture status
|
||||
|
||||
The x86 `MxTraceHarness` `user-map` scenario directly calls
|
||||
`CLMXProxyServer.ArchestrAUserToId`. On this test node, it does not return the
|
||||
Galaxy Repository `user_profile_id`:
|
||||
|
||||
| Capture | Input GUID | MXAccess return |
|
||||
| --- | --- | ---: |
|
||||
| `captures\mxaccess-user-map-administrator.log` | `9222FBBA-53F4-457E-8B37-C93A9A250B4A` | `1` |
|
||||
| `captures\mxaccess-user-map-systemengineer.log` | `626A355F-737E-45F8-9740-43372220DEAB` | `1` |
|
||||
| `captures\mxaccess-user-map-defaultuser.log` | `F4A9B907-6E72-48AE-83B5-BBDE918C890F` | `1` |
|
||||
| `captures\mxaccess-user-map-invalid.log` | `00000000-0000-0000-0000-000000000000` | `1` |
|
||||
|
||||
The managed compatibility layer mirrors this observed behavior for
|
||||
`ArchestrAUserToId`. GR profile lookup remains available separately through
|
||||
`GalaxyRepositoryUserResolver`.
|
||||
|
||||
## Authentication capture status
|
||||
|
||||
`captures\mxaccess-authenticate-user-administrator-empty.log` shows
|
||||
`AuthenticateUser(session, "Administrator", "")` returning user ID `1`.
|
||||
Frida captures `087-frida-authenticate-administrator-empty` and
|
||||
`088-frida-authenticate-invalid-empty` show both `Administrator` and
|
||||
`DefinitelyNotAUser` returning `S_OK` and user ID `1` when the password is
|
||||
empty on this dev node. The Frida hook records only password length, not
|
||||
password content.
|
||||
|
||||
Headless Ghidra decompile `analysis\ghidra\exports\LmxProxy.dll.auth-decompile.md`
|
||||
shows `AuthenticateUser` calling the underlying user-authenticator object and,
|
||||
when it receives a security token, incrementing a per-session user counter and
|
||||
mapping that generated integer to the token GUID. This matches
|
||||
`ArchestrAUserToId`: the public return value is a session-local MXAccess handle,
|
||||
not the GR `dbo.user_profile.user_profile_id`.
|
||||
|
||||
`MxNativeCompatibilityServer.AuthenticateUser` now mirrors the observed dev-node
|
||||
behavior by validating the server handle and returning a session-local user
|
||||
handle. It deliberately ignores and does not retain password material. A
|
||||
security-enabled Galaxy still needs dedicated captures before password/hash
|
||||
verification can be implemented safely.
|
||||
|
||||
## AddItem2 context capture status
|
||||
|
||||
`captures\mxaccess-additem2-testint-context.log` shows
|
||||
`CLMXProxyServer.AddItem2(session, "TestInt", "TestChildObject")` succeeding and
|
||||
returning item handle `1`. That confirms the simple relative-reference form used
|
||||
by `MxNativeCompatibilityServer.AddItem2`: combine context and item definition
|
||||
as `TestChildObject.TestInt` before GR resolution. More complex context strings
|
||||
still need captures.
|
||||
|
||||
## Advise capture status
|
||||
|
||||
`captures\mxaccess-plain-advise-testint.log` shows the public
|
||||
`CLMXProxyServer.Advise(session, item)` method succeeding for
|
||||
`TestChildObject.TestInt`. Frida capture `099-frida-plain-advise-testint`
|
||||
captures the lower-level body: plain `Advise` sends the same item-control
|
||||
`0x1f` body shape as the earlier `AdviseSupervisory` capture for the same
|
||||
scalar tag, including the same trailing option value `3`. The managed
|
||||
compatibility layer therefore routes both public methods through the same
|
||||
decoded subscription body for the observed scalar path.
|
||||
|
||||
## Suspend/Activate capture status
|
||||
|
||||
`captures\mxaccess-suspend-testint.log` and
|
||||
`captures\mxaccess-activate-testint.log` show the public MXAccess methods
|
||||
throwing `0x80070057` for `TestChildObject.TestInt` before a usable `MxStatus`
|
||||
is returned. These captures establish that the test integer attribute is not a
|
||||
valid suspend/activate scenario. A successful capture against the correct item
|
||||
class is still required before implementing native NMX bodies for these methods.
|
||||
|
||||
## Buffered item capture status
|
||||
|
||||
The x86 harness now covers the public buffered APIs:
|
||||
|
||||
- `captures\mxaccess-set-buffered-interval-1000.log` shows
|
||||
`SetBufferedUpdateInterval(session, 1000)` succeeding.
|
||||
- `captures\mxaccess-add-buffered-testint-context.log` shows
|
||||
`AddBufferedItem(session, "TestInt", "TestChildObject")` succeeding and
|
||||
returning item handle `1`.
|
||||
- `captures\mxaccess-add-buffered-write-testint-context.log` shows that using
|
||||
the buffered item handle with normal `Write` throws `0x80070057`.
|
||||
- `captures\079-frida-add-buffered-advise-testint` and
|
||||
`captures\080-frida-buffered-external-write-testint` show the advised
|
||||
buffered registration body. MXAccess does not send the normal `0x1f`
|
||||
item-control advise for the buffered handle. It sends an item-control `0x10`
|
||||
reference registration for `TestInt.property(buffer)` in context
|
||||
`TestChildObject`, wrapped in a `MessageKind=2` transfer envelope.
|
||||
- `captures\121-frida-buffered-history-testhistoryvalue-context` and
|
||||
`captures\122-frida-buffered-history-testhistoryvalue-plainadvise` repeat the
|
||||
buffered registration against GR-confirmed historized integer attribute
|
||||
`TestMachine_001.TestHistoryValue`. Both supervisory and plain advise forms
|
||||
emit the same context-bearing `0x10`/`0x11` registration/result shape for
|
||||
`TestHistoryValue.property(buffer)` in context `TestMachine_001`.
|
||||
Separate writer-session writes succeed and normal writer data callbacks are
|
||||
observed, but native MXAccess still does not enter `Fire_OnBufferedDataChange`
|
||||
on this VM.
|
||||
- `captures\085-frida-subscribe-property-buffer` and
|
||||
`captures\086-frida-write-property-buffer` show that adding the literal item
|
||||
`TestChildObject.TestInt.property(buffer)` does not enter the public
|
||||
`AddBufferedItem` helper. It follows the normal add/advise path, sends the
|
||||
same item-control `0x10` reference-registration body with an empty item
|
||||
context, and receives an NMX registration-result frame carrying the runtime
|
||||
internal-error text for `TestInt.property(buffer)`. Normal `Write` against
|
||||
the literal handle returns from `CLMXProxyServer.Write` without producing an
|
||||
observed buffered callback.
|
||||
- Ghidra decompile of `LmxProxy.dll` function `1001121d` confirms the public
|
||||
`AddBufferedItem` implementation allocates a BSTR copy of `strItemDef`,
|
||||
appends `.property(buffer)`, calls the normal add-item implementation, and
|
||||
marks the resulting item record as buffered. Function `1000fc80` confirms
|
||||
`SetBufferedUpdateInterval` rejects intervals below `1` and stores the
|
||||
interval as `(milliseconds + 99) / 100`, effectively rounding up to 100 ms
|
||||
ticks.
|
||||
- Ghidra decompile of `100163c0` confirms `OnBufferedDataChange` is fired
|
||||
through the `_ILMXProxyServerEvents2` connection point with seven arguments:
|
||||
server handle, item handle, data type, value variant, quality variant,
|
||||
timestamp variant, and status array.
|
||||
- Headless xref/decompile output shows `Fire_OnBufferedDataChange` is reached
|
||||
from the same native `OnDataChange callback received` method used for normal
|
||||
data changes. The callback looks up the item record and branches on the
|
||||
buffered item flag: non-buffered items fire `OnDataChange`; buffered items
|
||||
convert the callback value into value, quality, and timestamp SAFEARRAY
|
||||
variants before firing `OnBufferedDataChange`.
|
||||
- The buffered value conversion helper maps MX buffered element type `1` to a
|
||||
`VT_BOOL` value array, `2` to `VT_I4`, `3` to `VT_R4`, `4` to `VT_R8`, `5`
|
||||
to `VT_BSTR`, and `6`/`7` to `VT_UI8` FILETIME-style values. Quality is
|
||||
emitted as a `VT_I2` SAFEARRAY, timestamp as a `VT_UI8` SAFEARRAY, and the
|
||||
status argument uses the normal `MXSTATUS_PROXY[]` SAFEARRAY.
|
||||
|
||||
No live `OnBufferedDataChange` payload has been observed yet. A source/runtime
|
||||
condition that actually delivers buffered sample batches is still needed to
|
||||
validate the wire body and multi-sample parser. The managed library now
|
||||
implements the decoded add/registration surface, including context-bearing
|
||||
buffered registrations, and routes parsed buffered callbacks to a separate
|
||||
managed buffered event instead of the normal data-change event.
|
||||
|
||||
## Events
|
||||
|
||||
Event source interfaces:
|
||||
|
||||
- `_ILMXProxyServerEvents`: `{848299B6-DD61-4A0D-A304-3947A564B89C}`
|
||||
- `_ILMXProxyServerEvents2`: `{C70A6FC4-09EF-4F31-8874-A049FEE87A95}`
|
||||
|
||||
Events:
|
||||
|
||||
```csharp
|
||||
void OnDataChange(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
object pvItemValue,
|
||||
int pwItemQuality,
|
||||
object pftItemTimeStamp,
|
||||
ref MXSTATUS_PROXY[] pVars);
|
||||
|
||||
void OnWriteComplete(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
ref MXSTATUS_PROXY[] pVars);
|
||||
|
||||
void OperationComplete(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
ref MXSTATUS_PROXY[] pVars);
|
||||
|
||||
void OnBufferedDataChange(
|
||||
int hLMXServerHandle,
|
||||
int phItemHandle,
|
||||
MxDataType dtDataType,
|
||||
object pvItemValue,
|
||||
object pwItemQuality,
|
||||
object pftItemTimeStamp,
|
||||
ref MXSTATUS_PROXY[] pVars);
|
||||
```
|
||||
|
||||
Ghidra decompile of `LmxProxy.dll` event helpers confirms that
|
||||
`Fire_OnWriteComplete` and `Fire_OperationComplete` both construct a three
|
||||
element `VARIANT` argument array containing server handle, item handle, and one
|
||||
`MXSTATUS_PROXY` SAFEARRAY. `Fire_OnWriteComplete` dispatches event ID `2`;
|
||||
`Fire_OperationComplete` dispatches event ID `3`. In the capture set, successful
|
||||
writes raise only `OnWriteComplete`; no `mx.event.operation-complete` line has
|
||||
been observed yet.
|
||||
|
||||
Headless Ghidra xref/decompile output in
|
||||
`analysis\ghidra\exports\LmxProxy.dll.event-xrefs.md` and
|
||||
`analysis\ghidra\exports\LmxProxy.dll.event-callers-decompile.md` narrows the
|
||||
source further: `Fire_OnWriteComplete` is reached from
|
||||
`CUserConnectionCallback::OnSetAttributeResult`, while `Fire_OperationComplete`
|
||||
is reached from `CUserConnectionCallback::OperationComplete`. They are distinct
|
||||
callback paths even though their COM event payload shape is the same.
|
||||
|
||||
The installed interop assemblies expose the lower-level callback as
|
||||
`IMxCallback2.OperationComplete(int lCallbackId, ref MxStatus, string)`. They
|
||||
also expose `IDataConsumer.ActivateSuspend` and `ProcessActivateSuspend2`, whose
|
||||
response type is `ItemActiveResponse`. That makes the DataConsumer
|
||||
activate/suspend completion path the strongest remaining native trigger
|
||||
candidate. The public `LMXProxyServerClass.Suspend` and `Activate` methods
|
||||
tested in captures `118` and `119` instead decompile to direct
|
||||
`IMxScanOnDemand` calls and did not enter
|
||||
`CUserConnectionCallback.OperationComplete` on this node.
|
||||
|
||||
`aaMxDataConsumer.dll` is registered separately as `MxDataConsumer Class`
|
||||
(`{85209FB2-0BA1-4594-BBC4-59D3DDAB823D}`) and exposes the same
|
||||
`IDataConsumer` activate/suspend methods through its type library. A targeted
|
||||
x86 probe can instantiate it and call those methods, but the standalone object
|
||||
currently reports `IsConnected(namespaceId)=0` and
|
||||
`ProcessActivateSuspend2` returns `0x8007139F`. That means the COM surface is
|
||||
reachable, but a DataConsumer/DataClient bootstrap step is still missing before
|
||||
it can prove the public `OperationComplete` trigger.
|
||||
|
||||
The mixed-mode `aaMxDataConsumer.dll` decompile shows that bootstrap should
|
||||
ultimately route through managed ASB IData proxies: `CDataClientCLI` owns a
|
||||
`DataClientProxy`, starts an auto-connect worker, and passes the namespace
|
||||
string as an ASB access name to
|
||||
`IDataProxySelector.SelectProxyForLatestEndpoint`. `ASBIDataV2Adapter.dll`
|
||||
contains that selector; it looks for `IASBIDataV2` endpoints under
|
||||
`domainname/<accessName>/global` before falling back to IData V1. A new x64
|
||||
managed probe found and connected to the live `IASBIDataV2` endpoint with
|
||||
access name `ZB`, then successfully called `PublishWriteComplete`. This does
|
||||
not prove `OperationComplete` yet, but it proves the relevant data-service
|
||||
route can be reached from managed x64 code without MXAccess or COM.
|
||||
|
||||
The same x64 ASB probe now proves the register/read/write/complete flow for
|
||||
`TestChildObject.TestInt`. `RegisterItems` and `Read` succeed, ASB type `4`
|
||||
decodes as `Int32`, `Write(401)` is accepted with per-item
|
||||
`OperationWouldBlock`, the next read returns `401`, and
|
||||
`PublishWriteComplete` returns the submitted write handle with final per-item
|
||||
success. That gives a concrete managed completion queue to model for ASB-native
|
||||
write-complete behavior; `OperationComplete` should still not be synthesized
|
||||
until an operation other than a basic write is proven to use the native
|
||||
MXAccess event ID `3` path.
|
||||
|
||||
The same core flow is now reproduced by the pure .NET 10 x64
|
||||
`src\MxAsbClient` implementation with no AVEVA assembly references. Its live
|
||||
probe reads the ASB solution secret through DPAPI, performs the system-auth
|
||||
handshake, reads `TestChildObject.TestInt`, writes a new integer value, reads
|
||||
that value back, and decodes `PublishWriteComplete` result `0x00000020` with
|
||||
the submitted write handle and final per-item success. `RegisterItems` now
|
||||
matches the observed ASB startup behavior: the first immediate call after
|
||||
one-way `AuthenticateMe` can return `0x00000001`, so the client retries briefly
|
||||
and receives the expected item status/id once the server-side implementation is
|
||||
registered. The remaining ASB-native work is API breadth: multi-item calls,
|
||||
subscriptions, scalar/array type matrix, and status/error mapping.
|
||||
|
||||
`UnregisterItems` is now also implemented and compared against the installed
|
||||
AVEVA `ASBDataV2Proxy`. Both the pure .NET 10 client and installed proxy return
|
||||
global success for the unregister call and the same per-item `0x0000000B`
|
||||
(`OperationFailed`) for `TestChildObject.TestInt` on this provider. That is
|
||||
documented as parity with the deployed ASB provider, not a successful item-level
|
||||
cleanup status.
|
||||
|
||||
The pure .NET 10 client also handles multi-item register/read bodies. A two-tag
|
||||
probe registered and read `TestChildObject.TestInt` plus
|
||||
`TestMachine_001.TestHistoryValue` in single requests; both returned per-item
|
||||
success and decoded as ASB `TypeInt32`.
|
||||
|
||||
For write status callbacks, the public event is tied to the non-length-prefixed
|
||||
5-byte operation-status body `00 00 50 80 00`. Length-prefixed completion-only
|
||||
bodies are lower-level NMX status frames and did not produce public
|
||||
`OnWriteComplete` events in captures `089`, `091`, `092`, or `093`, even when
|
||||
the completion byte was `0x00`.
|
||||
|
||||
## Data types
|
||||
|
||||
`MxDataType`:
|
||||
|
||||
```text
|
||||
Unknown = -1
|
||||
NoData = 0
|
||||
Boolean = 1
|
||||
Integer = 2
|
||||
Float = 3
|
||||
Double = 4
|
||||
String = 5
|
||||
Time = 6
|
||||
ElapsedTime = 7
|
||||
ReferenceType = 8
|
||||
StatusType = 9
|
||||
Enum = 10
|
||||
SecurityClassificationEnum = 11
|
||||
DataQualityType = 12
|
||||
QualifiedEnum = 13
|
||||
QualifiedStruct = 14
|
||||
InternationalizedString = 15
|
||||
BigString = 16
|
||||
END = 17
|
||||
```
|
||||
|
||||
Live GR inventory on this node found deployed/configured instances of all core
|
||||
scalar types plus `ElapsedTime` and `InternationalizedString`. Target references
|
||||
captured for the two non-core types:
|
||||
|
||||
- `TestMachine_001.TestAlarm001.Alarm.TimeDeadband`: `ElapsedTime`, observed
|
||||
subscription callback wire kind `0x07` with four-byte zero payload in
|
||||
`captures\063-frida-subscribe-elapsed-time-deadband`.
|
||||
- `TestChildObject.ShortDesc`: `InternationalizedString`, observed callback
|
||||
normalizes the empty value to string wire kind `0x05` with compact payload
|
||||
`04 00 00 00` in `captures\062-frida-subscribe-intl-shortdesc`.
|
||||
- Non-empty GR defaults exist at `DevPlatform._EngUnitsPercent` and
|
||||
`DevAppEngine.Scheduler._EngUnitsMB`; captures `064` and `065` resolved and
|
||||
advised those items but did not emit a value callback during the capture
|
||||
window.
|
||||
|
||||
Write projection for these non-core types is caller-variant based in the
|
||||
observed MXAccess path. Capture `095` wrote an `Int32` to `ElapsedTime` and
|
||||
emitted integer wire kind `0x02`; capture `096` wrote a `string` to
|
||||
`InternationalizedString` and emitted string wire kind `0x05`. The managed
|
||||
library mirrors those projections for write attempts while keeping the data
|
||||
types out of generic `TryGetValueKind` classification.
|
||||
|
||||
`MxStatus` and `MXSTATUS_PROXY` are identical sequential structs with 4-byte
|
||||
packing:
|
||||
|
||||
```csharp
|
||||
public short success;
|
||||
public MxStatusCategory category;
|
||||
public MxStatusSource detectedBy;
|
||||
public short detail;
|
||||
```
|
||||
|
||||
`MxStatusCategory`:
|
||||
|
||||
```text
|
||||
Unknown = -1
|
||||
Ok = 0
|
||||
Pending = 1
|
||||
Warning = 2
|
||||
CommunicationError = 3
|
||||
ConfigurationError = 4
|
||||
OperationalError = 5
|
||||
SecurityError = 6
|
||||
SoftwareError = 7
|
||||
OtherError = 8
|
||||
```
|
||||
|
||||
`MxStatusSource`:
|
||||
|
||||
```text
|
||||
Unknown = -1
|
||||
RequestingLmx = 0
|
||||
RespondingLmx = 1
|
||||
RequestingNmx = 2
|
||||
RespondingNmx = 3
|
||||
RequestingAutomationObject = 4
|
||||
RespondingAutomationObject = 5
|
||||
```
|
||||
|
||||
## Status detail text
|
||||
|
||||
`C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\Lmx.aaDCT`
|
||||
contains localized text for common status detail codes. The .NET 10 managed
|
||||
model now includes the installed English entries:
|
||||
|
||||
| Detail | Text |
|
||||
| ---: | --- |
|
||||
| 16 | Request timed out |
|
||||
| 17 | Platform communication error |
|
||||
| 18 | Invalid platform ID |
|
||||
| 19 | Invalid engine ID |
|
||||
| 20 | Engine communication error |
|
||||
| 21 | Invalid reference |
|
||||
| 22 | No Galaxy Repository |
|
||||
| 23 | Invalid object ID |
|
||||
| 24 | Object signature mismatch |
|
||||
| 25 | Invalid primitive ID |
|
||||
| 26 | Invalid attribute ID |
|
||||
| 27 | Invalid property ID |
|
||||
| 28 | Index out of range |
|
||||
| 29 | Data out of range |
|
||||
| 30 | Incorrect data type |
|
||||
| 31 | Attribute not readable |
|
||||
| 32 | Attribute not writeable |
|
||||
| 33 | Write access denied |
|
||||
| 34 | Unknown error |
|
||||
| 35 | detected by |
|
||||
| 36 | Wrong data type |
|
||||
| 37 | Wrong number of dimensions |
|
||||
| 38 | Invalid index |
|
||||
| 39 | Index out of order |
|
||||
| 40 | Dimension does not exist |
|
||||
| 41 | Conversion not supported |
|
||||
| 42 | Unable to convert string |
|
||||
| 43 | Overflow |
|
||||
| 44 | Attribute signature mismatch |
|
||||
| 45 | Resolving local portion of reference |
|
||||
| 46 | Resolving global portion of reference |
|
||||
| 47 | Nmx version mismatch |
|
||||
| 48 | Nmx command not valid |
|
||||
| 49 | Lmx version mismatch |
|
||||
| 50 | Lmx command not valid |
|
||||
| 51 | However, the object could not be put On Scan - Permission to modify "Operate" attributes is required |
|
||||
| 52 | Unable to resolve reference for 'set' request because Galaxy Repository is busy performing a 'Deploy/Undeploy' operation |
|
||||
| 53 | Too many outstanding pending requests to engine |
|
||||
| 54 | Object Initializing |
|
||||
| 55 | Engine Initializing |
|
||||
| 56 | Secured Write |
|
||||
| 57 | Verified Write |
|
||||
| 58 | No Alarm Ack Privilege |
|
||||
| 59 | Alarm Acked Already |
|
||||
| 60 | User did not have the necessary permissions to write |
|
||||
| 61 | Verifier did not have the necessary permissions to verify |
|
||||
| 541 | Conversion to intended data type is not supported |
|
||||
| 542 | Unable to convert the input string to intended data type |
|
||||
| 8017 | Object must be offscan to modify attributes that have an MxSecurityConfigure security classification |
|
||||
@@ -0,0 +1,397 @@
|
||||
# MXAccess reverse-engineering analysis
|
||||
|
||||
## Executive summary
|
||||
|
||||
The primary `ArchestrA.MXAccess.dll` is not a protocol implementation. It is a
|
||||
.NET primary interop assembly generated from `LMXPROXYLib`. All useful methods
|
||||
on `LMXProxyServerClass` are `extern` COM calls. The actual implementation is
|
||||
native, 32-bit, and registered as an in-process COM server:
|
||||
|
||||
```text
|
||||
net48 x86 caller
|
||||
-> ArchestrA.MXAccess.dll
|
||||
-> COM CLSID {C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}
|
||||
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
|
||||
-> C:\Program Files (x86)\Common Files\ArchestrA\Framework\Bin\Lmx.dll
|
||||
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
|
||||
-> C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe
|
||||
-> runtime engines over NMX
|
||||
```
|
||||
|
||||
That is why the current OPC UA interface is constrained to .NET Framework and
|
||||
x86. The binding itself is not forcing .NET 4.8; the 32-bit COM/native stack is.
|
||||
The cleanest modern replacement is not "port the interop assembly"; it is either:
|
||||
|
||||
1. keep a 32-bit sidecar process and expose a modern IPC API to .NET 10/Rust, or
|
||||
2. implement the lower LMX/NMX client protocol directly from the native type
|
||||
libraries plus runtime traces.
|
||||
|
||||
Option 2 is possible but not complete from static decompilation alone because
|
||||
the proprietary message bodies are assembled inside native code, not in
|
||||
`ArchestrA.MXAccess.dll`.
|
||||
|
||||
## Evidence
|
||||
|
||||
### Managed DLL
|
||||
|
||||
`ArchestrA.MXAccess.dll`:
|
||||
|
||||
- File path: `C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll`
|
||||
- Assembly: `ArchestrA.MxAccess, Version=3.2.0.0, PublicKeyToken=23106a86e706d0ae`
|
||||
- File description/product: `Assembly imported from type library 'LMXPROXYLib'.`
|
||||
- Decompilation contains only COM-imported interfaces, structs, enums, delegates,
|
||||
and event sink helpers.
|
||||
- `LMXProxyServerClass` methods are all `runtime managed internalcall`, meaning
|
||||
the CLR dispatches them through COM; there is no managed implementation body to port.
|
||||
|
||||
### COM registration
|
||||
|
||||
`LMXProxy.LMXProxyServer` is registered only in the 32-bit COM registry view:
|
||||
|
||||
```text
|
||||
HKCR\Wow6432Node\CLSID\{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}
|
||||
(default) = LMXProxyServer Class
|
||||
InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
|
||||
ThreadingModel = Apartment
|
||||
ProgID = LMXProxy.LMXProxyServer.1
|
||||
VersionIndependentProgID = LMXProxy.LMXProxyServer
|
||||
TypeLib = {C36ECF28-3EF3-4528-9843-81D7FDC86328}
|
||||
```
|
||||
|
||||
Type libraries:
|
||||
|
||||
```text
|
||||
{77E896EC-B3E3-4DE4-B96F-F32570164211}\3.2
|
||||
LMXProxy 3.2 Type Library
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess32.tlb
|
||||
|
||||
{C36ECF28-3EF3-4528-9843-81D7FDC86328}\1.0
|
||||
LMXProxy 1.0 Type Library
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
|
||||
|
||||
{C36ECF28-3EF3-4528-9843-81D7FDC86328}\2.0
|
||||
LMXProxy 2.0 Type Library
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\MXAccess20.tlb
|
||||
```
|
||||
|
||||
### Native binaries
|
||||
|
||||
| Binary | Role inferred from registration/imports/strings | Architecture |
|
||||
| --- | --- | --- |
|
||||
| `LmxProxy.dll` | Public MXAccess COM wrapper. Implements `LMXProxy.LMXProxyServer`. Imports licensing wrapper and COM/OLE Automation APIs. | PE32 x86 |
|
||||
| `Lmx.dll` | Main local message exchange implementation. Exposes many COM objects including `IDataClient`, `IDataConsumer`, `Nmx`, bootstrap, runtime object, and data variant surfaces. | PE32 x86 |
|
||||
| `NmxAdptr.dll` | In-process adapter over NMX service. Imports Winsock and exposes `WonderWare.Nmx.CNmxAdapter`. | PE32 x86 |
|
||||
| `NmxSvc.exe` | Out-of-process NMX service. Registered as `NmxSvc.NmxService`; listens on TCP/UDP port `5026` on this machine. | PE32 x86 |
|
||||
| `NmxSvcps.dll` | MIDL proxy/stub for NMX service COM interfaces. | PE32 x86 |
|
||||
|
||||
`LmxProxy.dll` exports only standard COM DLL exports:
|
||||
|
||||
```text
|
||||
DllCanUnloadNow
|
||||
DllGetClassObject
|
||||
DllRegisterServer
|
||||
DllUnregisterServer
|
||||
```
|
||||
|
||||
This means there is no flat C ABI to P/Invoke from modern .NET or Rust.
|
||||
|
||||
`LmxProxy.dll` imports `LicAPINativeWrapper.dll`, including:
|
||||
|
||||
```text
|
||||
CreateClientConnection
|
||||
AddLicenseRequestInfo
|
||||
AcquireLicense
|
||||
GetLicenseAcquisitionError
|
||||
ReleaseLicense
|
||||
GetDeviceIdentity
|
||||
```
|
||||
|
||||
Any native replacement has to account for AVEVA licensing behavior, either by
|
||||
using a supported client surface or by keeping a licensed sidecar.
|
||||
|
||||
## Runtime model inferred from MXAccess
|
||||
|
||||
The current high-level API is handle based:
|
||||
|
||||
1. `Register(clientName)` returns an `hLMXServerHandle`.
|
||||
2. `AddItem(handle, fullReference)` resolves a Galaxy/runtime attribute and
|
||||
returns an item handle.
|
||||
3. `Advise` or `AdviseSupervisory` subscribes to updates.
|
||||
4. Updates arrive through COM connection-point events:
|
||||
`OnDataChange`, `OnWriteComplete`, and `OperationComplete`.
|
||||
5. `Write`/`Write2` submit writes and completion is reported asynchronously.
|
||||
6. `UnAdvise`, `RemoveItem`, and `Unregister` clean up.
|
||||
|
||||
The existing OPC UA host uses the canonical pattern correctly:
|
||||
|
||||
- All COM work runs on one STA thread.
|
||||
- Reads are implemented as transient subscribe -> first `OnDataChange` -> unsubscribe because MXAccess exposes no direct synchronous read.
|
||||
- Writes are submitted with `Write` and then matched with `OnWriteComplete`.
|
||||
- Subscriptions use `AdviseSupervisory`.
|
||||
|
||||
## Lower LMX data-client surface
|
||||
|
||||
`Lmx.dll` exposes a richer COM interface named `IDataClient`. It is still x86 COM,
|
||||
but it is closer to the underlying data model than `LMXProxyServer`:
|
||||
|
||||
```csharp
|
||||
void Initialize(string namespaceName);
|
||||
IntPtr Connect(string endPointUri, ulong timeout, ref _IUserToken userToken, out uint clientId);
|
||||
IArchestrAResult[] Connect2(...);
|
||||
long CreateSubscription2(...);
|
||||
IArchestrAResult[] RegisterItems2(IItemIdentity2[] items, ...);
|
||||
IArchestrAResult[] AddMonitoredItems2(long subscriptionId, IMonitoredItem2[] items, ...);
|
||||
IArchestrAResult[] Publish2(long subscriptionId, out DataChangeUpdate[] updates, out uint resultCount);
|
||||
IArchestrAResult[] Read2(IItemIdentity2[] items, out DataChangeUpdate[] updates, out uint updatesCount);
|
||||
IArchestrAResult[] Write2(WriteRequest2[] writes, ...);
|
||||
```
|
||||
|
||||
Important structs:
|
||||
|
||||
```csharp
|
||||
struct IItemIdentity2 {
|
||||
ushort ReferenceType;
|
||||
ushort type;
|
||||
string ContextName;
|
||||
string Name;
|
||||
ulong id;
|
||||
}
|
||||
|
||||
struct IMonitoredItem2 {
|
||||
byte Active;
|
||||
ushort ReferenceType;
|
||||
ushort type;
|
||||
string ContextName;
|
||||
string Name;
|
||||
ulong id;
|
||||
ulong SampleInterval;
|
||||
ulong TimeDeadband;
|
||||
}
|
||||
|
||||
struct DataChangeUpdate {
|
||||
ulong ItemId;
|
||||
int StatusCode;
|
||||
uint HighDateTime;
|
||||
uint LowDateTime;
|
||||
int quality;
|
||||
IDataVariant value;
|
||||
}
|
||||
|
||||
struct IDataVariant {
|
||||
ushort type;
|
||||
int length;
|
||||
byte[] Payload;
|
||||
}
|
||||
```
|
||||
|
||||
This surface suggests AVEVA has a newer OPC UA-like internal data client in the
|
||||
LMX layer. It may be a better reverse-engineering target than `LMXProxyServer`,
|
||||
but it is not registered as a 64-bit component on this machine. It still goes
|
||||
through the x86 native stack.
|
||||
|
||||
## NMX service surface
|
||||
|
||||
`NmxSvc.exe` is registered as an out-of-process COM local server:
|
||||
|
||||
```text
|
||||
CLSID {AE24BD51-2E80-44CC-905B-E5446C942BEB}
|
||||
LocalServer32 = "C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe"
|
||||
ProgID = NmxSvc.NmxService.1
|
||||
```
|
||||
|
||||
Key `INmxService2` methods:
|
||||
|
||||
```csharp
|
||||
void RegisterEngine2(int localEngineId, string engineName, int version, INmxSvcCallback callback);
|
||||
void UnRegisterEngine(int localEngineId);
|
||||
void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
|
||||
void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte msgBody);
|
||||
void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
|
||||
void RemoveSubscriberEngine(...);
|
||||
void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
|
||||
void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
|
||||
```
|
||||
|
||||
`NmxAdptr.dll` exposes `WonderWare.Nmx.CNmxAdapter` as an in-process COM adapter:
|
||||
|
||||
```text
|
||||
CLSID {42DB0511-28BE-11D3-80C0-00104B5F96A7}
|
||||
InprocServer32 = C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
|
||||
ProgID = WonderWare.Nmx.CNmxAdapter.1
|
||||
ThreadingModel = Apartment
|
||||
```
|
||||
|
||||
Key `INmx4` methods:
|
||||
|
||||
```csharp
|
||||
void Initialize2(int platformId, int engineId, int version, int queueExtent);
|
||||
void InitializeAnonymous2(out int platformId, out int engineId, int version);
|
||||
void PutRequest2(int galaxyId, int platformId, int engineId, byte priority, byte type, int size, ref byte data, out int requestHandle);
|
||||
void GetResponse2(byte type, out int responseCode, out int requestHandle, out int size, IntPtr data);
|
||||
void PutMessageNoReply(...);
|
||||
void GetPartnerVersion(...);
|
||||
```
|
||||
|
||||
Static strings identify NMX message categories:
|
||||
|
||||
```text
|
||||
NMXMTYPE_CONNECT_INFO
|
||||
NMXMTYPE_CONNECT_INFO_REQ
|
||||
NMXMTYPE_ENGINE_DATA
|
||||
NMXMTYPE_GET_HEARTBEAT_RATE
|
||||
NMXMTYPE_PLATFORM_HEARTBEAT
|
||||
NMXMTYPE_SET_HEARTBEAT_RATE
|
||||
NMXMTYPE_VERSION_ERROR
|
||||
NMXMTYPE_HEARTBEAT_DISCONNECT_REQ
|
||||
```
|
||||
|
||||
On this machine, the running `NmxSvc.exe` process is listening on:
|
||||
|
||||
```text
|
||||
TCP 10.100.0.48:5026
|
||||
UDP 10.100.0.48:5026
|
||||
```
|
||||
|
||||
That gives a network tracing target for the direct-native path.
|
||||
|
||||
## Why direct x64 COM is blocked
|
||||
|
||||
There are two different issues:
|
||||
|
||||
1. `ArchestrA.MXAccess.dll` is a .NET Framework-era interop assembly. Modern
|
||||
.NET can consume many COM interfaces, but the generated event provider and
|
||||
TLB-imported types are built for the old COM interop shape.
|
||||
2. The actual COM class is an x86 in-process DLL. A 64-bit process cannot load
|
||||
`LmxProxy.dll`, regardless of whether the caller is .NET Framework, .NET 10,
|
||||
or Rust.
|
||||
|
||||
An out-of-process COM local server can cross bitness, but `LMXProxyServer` is
|
||||
not registered as a local server. It is an in-proc DLL.
|
||||
|
||||
## Implementation options
|
||||
|
||||
### Option A: Keep and formalize the 32-bit sidecar
|
||||
|
||||
This is the shortest reliable path.
|
||||
|
||||
Build or keep a small x86 process that owns:
|
||||
|
||||
- COM apartment initialization.
|
||||
- `LMXProxy.LMXProxyServer` lifetime.
|
||||
- register/add/advise/write/unadvise cleanup.
|
||||
- reconnect and subscription replay.
|
||||
|
||||
Expose a stable IPC API to the modern service:
|
||||
|
||||
- named pipes for same-machine .NET/Rust services,
|
||||
- gRPC over named pipes or loopback TCP if cross-language tooling matters,
|
||||
- protobuf messages for `Read`, `Write`, `Subscribe`, `Unsubscribe`, and status events.
|
||||
|
||||
This is effectively what the existing `OtOpcUaGalaxyHost` / proxy work has moved
|
||||
toward. It removes the OPC UA server's net48/x86 constraint without attempting
|
||||
to clone AVEVA's internal protocol.
|
||||
|
||||
### Option B: Use lower LMX COM interfaces from an x86 sidecar
|
||||
|
||||
Instead of the high-level `LMXProxyServer`, use `Lmx.dll`'s `IDataClient` /
|
||||
`IDataConsumer` APIs inside the sidecar. Potential benefits:
|
||||
|
||||
- direct `Read2` API rather than transient subscribe for reads,
|
||||
- batch register/read/write/monitor operations,
|
||||
- explicit `IDataVariant` payloads,
|
||||
- closer alignment with OPC UA-like status codes.
|
||||
|
||||
Risks:
|
||||
|
||||
- less documented than MXAccess,
|
||||
- still x86 COM,
|
||||
- endpoint URI, namespace, token setup, and licensing behavior need live probing.
|
||||
|
||||
### Option C: Direct NMX protocol client in Rust or .NET
|
||||
|
||||
This is the true native replacement path.
|
||||
|
||||
Required work:
|
||||
|
||||
1. Discover galaxy/platform/engine ids for target attributes.
|
||||
2. Reconstruct NMX message envelope layout used by `PutRequest2` and
|
||||
`NmxSvc.TransferData`.
|
||||
3. Reconstruct LMX item registration, subscription, write, and completion
|
||||
message bodies.
|
||||
4. Reproduce heartbeat/version negotiation.
|
||||
5. Reproduce security/licensing behavior or find a supported token flow.
|
||||
6. Validate behavior across platform stopped/off-scan, engine restart, deploy,
|
||||
security-denied write, and repository-busy cases.
|
||||
|
||||
Static type-library analysis gives method names and high-level message routes,
|
||||
but not the binary message schemas. Those schemas are assembled in native code.
|
||||
The next step for this option is live tracing.
|
||||
|
||||
## Proposed tracing plan
|
||||
|
||||
1. Build a minimal x86 MXAccess harness that:
|
||||
- registers as a known client name,
|
||||
- adds one known good tag and one bad tag,
|
||||
- subscribes with `AdviseSupervisory`,
|
||||
- writes a toggled value,
|
||||
- unregisters cleanly.
|
||||
|
||||
2. Capture process activity:
|
||||
- Process Monitor filters for `LmxProxy.dll`, `Lmx.dll`, `NmxAdptr.dll`,
|
||||
`NmxSvc.exe`.
|
||||
- ETW or API Monitor for COM method calls if available.
|
||||
- Wireshark or `netsh trace` on TCP/UDP `5026`.
|
||||
|
||||
3. Correlate high-level actions to low-level packets:
|
||||
- `Register`
|
||||
- `AddItem`
|
||||
- `AdviseSupervisory`
|
||||
- first data change
|
||||
- `Write`
|
||||
- `OnWriteComplete`
|
||||
- `UnAdvise` / `RemoveItem`
|
||||
|
||||
4. Repeat against:
|
||||
- local object attribute,
|
||||
- remote platform attribute,
|
||||
- stopped engine,
|
||||
- security-denied write,
|
||||
- secured/verified write.
|
||||
|
||||
5. Use `NmxAdptr.INmx4` as a stepping stone:
|
||||
- call `InitializeAnonymous2`,
|
||||
- issue controlled `PutRequest2` from an x86 harness,
|
||||
- compare buffers and responses with MXAccess traffic.
|
||||
|
||||
## Current recommendation
|
||||
|
||||
For production, do not block the OPC UA server on a full native NMX clone. The
|
||||
low-risk path is:
|
||||
|
||||
1. keep the x86 MXAccess host as the vendor-bound adapter,
|
||||
2. make the main OPC UA interface .NET 10 or Rust,
|
||||
3. communicate through a narrow, well-tested IPC contract,
|
||||
4. continue reverse-engineering `IDataClient` and NMX in parallel as an R&D path.
|
||||
|
||||
The direct native path may still be worth pursuing, but it should be treated as
|
||||
a protocol-reconstruction project with packet captures and failure-mode testing,
|
||||
not as a decompile-only port.
|
||||
|
||||
## Artifacts produced
|
||||
|
||||
Generated files in this project folder:
|
||||
|
||||
```text
|
||||
analysis/decompiled-mxaccess/
|
||||
analysis/interop/
|
||||
analysis/decompiled-interop/
|
||||
```
|
||||
|
||||
Installed tools used:
|
||||
|
||||
```text
|
||||
dotnet tool install --global ilspycmd
|
||||
python pefile
|
||||
Windows SDK TlbImp.exe
|
||||
```
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
# Managed LMX/NMX capture plan
|
||||
|
||||
Goal: build a full managed .NET 10 x64 DLL that talks to AVEVA/Wonderware
|
||||
LMX/NMX without loading the x86 MXAccess COM/native stack.
|
||||
|
||||
Assumption: licensing is not a blocker for this environment. The capture plan
|
||||
still records licensing and security behavior because the managed client must
|
||||
match runtime behavior for secured writes, permission failures, and audit paths.
|
||||
|
||||
## Success criteria
|
||||
|
||||
The managed DLL is viable when it can perform these operations from an x64
|
||||
.NET 10 process using only managed code:
|
||||
|
||||
- discover or configure the local Galaxy/runtime endpoint,
|
||||
- resolve a Galaxy attribute reference such as `Object.Attribute`,
|
||||
- read current value, timestamp, quality, and status,
|
||||
- subscribe and receive initial value plus subsequent data changes,
|
||||
- write standard values and receive definitive completion status,
|
||||
- preserve bad-quality/status semantics when a platform or engine stops,
|
||||
- handle reconnect, heartbeat, runtime restart, and deploy/undeploy churn,
|
||||
- map common MXAccess errors to deterministic managed status codes.
|
||||
|
||||
## Known protocol gaps
|
||||
|
||||
Static analysis gave us interfaces and routing, but not enough wire/message
|
||||
detail. These are the missing pieces to capture:
|
||||
|
||||
| Area | Missing detail |
|
||||
| --- | --- |
|
||||
| NMX session startup | version negotiation, anonymous engine allocation, heartbeat setup, local/remote ids |
|
||||
| Endpoint discovery | how local Galaxy/platform/engine ids are chosen or discovered |
|
||||
| Item resolution | request body for `AddItem`, `AddItem2`, invalid-reference response |
|
||||
| Subscription | request body for `Advise`, `AdviseSupervisory`, buffered items, initial value behavior |
|
||||
| Data update | payload format for value, type, quality, timestamp, `MXSTATUS_PROXY` |
|
||||
| Reads | whether MXAccess implements read as transient subscription only or lower `IDataClient.Read2` messages can be used directly |
|
||||
| Writes | standard write body, timestamped write, array write, status and completion correlation |
|
||||
| Security | secured write, verified write, user id/GUID mapping, denied-write status |
|
||||
| Lifecycle | unregister/unadvise/remove cleanup messages |
|
||||
| Failure modes | platform down, engine down, bad reference, repository busy, wrong type, permission denied |
|
||||
|
||||
## Phase 1: controlled MXAccess harness
|
||||
|
||||
Build a tiny x86 harness that uses the primary MXAccess stack exactly as
|
||||
production does:
|
||||
|
||||
```text
|
||||
ArchestrA.MXAccess.dll -> LMXProxy.LMXProxyServer -> LmxProxy.dll
|
||||
```
|
||||
|
||||
Harness requirements:
|
||||
|
||||
- target `net48`, `x86`,
|
||||
- single STA thread,
|
||||
- deterministic client name, for example `MxProtoTraceHarness`,
|
||||
- configurable tag list and write values,
|
||||
- timestamps every high-level API call,
|
||||
- logs:
|
||||
- `Register` handle,
|
||||
- every `AddItem` item handle,
|
||||
- every `AdviseSupervisory`,
|
||||
- every `OnDataChange`,
|
||||
- every `Write`,
|
||||
- every `OnWriteComplete`,
|
||||
- every status array,
|
||||
- cleanup calls.
|
||||
|
||||
Initial scenarios:
|
||||
|
||||
1. Register/unregister only.
|
||||
2. Add/remove one known-good read-only item.
|
||||
3. Subscribe/unsubscribe one known-good item.
|
||||
4. One-shot read pattern: add, advise, wait first callback, unadvise, remove.
|
||||
5. Write one boolean or integer setpoint and capture completion.
|
||||
6. Add invalid item reference.
|
||||
7. Subscribe to a stopped/unavailable runtime host probe if available.
|
||||
|
||||
Artifacts:
|
||||
|
||||
- harness source,
|
||||
- timestamped harness log,
|
||||
- Process Monitor trace,
|
||||
- network trace,
|
||||
- optional API Monitor trace.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- one repeatable capture set where every high-level harness operation can be
|
||||
correlated to lower process/network activity.
|
||||
|
||||
## Phase 2: process and network capture
|
||||
|
||||
Capture layers:
|
||||
|
||||
1. Process Monitor:
|
||||
- processes: harness, `NmxSvc.exe`, `aaBootstrap`, relevant AppEngine/Platform processes,
|
||||
- operations: registry, file, process/thread, TCP/UDP where available,
|
||||
- objective: confirm DLLs, config, registry keys, and runtime dependencies touched per operation.
|
||||
|
||||
2. Network:
|
||||
- filter: host IP plus TCP/UDP port `5026`,
|
||||
- tools: Wireshark and/or `netsh trace`,
|
||||
- objective: recover packet framing and message timing.
|
||||
|
||||
3. COM/API call tracing:
|
||||
- target `LmxProxy.dll`, `Lmx.dll`, `NmxAdptr.dll`, `NmxSvc.exe`,
|
||||
- APIs of interest:
|
||||
- COM object creation,
|
||||
- `NmxSvc.NmxService.TransferData`,
|
||||
- `NmxAdptr.INmx4.PutRequest2`,
|
||||
- `NmxAdptr.INmx4.GetResponse2`,
|
||||
- `IDataClient.*` if used,
|
||||
- Winsock `sendto`, `recvfrom`, TCP send/recv.
|
||||
|
||||
Capture naming:
|
||||
|
||||
```text
|
||||
captures/
|
||||
001-register/
|
||||
002-additem-good/
|
||||
003-subscribe-good/
|
||||
004-write-good/
|
||||
005-additem-invalid/
|
||||
```
|
||||
|
||||
Each folder should contain:
|
||||
|
||||
```text
|
||||
harness.log
|
||||
procmon.pml
|
||||
network.pcapng
|
||||
notes.md
|
||||
```
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- identify which layer carries the actual item-resolution, subscribe, update,
|
||||
and write messages.
|
||||
- know whether the best implementation target is:
|
||||
- direct NMX socket protocol,
|
||||
- local COM `NmxSvc` automation,
|
||||
- lower `Lmx.dll` `IDataClient` behavior replicated over sockets.
|
||||
|
||||
## Phase 3: schema reconstruction
|
||||
|
||||
Start with messages that have simple cause/effect:
|
||||
|
||||
1. `Register` / startup heartbeat.
|
||||
2. `AddItem` good versus invalid reference.
|
||||
3. `AdviseSupervisory` initial value.
|
||||
4. `Write` and `OnWriteComplete`.
|
||||
|
||||
For each message:
|
||||
|
||||
- identify envelope:
|
||||
- magic/version,
|
||||
- message type,
|
||||
- source galaxy/platform/engine id,
|
||||
- destination galaxy/platform/engine id,
|
||||
- request handle/correlation id,
|
||||
- payload length,
|
||||
- checksum if any.
|
||||
- identify payload:
|
||||
- item reference string encoding,
|
||||
- context string,
|
||||
- item handle/id,
|
||||
- Mx data type,
|
||||
- quality,
|
||||
- FILETIME timestamp,
|
||||
- status category/source/detail.
|
||||
|
||||
Use differential captures:
|
||||
|
||||
- same operation, different tag name,
|
||||
- same tag, different value,
|
||||
- valid tag versus invalid tag,
|
||||
- boolean versus integer versus string,
|
||||
- normal write versus denied write.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- documented binary structs for the minimum viable read/subscribe/write path,
|
||||
- sample parser that can decode captured traffic into structured JSON.
|
||||
|
||||
## Phase 4: managed transport prototype
|
||||
|
||||
Build a new .NET 10 x64 prototype library:
|
||||
|
||||
```text
|
||||
src/ManagedLmxNmx/
|
||||
LmxNmxClient.cs
|
||||
NmxTransport.cs
|
||||
NmxFrame.cs
|
||||
LmxMessages.cs
|
||||
MxValueCodec.cs
|
||||
MxStatus.cs
|
||||
```
|
||||
|
||||
Prototype scope:
|
||||
|
||||
- connect to local NMX endpoint,
|
||||
- perform startup/version/heartbeat,
|
||||
- resolve one item,
|
||||
- subscribe to one item,
|
||||
- decode updates,
|
||||
- write one item,
|
||||
- disconnect cleanly.
|
||||
|
||||
Hard rules:
|
||||
|
||||
- no COM,
|
||||
- no native P/Invoke except optional OS primitives already wrapped by .NET,
|
||||
- no x86 process dependency,
|
||||
- deterministic timeouts and cancellation,
|
||||
- all binary parsers bounds-checked.
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- x64 test app receives the same value/quality/timestamp as MXAccess for a known tag.
|
||||
- x64 test app can write and observe `OnWriteComplete` equivalent status.
|
||||
|
||||
## Phase 5: parity suite
|
||||
|
||||
Create side-by-side tests:
|
||||
|
||||
```text
|
||||
MXAccess x86 harness result == Managed x64 result
|
||||
```
|
||||
|
||||
Parity matrix:
|
||||
|
||||
| Scenario | Expected |
|
||||
| --- | --- |
|
||||
| Good boolean read | same value, quality, timestamp within tolerance |
|
||||
| Good numeric read | same value/type/quality |
|
||||
| Good string read | same value/type/quality |
|
||||
| Invalid reference | same category/source/detail |
|
||||
| Subscribe initial callback | same initial value behavior |
|
||||
| Subscribe change callback | same change behavior |
|
||||
| Write allowed | same completion success |
|
||||
| Write wrong type | same failure detail |
|
||||
| Write denied | same security failure detail |
|
||||
| Platform stopped | same bad quality/status behavior |
|
||||
| Engine restart | reconnect and resubscribe |
|
||||
| Deploy/undeploy busy | same busy status |
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- managed client passes parity for the minimum production tag classes,
|
||||
- documented unsupported cases are explicit.
|
||||
|
||||
## Phase 6: production hardening
|
||||
|
||||
Before replacing the sidecar:
|
||||
|
||||
- fuzz message parsers with captured and mutated frames,
|
||||
- soak-test subscriptions with production-scale tag counts,
|
||||
- run AppEngine/Platform stop-start tests,
|
||||
- verify no unbounded queues,
|
||||
- verify reconnect backoff,
|
||||
- verify audit/security behavior for writes,
|
||||
- expose metrics:
|
||||
- connected state,
|
||||
- heartbeat age,
|
||||
- subscription count,
|
||||
- pending request count,
|
||||
- reconnect count,
|
||||
- bad frame count.
|
||||
|
||||
## Immediate next work items
|
||||
|
||||
Completed in the 2026-04-25 run:
|
||||
|
||||
1. Created the x86 MXAccess trace harness at `src\MxTraceHarness`.
|
||||
2. Queried the Galaxy repository for safe candidate tags using
|
||||
`analysis\sql\select_capture_tags.sql`.
|
||||
3. Captured register, add/remove, subscribe, invalid reference, array naming,
|
||||
and advised write scenarios under `captures\`.
|
||||
4. Converted `netsh.etl` traces to `network.pcapng` with `etl2pcapng`.
|
||||
5. Installed Wireshark 4.6.4 and attempted Npcap 1.87 installation.
|
||||
|
||||
Current blocker:
|
||||
|
||||
- The local NMX payload is on a loopback path (`::1`) that `netsh trace` and
|
||||
`pktmon` did not expose as pcap payload in this session. `dumpcap` cannot
|
||||
capture until Npcap installs successfully.
|
||||
|
||||
Next work items:
|
||||
|
||||
1. Complete an elevated/interactive Npcap install or use API Monitor.
|
||||
2. Recapture scalar subscribe, array subscribe, invalid subscribe, and advised
|
||||
write on the NMX loopback path.
|
||||
3. Write the first decoder for loopback NMX frame boundaries and timestamps.
|
||||
4. Decide whether direct socket protocol or lower `IDataClient` behavior is the
|
||||
better implementation target after loopback payloads are available.
|
||||
@@ -0,0 +1,426 @@
|
||||
# MxNativeSession API
|
||||
|
||||
`MxNativeSession` is the first high-level .NET 10 x64 facade over the managed
|
||||
NMX protocol work in this repository. It is intended to be the consumer-facing
|
||||
entry point while the lower layers remain available for protocol tests and
|
||||
diagnostics.
|
||||
|
||||
## Runtime prerequisites
|
||||
|
||||
- AVEVA System Platform/NMX installed locally so `NmxSvc.NmxService` can be
|
||||
activated.
|
||||
- The Galaxy Repository SQL database available to the current Windows session.
|
||||
- Managed NTLM runtime credentials supplied through the existing environment
|
||||
variable path used by `ManagedNtlmClientContext`.
|
||||
- The x64 callback COM registration/type-library setup already documented in
|
||||
`DotNet10-Native-Library-Plan.md`.
|
||||
|
||||
The session facade does not store credentials. It relies on the process
|
||||
environment and the already-authenticated Windows/SQL context.
|
||||
|
||||
## Basic write
|
||||
|
||||
```csharp
|
||||
using MxNativeClient;
|
||||
|
||||
using var session = MxNativeSession.Open(new MxNativeClientOptions
|
||||
{
|
||||
LocalEngineId = 0x7fee,
|
||||
EngineName = "MxNativeClient.Sample",
|
||||
// Optional. Leave null unless a deployment-specific heartbeat cadence is known.
|
||||
HeartbeatTicksPerBeat = null,
|
||||
});
|
||||
|
||||
await session.WriteAsync("TestChildObject.TestInt", 123);
|
||||
await session.Write2Async(
|
||||
"TestChildObject.TestInt",
|
||||
124,
|
||||
DateTime.UtcNow);
|
||||
```
|
||||
|
||||
`HeartbeatTicksPerBeat` and `HeartbeatMaxMissedTicks` expose
|
||||
`INmxService2.SetHeartbeatSendInterval`. A .NET 10 x64 probe validated
|
||||
`SetHeartbeatSendInterval(5, 3)` against local NmxSvc. The setting remains
|
||||
opt-in because the exact MXAccess heartbeat cadence has not been captured on
|
||||
this VM; leaving `HeartbeatTicksPerBeat` null preserves the previously
|
||||
validated connection behavior.
|
||||
|
||||
## Recovery
|
||||
|
||||
`MxNativeSession.RecoverConnection()` explicitly rebuilds the managed NMX
|
||||
service client, re-registers the local engine and callback, reapplies optional
|
||||
heartbeat settings, reconnects known publisher engines, and replays current
|
||||
normal or buffered subscriptions with their existing correlation IDs. The
|
||||
compatibility facade exposes the same operation as
|
||||
`MxNativeCompatibilityServer.RecoverConnection(serverHandle)`.
|
||||
|
||||
`RecoverConnectionAsync(policy, cancellationToken)` adds an explicit retry loop
|
||||
around that primitive. `MxNativeRecoveryPolicy.MaxAttempts` defaults to `1`;
|
||||
`Delay` defaults to zero. The compatibility facade exposes the same retrying
|
||||
operation as `RecoverConnectionAsync(serverHandle, policy, cancellationToken)`.
|
||||
|
||||
Recovery progress is observable:
|
||||
|
||||
- `RecoveryAttemptStarted` fires before each reconnect/replay attempt.
|
||||
- `RecoveryAttemptFailed` fires after a recoverable attempt failure and reports
|
||||
the exception plus whether the retry loop will continue.
|
||||
- `RecoveryCompleted` fires after the replacement service has been registered,
|
||||
publisher endpoints have been reconnected, subscriptions have been replayed,
|
||||
and the session has swapped to the recovered service.
|
||||
|
||||
The compatibility facade exposes the same events with the server handle added
|
||||
to each event payload. A live .NET 10 x64 probe with two allowed attempts and a
|
||||
100 ms delay observed one started event, zero failed events, one completed
|
||||
event, preserved the existing subscription count, and wrote through the
|
||||
recovered session.
|
||||
|
||||
Callbacks are passed through during the recovery window instead of being
|
||||
suppressed or buffered. `MxNativeCallbackEvent`,
|
||||
`MxNativeOperationStatusEvent`, `MxNativeReferenceRegistrationEvent`, and
|
||||
`MxNativeUnparsedCallbackEvent` include `IsDuringRecovery` so callers can
|
||||
separate replay-window events from steady-state events. Compatibility
|
||||
`DataChanged`, `BufferedDataChanged`, and `WriteCompleted` payloads carry the
|
||||
same marker.
|
||||
|
||||
`MxNativeClient.Probe --probe-session-recover-multi` subscribes to multiple
|
||||
tags, runs explicit recovery, and reports total callbacks plus
|
||||
`IsDuringRecovery` counts for data, operation-status, reference-registration,
|
||||
and unparsed callback families. The first live run replayed four subscriptions
|
||||
and preserved all four, with zero callback events marked as recovery-window
|
||||
events on this VM.
|
||||
|
||||
Adding `--recover-concurrent-writes` starts a separate writer session during
|
||||
the same recovery window. A live run wrote `TestChildObject.TestInt` values
|
||||
`330`-`334`, preserved all four subscriptions, and observed two data callbacks
|
||||
with `IsDuringRecovery=true`. This confirms that callbacks are not quiesced by
|
||||
the library during recovery; consumers that require strict post-recovery
|
||||
ordering should ignore or queue marked events at their boundary.
|
||||
|
||||
Automatic background recovery is intentionally not enabled yet. Retrying writes
|
||||
inside normal write calls can duplicate writes, so callers should choose where
|
||||
recovery belongs in their own operation model. A .NET 10 x64 probe validated the
|
||||
current explicit path by subscribing to `TestChildObject.TestInt`, recovering
|
||||
the connection, preserving the subscription count, and writing through the
|
||||
recovered session.
|
||||
|
||||
`WriteSecured2Async` is implemented for the observed boolean secured/verified
|
||||
payload shape:
|
||||
|
||||
```csharp
|
||||
await session.WriteSecured2Async(
|
||||
"TestMachine_001.ProtectedValue",
|
||||
true,
|
||||
DateTime.Now,
|
||||
currentUserId: 1,
|
||||
verifierUserId: 0);
|
||||
```
|
||||
|
||||
The encoder emits native command `0x38` and has been live validated from .NET
|
||||
10 x64 against the secured and verified boolean tags deployed on this node, as
|
||||
well as authenticated bool, int, float, double, string, datetime, and
|
||||
scalar-array calls against `TestChildObject` attributes. The handle-based
|
||||
compatibility facade also supports `AuthenticateUser` followed by
|
||||
`WriteSecured2`, and intentionally does not synthesize `OnWriteComplete` for
|
||||
this path because the successful native captures did not show that event.
|
||||
`WriteSecuredAsync` remains unsupported because native MXAccess still returns
|
||||
`0x80004021` before emitting a value-bearing body in every captured scenario.
|
||||
Additional native captures beyond bool and int would improve fixture coverage,
|
||||
but the managed encoder is now generic over the timestamped write-body support.
|
||||
|
||||
## Browse
|
||||
|
||||
```csharp
|
||||
IReadOnlyList<GalaxyTagMetadata> tags =
|
||||
await session.BrowseAsync("TestChildObject", "Test%", maxRows: 25);
|
||||
```
|
||||
|
||||
`BrowseAsync` uses Galaxy Repository metadata directly. It does not call the
|
||||
x86 LMX resolver and returns the same metadata needed by the managed handle and
|
||||
wire encoders.
|
||||
|
||||
`ResolveAsync` accepts full references in the observed MXAccess forms:
|
||||
`Object.Attribute`, `Object.Primitive.Attribute`, and
|
||||
`Object.Primitive.Dotted.Attribute` for primitive attributes such as
|
||||
`TestMachine_001.TestAlarm001.Alarm.TimeDeadband`. It also recognizes the
|
||||
captured literal property form `Object.Attribute.property(buffer)`, resolving it
|
||||
to the base attribute handle with property id `0x32`.
|
||||
|
||||
Each `GalaxyTagMetadata` exposes `IsSupportedValueKind` and
|
||||
`TryGetValueKind(out MxValueKind valueKind)`. Use those before write/read
|
||||
projection when browsing broad GR metadata. The current wire codec supports the
|
||||
live OPC-UA-critical GR types `Boolean`, `Integer`, `Float`, `Double`, `String`,
|
||||
`Time`, and array forms captured so far. Live GR inspection also found
|
||||
`ElapsedTime` and `InternationalizedString`. Subscribe/read callback decoding now
|
||||
handles the observed `ElapsedTime` wire kind `0x07` as `TimeSpan` and the compact
|
||||
empty `InternationalizedString`/string payload form `0x05 04 00 00 00` as
|
||||
`string.Empty`. They are still reported as unsupported by `TryGetValueKind`
|
||||
because they are not core one-to-one data kinds, but write projection now follows
|
||||
captured MXAccess caller-variant behavior: `ElapsedTime` values supplied as
|
||||
`TimeSpan` or integer project to integer milliseconds/wire kind `0x02`, and
|
||||
`InternationalizedString` values project to normal string wire kind `0x05`.
|
||||
|
||||
`WriteAsync` and `Write2Async` resolve the tag from Galaxy Repository metadata,
|
||||
build the managed `MxReferenceHandle`, encode the NMX write body, wrap it in
|
||||
the `TransferData` envelope, and call `INmxService2::TransferData` through the
|
||||
managed DCOM path.
|
||||
|
||||
## Subscribe
|
||||
|
||||
```csharp
|
||||
using MxNativeClient;
|
||||
|
||||
using var session = MxNativeSession.Open();
|
||||
|
||||
session.CallbackReceived += (_, evt) =>
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"{evt.Record.TimestampUtc:O} status={evt.Status.Category} quality=0x{evt.Record.Quality:X4} value={evt.Record.Value}");
|
||||
};
|
||||
|
||||
session.OperationStatusReceived += (_, evt) =>
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"operation_status=0x{evt.Message.StatusCode:X4} status={evt.Message.Status.Category}");
|
||||
};
|
||||
|
||||
MxNativeSubscription sub = await session.SubscribeAsync("TestChildObject.TestInt");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||
session.Unsubscribe(sub.CorrelationId);
|
||||
```
|
||||
|
||||
## Read
|
||||
|
||||
```csharp
|
||||
object? value = await session.ReadAsync(
|
||||
"TestChildObject.TestInt",
|
||||
timeout: TimeSpan.FromSeconds(10));
|
||||
```
|
||||
|
||||
`ReadAsync` is implemented as a transient subscription read. It subscribes,
|
||||
waits for the first typed value callback matching that item correlation, then
|
||||
unsubscribes.
|
||||
|
||||
`SubscribeAsync` performs the managed equivalent of the observed MXAccess path:
|
||||
|
||||
1. Resolve tag metadata from the Galaxy Repository.
|
||||
2. Connect the local engine to the tag's owning engine.
|
||||
3. Add the local engine as a subscriber.
|
||||
4. Send the generated item-control `0x1f` body using the native value handle.
|
||||
5. Decode incoming `INmxSvcCallback` bodies with `NmxSubscriptionMessage`.
|
||||
|
||||
Capture `099-frida-plain-advise-testint` showed public `Advise` and the earlier
|
||||
`AdviseSupervisory` scalar path using the same `0x1f` body shape for
|
||||
`TestChildObject.TestInt`, so the compatibility wrapper maps both methods onto
|
||||
this subscription path.
|
||||
|
||||
`CallbackReceived` is raised for typed `0x32` subscription-status and `0x33`
|
||||
data-update records. Each callback exposes the raw NMX record fields plus a
|
||||
first-pass `MxStatus` projection. `MxStatus.DetailText` returns the installed
|
||||
English `Lmx.aaDCT` detail text for known LMX/NMX status details, including
|
||||
reference, conversion, security, alarm, initializing, and secured/verified
|
||||
write conditions. `OperationStatusReceived`
|
||||
is raised for both observed operation-status frame forms. The non-length-prefixed
|
||||
5-byte status-word form with status word `0x8050` and completion byte `0x00`
|
||||
maps to `MxStatus.WriteCompleteOk` and is the captured public
|
||||
`OnWriteComplete` success notification. Length-prefixed completion-only frames
|
||||
are decoded separately and expose `NmxOperationStatusFormat.CompletionOnly`.
|
||||
Captures `089`, `092`, and `093` produced completion byte `0x41` after
|
||||
wrong-type string writes, and capture `091` produced completion byte `0x00`
|
||||
after double-to-int coercion; MXAccess did not raise `OnWriteComplete` for
|
||||
either completion-only form.
|
||||
|
||||
The managed x64 path now has a proven value-bearing subscription fixture:
|
||||
`SubscribeAsync("TestChildObject.ShortDesc")` receives the same compact empty
|
||||
string callback observed from x86 MXAccess (`0x32`, wire kind `0x05`, quality
|
||||
`0x00C0`). The key handle detail is that value references use property id `10`;
|
||||
GR `mx_attribute_category` is metadata and is not the NMX value-handle property
|
||||
field.
|
||||
|
||||
String-array callback handling is intentionally conservative. Captures `100`
|
||||
and `101` showed `0x45` string-array callback records whose buffers stopped
|
||||
inside the final element, and MXAccess did not raise a public data-change event
|
||||
for those runs. The decoder does not fabricate a value for malformed array
|
||||
payloads; callers should treat a `null` value with a known array wire kind as an
|
||||
incomplete callback until a complete/public string-array callback is captured.
|
||||
|
||||
`ReferenceRegistrationReceived` is raised for decoded `0x11`
|
||||
reference-registration result frames. Those frames are emitted after the `0x10`
|
||||
normal/buffered registration request and include the item handle, correlation
|
||||
ID, reference text, context, and MX data type.
|
||||
|
||||
`UnparsedCallbackReceived` is a diagnostic event for callback bodies that are
|
||||
not yet part of the high-level API surface. It is currently used to expose
|
||||
frames such as the 92-byte operation/status-shaped body seen before scalar
|
||||
subscription status callbacks, rather than silently swallowing unknown protocol
|
||||
bodies during reverse engineering.
|
||||
|
||||
## Probe commands
|
||||
|
||||
The probe now has facade-level entry points:
|
||||
|
||||
```powershell
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-write --tag=TestChildObject.TestInt --value=123 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-read --tag=TestChildObject.TestInt --read-timeout-seconds=10 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-subscribe --tag=TestChildObject.TestInt --subscribe-hold-seconds=5 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-subscribe --tag=TestChildObject.ShortDesc --subscribe-hold-seconds=8 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe --tag=TestChildObject.ShortDesc --subscribe-hold-seconds=8 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe-write --tag=TestChildObject.TestInt --value=793 --subscribe-hold-seconds=8 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe --tag=NoSuchObject_999.NoSuchAttr --subscribe-hold-seconds=2 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-subscribe-multi --tag=TestChildObject.ShortDesc --tag=TestChildObject.TestInt --tag=NoSuchObject_999.NoSuchAttr --subscribe-hold-seconds=4 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-write-multi --subscribe-hold-seconds=4 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-compatibility-write-unadvised --tag=TestChildObject.TestInt --value=99 --objref-only
|
||||
dotnet run --project .\src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-managed-subscribe --tag=TestChildObject.TestInt --subscribe-hold-seconds=5 --send-observed-preadvise-metadata --objref-only
|
||||
```
|
||||
|
||||
These commands require the same runtime managed NTLM environment as the lower
|
||||
level managed probes.
|
||||
|
||||
## MXAccess-style compatibility wrapper
|
||||
|
||||
`MxNativeCompatibilityServer` provides an MXAccess-like handle model for code
|
||||
that expects integer server and item handles:
|
||||
|
||||
```csharp
|
||||
using MxNativeClient;
|
||||
|
||||
using var mx = new MxNativeCompatibilityServer();
|
||||
|
||||
mx.DataChanged += (_, evt) =>
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"server={evt.ServerHandle} item={evt.ItemHandle} value={evt.Value} quality=0x{evt.Quality:X4}");
|
||||
};
|
||||
|
||||
mx.WriteCompleted += (_, evt) =>
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"server={evt.ServerHandle} item={evt.ItemHandle} status={evt.Statuses[0].Category}");
|
||||
};
|
||||
|
||||
int server = mx.Register("OpcUaBridge.Native");
|
||||
int item = mx.AddItem(server, "TestChildObject.TestInt");
|
||||
mx.AdviseSupervisory(server, item);
|
||||
mx.Write(server, item, 123);
|
||||
mx.UnAdvise(server, item);
|
||||
mx.RemoveItem(server, item);
|
||||
mx.Unregister(server);
|
||||
```
|
||||
|
||||
The compatibility wrapper maps `Register`, `Unregister`, `AddItem`,
|
||||
`RemoveItem`, `Advise`, `AdviseSupervisory`, `UnAdvise`, `Write`, and `Write2`
|
||||
onto `MxNativeSession`. `Suspend` and `Activate` mirror observed MXAccess local
|
||||
behavior for advised items: unadvised calls throw, `Suspend` returns
|
||||
pending/requesting-LMX status, and `Activate` returns ok/requesting-LMX status.
|
||||
After `RemoveItem`, the wrapper mirrors native x86 stale-item behavior by
|
||||
returning `ArgumentException` with `HResult=0x80070057` for advise, unadvise,
|
||||
write, write2, suspend, activate, and repeated remove attempts.
|
||||
Invalid server handles and cross-server item handles use the same
|
||||
`ArgumentException`/`0x80070057` shape observed from native MXAccess.
|
||||
Literal `property(buffer)` items added with `AddItem` are not treated as
|
||||
`AddBufferedItem` handles. They follow the captured MXAccess literal-reference
|
||||
path: add/advise succeeds, normal write returns without a public write-complete,
|
||||
and no public data-change is promoted on the current VM state.
|
||||
`AddItem2` resolves the item definition directly first and then retries with the
|
||||
supplied context. This covers the captured simple form
|
||||
`AddItem2("TestInt", "TestChildObject")` plus dotted relative forms such as
|
||||
`AddItem2("Alarm.TimeDeadband", "TestMachine_001.TestAlarm001")` and
|
||||
`AddItem2("TestInt.property(buffer)", "TestChildObject")`.
|
||||
`AddBufferedItem` and `SetBufferedUpdateInterval` now cover the decoded public
|
||||
API path: buffered add creates a handle, the interval is stored as local session
|
||||
state, and advising the buffered handle sends the observed NMX `0x10`
|
||||
registration for `itemDefinition.property(buffer)` in the supplied item
|
||||
context. Headless Ghidra analysis shows native MXAccess routes buffered item
|
||||
callbacks through the same `OnDataChange` callback method as normal items, then
|
||||
branches to `_ILMXProxyServerEvents2::OnBufferedDataChange` when the item record
|
||||
is marked buffered. `MxNativeCompatibilityServer.BufferedDataChanged` mirrors
|
||||
that separation for any parsed buffered callback and does not promote buffered
|
||||
items through `DataChanged`. Captures against `TestChildObject.TestInt` and
|
||||
GR-confirmed historized `TestMachine_001.TestHistoryValue` prove the outbound
|
||||
context-bearing registration/result bodies, but this VM has not emitted a live
|
||||
`OnBufferedDataChange` payload. True multi-sample buffered payload decoding
|
||||
still needs that runtime condition.
|
||||
`ArchestrAUserToId` follows the observed MXAccess x86
|
||||
behavior on this node: known user GUIDs and an invalid zero GUID all return `1`.
|
||||
`AuthenticateUser` follows the same session-local handle model for the observed
|
||||
dev-node behavior: MXAccess returned `S_OK` and user ID `1` for both
|
||||
`Administrator` and an invalid user name with an empty password. The managed
|
||||
compatibility method validates the server handle and user string, returns a
|
||||
session-local handle, and does not store or compare password material.
|
||||
`WriteCompleted` is raised only for the captured MXAccess-visible 5-byte
|
||||
`0x8050/0x00` operation-status frame. Completion-only frames are still available
|
||||
through `MxNativeSession.OperationStatusReceived`, but the compatibility wrapper
|
||||
does not convert them into `OnWriteComplete` events because x86 MXAccess did not
|
||||
fire that event in those captures. Pending write handles are queued per server
|
||||
session, so concurrent compatibility sessions do not consume each other's
|
||||
completion callbacks. The mixed `--probe-compatibility-write-multi` path
|
||||
validates that normal int, internationalized string, literal buffer-property,
|
||||
and invalid-reference items in one server session do not leak public data-change
|
||||
or write-complete events across item handles; only the invalid reference emits
|
||||
the expected configuration-error data-change. `OperationCompleted` keeps the public event
|
||||
shape, but no trigger has been modeled yet because the capture set contains no
|
||||
`mx.event.operation-complete` events.
|
||||
`DataChanged` is also suppressed when the lower-level decoder reports a known
|
||||
wire kind with a `null` value, which is how malformed/incomplete callback
|
||||
payloads such as the observed string-array records from captures `100` and
|
||||
`101` are represented.
|
||||
`GalaxyRepositoryUserResolver` is separate metadata support: it resolves
|
||||
`dbo.user_profile` rows, profile IDs, default security group, InTouch access
|
||||
level, and decoded role names from the GR role blob. `AddItem2` currently
|
||||
combines context and item text only for simple relative references; the real
|
||||
MXAccess context behavior still needs captures. Unsupported MXAccess methods
|
||||
throw `NotSupportedException` with the missing capture/protocol reason.
|
||||
|
||||
For lower-level use, `GalaxyRepositoryUserResolver` exposes:
|
||||
|
||||
```csharp
|
||||
var users = new GalaxyRepositoryUserResolver();
|
||||
GalaxyUserProfile profile = await users.ResolveByNameAsync("Administrator");
|
||||
int profileId = await users.ResolveUserProfileIdByGuidAsync(profile.UserGuid);
|
||||
IReadOnlyList<string> roles = profile.Roles;
|
||||
```
|
||||
|
||||
Security-enabled `AuthenticateUser` password verification remains intentionally
|
||||
out of scope until successful and failed captures from a security-enabled Galaxy
|
||||
show whether MXAccess delegates to OS/domain auth, GR hashes, or another token
|
||||
provider.
|
||||
|
||||
## Current limitations
|
||||
|
||||
- Managed NTLM live probes now validate .NET 10 x64 activation, RemQI,
|
||||
`GetPartnerVersion`, direct `WriteAsync` transport, callback delivery, and
|
||||
unsubscribe cleanup with unique local engine IDs.
|
||||
- Value-bearing managed `OnDataChange` is proven for
|
||||
`TestChildObject.ShortDesc` after matching native advise transfer kind and
|
||||
value-handle property id. The lower-level session exposes that callback, but
|
||||
the MXAccess-style compatibility wrapper suppresses the empty
|
||||
`InternationalizedString` public `DataChanged` event because fresh x86
|
||||
MXAccess capture `108` also raises no public event for `ShortDesc`. Fresh
|
||||
current-state x86 MXAccess captures also raise no public `TestInt`
|
||||
data-change for subscribe-only or subscribe-then-write, matching the managed
|
||||
status-only `TestInt` callbacks on this VM. Broader scalar/array parity still
|
||||
needs live validation against a runtime state that emits public values.
|
||||
Replaying the captured
|
||||
adapter-visible `0x17` metadata body is accepted by `NmxSvc` and produces
|
||||
decoded `0x40` metadata-response plus `0x32` metadata-status callbacks, but
|
||||
the response still contains an internal base-object error and does not appear
|
||||
to be the gate for the `ShortDesc` value callback.
|
||||
- `ReadAsync` currently uses a transient subscription. A lower-latency direct
|
||||
read path can be added if a distinct NMX request/response read body is found;
|
||||
tags with no initial value can still time out after receiving only status
|
||||
callbacks.
|
||||
- String-array callbacks are partially modeled from the element format, but the
|
||||
available Frida string-array callback line is truncated before all values.
|
||||
- Error/failure callback bodies still need more captures for exact
|
||||
`MXSTATUS_PROXY[]` parity.
|
||||
- `ElapsedTime` and `InternationalizedString` writes are value-projected from
|
||||
captures `095` and `096`. The managed `ShortDesc` write body now matches
|
||||
capture `096` byte-for-byte and returns the same completion-only `0xef` as
|
||||
native MXAccess on this node, while `TestInt` returns completion-only `0x00`
|
||||
and a status-only update callback. Public write-complete semantics for these
|
||||
completion-only frames still need more captures.
|
||||
- `MxNativeCompatibilityServer` provides source-level migration help but is not
|
||||
a binary COM replacement for `ArchestrA.MxAccess`.
|
||||
- For invalid references, the compatibility wrapper intentionally differs from
|
||||
strict `MxNativeSession`: it returns an item handle and raises the observed
|
||||
public configuration-error data-change (`null`, quality `0`, detail `6`) on
|
||||
advise, matching x86 MXAccess captures.
|
||||
@@ -0,0 +1,670 @@
|
||||
# NMX COM contracts and managed-client implications
|
||||
|
||||
This note captures the COM/type-library layer that sits below MXAccess and above
|
||||
the local NMX transport.
|
||||
|
||||
## Native binaries inspected
|
||||
|
||||
Primary installed files:
|
||||
|
||||
```text
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxAdptr.dll
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvcps.dll
|
||||
C:\Program Files (x86)\ArchestrA\Framework\Bin\WWProxyStub.dll
|
||||
```
|
||||
|
||||
No x64 equivalents were found under `C:\Program Files`; only x86 installed
|
||||
copies and Galaxy file-repository copies were present.
|
||||
|
||||
Generated inspection reports:
|
||||
|
||||
```text
|
||||
analysis\native\LmxProxy.dll.md
|
||||
analysis\native\NmxAdptr.dll.md
|
||||
analysis\native\NmxSvcps.dll.md
|
||||
analysis\native\WWProxyStub.dll.md
|
||||
```
|
||||
|
||||
`NmxSvcps.dll` is a MIDL COM proxy/stub DLL. It exports:
|
||||
|
||||
```text
|
||||
DllCanUnloadNow
|
||||
DllGetClassObject
|
||||
DllRegisterServer
|
||||
DllUnregisterServer
|
||||
```
|
||||
|
||||
It imports the expected RPC proxy/stub helpers from `RPCRT4.dll`:
|
||||
|
||||
```text
|
||||
NdrDllGetClassObject
|
||||
NdrDllRegisterProxy
|
||||
NdrDllUnregisterProxy
|
||||
CStdStubBuffer_*
|
||||
IUnknown_*_Proxy
|
||||
```
|
||||
|
||||
## Key NMX service contracts
|
||||
|
||||
The already decompiled interop tree contains the type-library contracts:
|
||||
|
||||
```text
|
||||
analysis\decompiled-interop\Interop.NmxSvc
|
||||
analysis\decompiled-interop\Interop.NmxAdptr
|
||||
```
|
||||
|
||||
Important interfaces:
|
||||
|
||||
| Interface | GUID | Important methods |
|
||||
| --- | --- | --- |
|
||||
| `INmxService` | `575008DB-845D-46C6-A906-F6F8CA86F315` | `RegisterEngine`, `UnRegisterEngine`, `Connect`, `TransferData`, subscriber and heartbeat methods |
|
||||
| `INmxService2` | `2630A513-A974-4B1A-8025-457A9A7C56B8` | `RegisterEngine2`, `GetPartnerVersion` |
|
||||
| `INmxSvcCallback` | `B49F92F7-C748-4169-8ECA-A0670B012746` | `DataReceived`, `StatusReceived` |
|
||||
| `INmxNotify` | `73849AEA-472A-4715-B8C6-1C806AF12DFC` | `ConnectionEstablished`, `ConnectionClosed` |
|
||||
| `INmx4` | `84168012-B544-4217-A145-32819C607435` | `PutRequest2`, `GetResponse2`, `Initialize2`, `InitializeAnonymous2` |
|
||||
|
||||
Core transport methods:
|
||||
|
||||
```csharp
|
||||
void INmxService.TransferData(
|
||||
int lRemoteGalaxyID,
|
||||
int lRemotePlatformID,
|
||||
int lRemoteEngineID,
|
||||
int lSize,
|
||||
ref byte pMsgBody);
|
||||
|
||||
void INmxSvcCallback.DataReceived(
|
||||
int dwBufferSize,
|
||||
ref sbyte lpDataBuffer);
|
||||
|
||||
void INmxSvcCallback.StatusReceived(
|
||||
int dwBufferSize,
|
||||
ref sbyte lpStatusBuffer);
|
||||
|
||||
void INmx4.PutRequest2(
|
||||
int dwClusterId,
|
||||
int dwPlatformId,
|
||||
int dwEngineId,
|
||||
byte byPriority,
|
||||
byte byType,
|
||||
int dwSize,
|
||||
ref byte pData,
|
||||
out int pdwRequestHandle);
|
||||
|
||||
void INmx4.GetResponse2(
|
||||
byte byType,
|
||||
out int pdwResponseCode,
|
||||
out int pdwRequestHandle,
|
||||
out int pdwSize,
|
||||
IntPtr pData);
|
||||
```
|
||||
|
||||
## Local mixed stream
|
||||
|
||||
The localhost `127.0.0.1:57415 <-> 127.0.0.1:57433` stream is not plain
|
||||
DCE/RPC. It is a compact mixed protocol:
|
||||
|
||||
- 12-byte control records: `int32 code_or_status`, `int32 token_low`,
|
||||
`int32 token_high`.
|
||||
- Data records: `uint32 body_length`, followed by `body_length` bytes.
|
||||
- A positive control `code_or_status` often announces the total byte count of
|
||||
one or more following data records.
|
||||
- `-1` appears as a normal acknowledgement/status control.
|
||||
- `-2` appears as a bidirectional status/control marker around write windows.
|
||||
|
||||
The parser for this stream is:
|
||||
|
||||
```text
|
||||
analysis\scripts\decode_mixed_local_stream.py
|
||||
```
|
||||
|
||||
Generated mixed-stream decodes:
|
||||
|
||||
```text
|
||||
captures\016-loopback-write-test-int-advised\mixed-stream-57415-to-57433.tsv
|
||||
captures\016-loopback-write-test-int-advised\mixed-stream-57433-to-57415.tsv
|
||||
captures\017-loopback-write-test-int-100\mixed-stream-57415-to-57433.tsv
|
||||
captures\017-loopback-write-test-int-100\mixed-stream-57433-to-57415.tsv
|
||||
captures\020-loopback-write-test-int-102\mixed-stream-57415-to-57433.tsv
|
||||
captures\020-loopback-write-test-int-102\mixed-stream-57433-to-57415.tsv
|
||||
```
|
||||
|
||||
Write-window extraction and diff helpers:
|
||||
|
||||
```text
|
||||
analysis\scripts\analyze_write_window.py
|
||||
analysis\scripts\diff_write_window_records.py
|
||||
```
|
||||
|
||||
## Controlled write captures
|
||||
|
||||
Usable value-change captures:
|
||||
|
||||
| Folder | Write path |
|
||||
| --- | --- |
|
||||
| `captures\017-loopback-write-test-int-100` | `TestInt` changed from `99` to `100` |
|
||||
| `captures\020-loopback-write-test-int-102` | `TestInt` changed from `101` to `102` |
|
||||
| `captures\021-loopback-write-test-int-sequence-103-105` | `TestInt` changed from `102` to `103`, `104`, then `105` in one session |
|
||||
|
||||
`captures\018-loopback-write-test-int-101` has a successful harness log but a
|
||||
header-only pcap, so it should not be used for packet analysis. The rerun
|
||||
`captures\019-loopback-write-test-int-101-rerun` captured correctly, but it was
|
||||
a same-value write because the attribute was already `101`.
|
||||
|
||||
The same-session sequence shows that the decoded write-window records do not
|
||||
carry the requested `int32` values as plain little-endian scalar payloads. The
|
||||
visible moving fields are mostly local sequence tokens and opaque body fields.
|
||||
For a managed implementation, this raises the priority of tracing the in-process
|
||||
native API boundary before the payload enters the localhost transport.
|
||||
|
||||
That boundary has now been traced with headless Ghidra-derived Frida hooks. See:
|
||||
|
||||
```text
|
||||
docs\Ghidra-Headless-Analysis.md
|
||||
captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv
|
||||
```
|
||||
|
||||
The key result is that `CLMXProxyServer::Write` receives the raw `int32` scalar
|
||||
directly, and `CNmxAdapter::PutRequest` receives a 40-byte body with the scalar
|
||||
at offset `18`. `CNmxAdapter::TransferData` wraps that body in an 86-byte
|
||||
message, placing the scalar at offset `64`. The corresponding
|
||||
`ProcessDataReceived` update body carries the scalar at offset `84`.
|
||||
|
||||
Additional Frida captures generalized the write body across common scalar
|
||||
types:
|
||||
|
||||
| Type | `PutRequest` body | `TransferData` body | Callback/update body | Encoding |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| bool | size `37`, value offset `18` | size `83`, value offset `64` | size `85`, value 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`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `int32` |
|
||||
| float | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `float32` |
|
||||
| double | size `44`, value offset `18` | size `90`, value offset `64` | size `92`, value offset `84` | little-endian `float64` |
|
||||
| string | size `58` or `60`, value offset `26` | size `104` or `106`, value offset `72` | size `106` or `108`, value offset `92` | UTF-16LE |
|
||||
| datetime | size `86`, value offset `26` | size `132`, value offset `72` | size `98`, value offset `88` | outbound UTF-16LE display string; callback/update FILETIME |
|
||||
|
||||
The matrix is saved at:
|
||||
|
||||
```text
|
||||
analysis\frida\write-body-matrix.tsv
|
||||
```
|
||||
|
||||
Array writes are also captured:
|
||||
|
||||
```text
|
||||
analysis\frida\write-array-body-matrix.tsv
|
||||
```
|
||||
|
||||
Write-mode captures are saved at:
|
||||
|
||||
```text
|
||||
analysis\frida\write-mode-matrix.tsv
|
||||
```
|
||||
|
||||
The first combined Frida plus loopback correlation is:
|
||||
|
||||
```text
|
||||
captures\043-frida-loopback-write-test-int-115
|
||||
captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv
|
||||
captures\044-frida-loopback-write-test-int-123456789
|
||||
captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv
|
||||
```
|
||||
|
||||
Numeric arrays use an array descriptor at body offset `17`, then packed values
|
||||
at offset `28`. The descriptor is:
|
||||
|
||||
```text
|
||||
kind_byte 00 00 00 00 element_count:uint16 element_width_or_code:uint32
|
||||
```
|
||||
|
||||
Observed kind bytes are `0x41` bool, `0x42` int, `0x43` float, `0x44` double,
|
||||
and `0x45` variable-width string/date. String and datetime arrays use
|
||||
per-element variable records; outbound datetime array writes encode display
|
||||
strings, while callback/update bodies encode FILETIME values. The bool-array
|
||||
capture succeeded but did not preserve the requested alternating pattern, so
|
||||
that path is documented as unresolved pending a targeted follow-up.
|
||||
|
||||
Secured and verified attributes did not require the COM `WriteSecured` methods.
|
||||
Those methods returned before value-bearing NMX requests in the tested cases.
|
||||
The supported public path was normal `Write` with the fourth argument set to the
|
||||
Galaxy security classification (`2` for `SecuredWrite`, `3` for
|
||||
`VerifiedWrite`). Timestamped `Write2` keeps the scalar value slot and embeds a
|
||||
FILETIME after the value.
|
||||
|
||||
The adapter body is still not the wire format. Capture `043` proves that exact
|
||||
`PutRequest`, `TransferData`, and callback bodies are absent from the
|
||||
reassembled TCP streams. Capture `044` uses the distinctive value `123456789`
|
||||
and shows the raw scalar is absent from the full pcap payload scan, parsed
|
||||
DCE/RPC stubs, and mixed local stream. A native managed client therefore needs a
|
||||
structural DCE/RPC/NDR decoder in addition to the adapter-body codec.
|
||||
|
||||
The initial managed codec is:
|
||||
|
||||
```text
|
||||
src\MxNativeCodec\MxNativeCodec.csproj
|
||||
src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj
|
||||
```
|
||||
|
||||
It does not yet synthesize every unknown header field. It uses captured
|
||||
`PutRequest` bodies as templates and proves that scalar typed value slots,
|
||||
timestamped scalar bodies, packed numeric arrays, string-array records, and the
|
||||
write index can be decoded and re-encoded in .NET 10 x64 managed code.
|
||||
|
||||
## Implication for .NET 10 x64
|
||||
|
||||
The public COM contracts are useful as a schema, but the installed runtime path
|
||||
is x86. A full managed .NET 10 x64 implementation cannot simply load these
|
||||
in-proc COM components.
|
||||
|
||||
The service boundary was traced directly in capture `046`:
|
||||
|
||||
```text
|
||||
captures\046-service-boundary-write-test-int-123456791
|
||||
```
|
||||
|
||||
The same 86-byte write body appears at:
|
||||
|
||||
```text
|
||||
CNmxAdapter.TransferData
|
||||
CNmxService.TransferData
|
||||
CNmxControler.TransferData
|
||||
CNmxControler.DataReceived
|
||||
CNmxControler.ProcessDataReceivedForEngine
|
||||
```
|
||||
|
||||
The distinctive scalar `123456791` is at body offset `64` in every one of those
|
||||
86-byte bodies.
|
||||
|
||||
The `TransferData` service body is a 46-byte NMX envelope followed by the
|
||||
adapter `PutRequest` body. The envelope stores the inner body length at offset
|
||||
`2`. For the observed `TestInt=123456791` write:
|
||||
|
||||
Do not send a bare 46-byte envelope as a normal probe payload. If the adapter
|
||||
receives a header-only or length-mismatched `TransferData` body, System Platform
|
||||
can log `NMX Header ... buffer size pktHeader.dwDataSize ... doesn't match
|
||||
received message size ...`. The local COM and managed DCE/RPC harnesses now
|
||||
validate this envelope length before sending.
|
||||
|
||||
| Body | Size | Value offset |
|
||||
| --- | ---: | ---: |
|
||||
| `CNmxAdapter.PutRequest` | 40 | 18 |
|
||||
| `INmxService2.TransferData` / `CNmxAdapter.TransferData` | 86 | 64 |
|
||||
|
||||
The managed codec now includes `NmxTransferEnvelopeTemplate` to decode and
|
||||
re-encode this observed envelope form.
|
||||
|
||||
The .NET 10 x64 probe can activate `NmxSvc.NmxService`, but the first
|
||||
`INmxService2` method call fails with `0x8007000B` because COM tries to load the
|
||||
32-bit `NmxSvcps.dll` proxy/stub into the x64 process. The probe is:
|
||||
|
||||
```text
|
||||
src\MxNativeClient.Probe
|
||||
```
|
||||
|
||||
The same probe can marshal the activated object as remote `IUnknown` and dump a
|
||||
standard OBJREF:
|
||||
|
||||
```text
|
||||
analysis\proxy\nmxservice-objref-context2.txt
|
||||
```
|
||||
|
||||
The OBJREF exposes a stable OXID plus per-activation OID/IPID values and
|
||||
binding towers. This proves a managed implementation can obtain the object
|
||||
identity without loading `NmxSvcps.dll`; the remaining step is to implement the
|
||||
ORPC `QueryInterface` and method call flow manually.
|
||||
|
||||
The .NET 10 client scaffold now has the first managed protocol primitives:
|
||||
|
||||
```text
|
||||
src\MxNativeClient\DceRpcPdu.cs
|
||||
src\MxNativeClient\DceRpcTcpClient.cs
|
||||
src\MxNativeClient\OrpcStructures.cs
|
||||
src\MxNativeClient\RemUnknownMessages.cs
|
||||
src\MxNativeClient\ObjectExporterMessages.cs
|
||||
src\MxNativeClient\ObjectExporterClient.cs
|
||||
src\MxNativeClient.Tests\MxNativeClient.Tests.csproj
|
||||
```
|
||||
|
||||
Those tests parse and re-encode real bind/alter-context style traffic from
|
||||
capture `046` and parse a captured request PDU. The capture parser now records
|
||||
presentation-context UUIDs in `dcerpc-stream-pdus.tsv`, which makes it easier to
|
||||
separate useful DCE/RPC structure from traffic that is unrelated to the NMX
|
||||
service body.
|
||||
|
||||
The visible DCE/RPC stream does not begin request stubs with `ORPCTHIS`, so it
|
||||
is not the direct `INmxService2` ORPC method channel. The managed ORPC work is
|
||||
therefore being built from the DCOM specification plus the OBJREF returned by
|
||||
local COM activation. `IRemUnknown::RemQueryInterface` composition is now
|
||||
represented in code; a live RPC binding and response validation remain pending.
|
||||
|
||||
The first live managed RPC probe binds to `IObjectExporter` on RPCSS and sends
|
||||
`ResolveOxid` for the OBJREF OXID. The request is accepted, but the unauthenticated
|
||||
call returns `0x00000005` (`ERROR_ACCESS_DENIED`). That matches the Python and
|
||||
Impacket reference behavior, so the next blocker is RPC authentication rather
|
||||
than the OXID NDR layout.
|
||||
|
||||
The DCOM activation and scalar call path has now been proven with an Impacket
|
||||
reference probe:
|
||||
|
||||
```text
|
||||
analysis\scripts\probe_dcom_inmxservice2.py
|
||||
analysis\proxy\dcom-inmxservice2-getpartner-probe.txt
|
||||
```
|
||||
|
||||
That probe uses packet privacy, activates CLSID
|
||||
`{AE24BD51-2E80-44CC-905B-E5446C942BEB}`, requests
|
||||
`INmxService2` `{2630A513-A974-4B1A-8025-457A9A7C56B8}`, binds to the returned
|
||||
OXID endpoint, and calls `GetPartnerVersion` opnum `11`. The service returns
|
||||
`partner_version=6` and `ErrorCode=0x00000000`.
|
||||
|
||||
This changes the remaining work from "prove DCOM can reach NmxSvc without the
|
||||
x86 proxy" to "port packet-private DCOM authentication and the decoded NDR
|
||||
method stubs into the .NET 10 client." The public method schema, target CLSID,
|
||||
service endpoint, object identity flow, and at least one working service method
|
||||
are now verified.
|
||||
|
||||
The .NET 10 managed client now reproduces the critical scalar path without the
|
||||
AVEVA x86 proxy and without using SSPI `MakeSignature`:
|
||||
|
||||
```text
|
||||
analysis\proxy\managed-remqi-and-getpartner-probe.txt
|
||||
src\MxNativeClient\ManagedNtlmClientContext.cs
|
||||
src\MxNativeClient\DceRpcTcpClient.cs
|
||||
src\MxNativeClient\NmxService2Messages.cs
|
||||
```
|
||||
|
||||
The managed probe obtains an `IUnknown` OBJREF, resolves the OXID with a
|
||||
managed NTLMv2 packet-integrity DCE/RPC call, calls
|
||||
`IRemUnknown::RemQueryInterface` for `INmxService2`, and then invokes
|
||||
`INmxService2::GetPartnerVersion`. The live result is
|
||||
`managed_getpartner_version=6` and `managed_getpartner_hresult=0x00000000`.
|
||||
|
||||
The remaining client-side proxy work is now concentrated on the non-scalar COM
|
||||
methods: marshaling a managed callback object for `RegisterEngine2`, encoding
|
||||
the correlated `byte[size]` parameter for `TransferData`, and implementing the
|
||||
callback endpoint that receives `DataReceived` and `StatusReceived`.
|
||||
|
||||
`TransferData` has since been encoded and live-probed:
|
||||
|
||||
```text
|
||||
analysis\proxy\managed-transferdata-control-probe.txt
|
||||
```
|
||||
|
||||
The service returned `0x80041101`, which is an application-level HRESULT after
|
||||
the ORPC call reached `NmxSvc.exe`; it was not a DCE/RPC/NDR failure. That
|
||||
confirms the `byte[size]` request shape.
|
||||
|
||||
The x64 callback marshal probe is:
|
||||
|
||||
```text
|
||||
analysis\proxy\callback-marshal-probe.txt
|
||||
```
|
||||
|
||||
It fails with `0x80040154 REGDB_E_CLASSNOTREG` when trying to marshal
|
||||
`INmxSvcCallback`. This is the same architecture problem in reverse: the
|
||||
client-side x64 process has no registered `NmxSvcps.dll` proxy/stub for the
|
||||
callback IID. A full managed client therefore needs to export the callback
|
||||
object itself over DCE/RPC/ORPC.
|
||||
|
||||
Therefore the viable managed path is to implement the observed local contracts
|
||||
directly:
|
||||
|
||||
1. Recreate enough of the NMX service/session behavior represented by
|
||||
`INmxService`, `INmxSvcCallback`, and `INmx4`.
|
||||
2. Encode/decode the NMX adapter message bodies identified by the Frida trace.
|
||||
3. Replace the 32-bit `NmxSvcps.dll` MIDL proxy with a managed NDR/DCOM proxy
|
||||
for `NmxSvc.exe`.
|
||||
4. Use the Galaxy repository for tag/type/security metadata rather than
|
||||
depending on the x86 MXAccess wrapper.
|
||||
|
||||
The next hard blocker is no longer the basic `int32` value location. It is the
|
||||
managed replacement for the MIDL proxy/stub plus synthesis of add-item,
|
||||
advise/unadvise, remove-item, and status/error request bodies.
|
||||
|
||||
See also:
|
||||
|
||||
```text
|
||||
docs\DotNet10-Native-Library-Plan.md
|
||||
analysis\proxy\nmxsvcps-proxy-layout.tsv
|
||||
analysis\proxy\nmxsvcps-procedures.tsv
|
||||
```
|
||||
|
||||
## Decoded proxy/stub procedure table
|
||||
|
||||
The proxy/stub MIDL procedure bytecode is extracted by:
|
||||
|
||||
```text
|
||||
analysis\scripts\extract_nmxsvcps_proc_formats.py
|
||||
```
|
||||
|
||||
The output is:
|
||||
|
||||
```text
|
||||
analysis\proxy\nmxsvcps-procedures.tsv
|
||||
analysis\proxy\type-format-snippets\
|
||||
```
|
||||
|
||||
The core service opnums recovered from `NmxSvcps.dll` are:
|
||||
|
||||
| Interface | Method | Opnum | Parameter shape |
|
||||
| --- | --- | ---: | --- |
|
||||
| `INmxService2` | `RegisterEngine` | 3 | `int`, `BSTR`, `INmxSvcCallback*`, `HRESULT` |
|
||||
| `INmxService2` | `UnRegisterEngine` | 4 | `int`, `HRESULT` |
|
||||
| `INmxService2` | `Connect` | 5 | `int`, `int`, `int`, `int`, `HRESULT` |
|
||||
| `INmxService2` | `TransferData` | 6 | `int`, `int`, `int`, `int size`, `byte[size]`, `HRESULT` |
|
||||
| `INmxService2` | `AddSubscriberEngine` | 7 | `int`, `int`, `int`, `int`, `HRESULT` |
|
||||
| `INmxService2` | `RemoveSubscriberEngine` | 8 | `int`, `int`, `int`, `int`, `HRESULT` |
|
||||
| `INmxService2` | `SetHeartbeatSendInterval` | 9 | `int`, `int`, `HRESULT` |
|
||||
| `INmxService2` | `RegisterEngine2` | 10 | `int`, `BSTR`, `int version`, `INmxSvcCallback*`, `HRESULT` |
|
||||
| `INmxService2` | `GetPartnerVersion` | 11 | `int`, `int`, `int`, `out int`, `HRESULT` |
|
||||
| `INmxSvcCallback` | `DataReceived` | 3 | `int size`, `sbyte[size]`, `HRESULT` |
|
||||
| `INmxSvcCallback` | `StatusReceived` | 4 | `int size`, `sbyte[size]`, `HRESULT` |
|
||||
|
||||
Important NDR type-format offsets:
|
||||
|
||||
| Offset | Usage |
|
||||
| --- | --- |
|
||||
| `0x0006` | callback byte array correlated to `dwBufferSize` |
|
||||
| `0x002c` | `BSTR_UserMarshal` string |
|
||||
| `0x0036` | `INmxSvcCallback` interface pointer |
|
||||
| `0x004c` | `TransferData` byte array correlated to `lSize` |
|
||||
| `0x005c` | `INmxNotify` interface pointer |
|
||||
|
||||
This confirms that the missing x64/full-managed layer is not the public method
|
||||
schema. The remaining native dependency is the DCOM/ORPC transport and the NDR
|
||||
interpreter behavior normally provided by `NmxSvcps.dll`.
|
||||
|
||||
## RegisterEngine2 marshaling findings
|
||||
|
||||
The direct x86 COM harness is:
|
||||
|
||||
```text
|
||||
src\NmxComHarness\NmxComHarness.csproj
|
||||
src\NmxComHarness\Program.cs
|
||||
```
|
||||
|
||||
It bypasses `ArchestrA.MXAccess.dll` and invokes
|
||||
`NmxSvc.NmxService.RegisterEngine2` directly through the installed 32-bit
|
||||
`NmxSvcps.dll` proxy. The focused Frida hook is:
|
||||
|
||||
```text
|
||||
analysis\frida\nmx-com-proxy-trace.js
|
||||
```
|
||||
|
||||
Captured runs:
|
||||
|
||||
```text
|
||||
captures\052-frida-direct-nmx-registerengine2-marshals-retry
|
||||
captures\053-frida-direct-nmx-registerengine2-null-stub
|
||||
captures\054-frida-direct-nmx-registerengine2-callback-stub
|
||||
analysis\proxy\x86-callback-objref-probe.txt
|
||||
analysis\proxy\x86-registerengine2-null-callback-probe.txt
|
||||
analysis\proxy\managed-registerengine2-null-callback-probe.txt
|
||||
```
|
||||
|
||||
`BSTR_UserMarshal` writes the string as:
|
||||
|
||||
```text
|
||||
char_count:uint32
|
||||
byte_length:uint32
|
||||
char_count:uint32
|
||||
utf16_payload_without_null
|
||||
```
|
||||
|
||||
For `NmxComProxyWire5`, the exact captured bytes were:
|
||||
|
||||
```text
|
||||
10 00 00 00 20 00 00 00 10 00 00 00
|
||||
4e 00 6d 00 78 00 43 00 6f 00 6d 00 50 00 72 00
|
||||
6f 00 78 00 79 00 57 00 69 00 72 00 65 00 35 00
|
||||
```
|
||||
|
||||
The generated proxy also writes a 4-byte user-marshal marker before that BSTR
|
||||
payload:
|
||||
|
||||
```text
|
||||
55 73 65 72
|
||||
```
|
||||
|
||||
Interpreted little-endian, that value is `0x72657355` (`"User"`).
|
||||
|
||||
The managed encoder in `src\MxNativeClient\NmxService2Messages.cs` now
|
||||
reproduces the null-callback `RegisterEngine2` request:
|
||||
|
||||
```text
|
||||
ORPCTHIS
|
||||
localEngineId:int32
|
||||
0x72657355:uint32
|
||||
BSTR_UserMarshal(engineName)
|
||||
padding to 4-byte boundary
|
||||
version:int32
|
||||
callback:null-interface-pointer:uint32 = 0
|
||||
```
|
||||
|
||||
The live .NET 10 x64 probe reaches `NmxSvc.exe` and returns a non-failing COM
|
||||
success code:
|
||||
|
||||
```text
|
||||
managed_register2_null_hresult=0x00000001
|
||||
managed_unregister_after_register_hresult=0x00000001
|
||||
```
|
||||
|
||||
This confirms that the managed DCOM/NDR path can perform the service
|
||||
registration lifecycle without the x86 proxy when no callback endpoint is
|
||||
required.
|
||||
|
||||
For a non-null callback, the x86 proxy wraps the callback OBJREF as:
|
||||
|
||||
```text
|
||||
0x00020000:uint32
|
||||
objref_size:uint32
|
||||
objref_size:uint32
|
||||
objref_bytes
|
||||
```
|
||||
|
||||
The same capture showed `objref_size=0x44` for a compact same-machine standard
|
||||
OBJREF. A separately marshaled x86 callback stream produced a 366-byte standard
|
||||
OBJREF with dual-string bindings. The managed callback exporter can therefore
|
||||
use the same MInterfacePointer wrapper around an OBJREF that advertises the
|
||||
managed callback endpoint.
|
||||
|
||||
## Callback OBJREF experiments
|
||||
|
||||
Two managed callback OBJREF strategies have now been tested.
|
||||
|
||||
### Synthetic managed TCP OBJREF
|
||||
|
||||
`ManagedCallbackExporter` can build a standard OBJREF that advertises a managed
|
||||
TCP listener:
|
||||
|
||||
```text
|
||||
src\MxNativeClient\ManagedCallbackExporter.cs
|
||||
analysis\proxy\managed-registerengine2-callback-probe.txt
|
||||
analysis\proxy\managed-registerengine2-callback-loopback-probe.txt
|
||||
analysis\proxy\managed-registerengine2-callback-fixed-port-probe.txt
|
||||
analysis\proxy\managed-callback-fixed-port-tcp-poll.txt
|
||||
analysis\proxy\managed-callback-nmxsvc-tcp-poll.txt
|
||||
```
|
||||
|
||||
Without security bindings the service rejects the callback OBJREF with
|
||||
`0x8001011D`. Adding the default security binding sequence seen in x86
|
||||
`CoMarshalInterface` changes the failure to `0x800706BA` (`RPC server
|
||||
unavailable`), but the managed listener sees no inbound connection. TCP polling
|
||||
also shows no SYN to the advertised port.
|
||||
|
||||
Inference: for standard OBJREFs, COM is not treating the embedded string binding
|
||||
as a direct object endpoint. It is resolving the OXID through the local COM/OXID
|
||||
resolver machinery. A purely synthetic OXID that is not registered with RPCSS is
|
||||
not enough.
|
||||
|
||||
### COM-registered IUnknown OBJREF patched to callback IID
|
||||
|
||||
The probe can also ask the local x64 COM runtime to marshal a managed
|
||||
`IUnknown` OBJREF, then replace only the OBJREF IID with `INmxSvcCallback`:
|
||||
|
||||
```text
|
||||
analysis\proxy\managed-registerengine2-callback-com-iunknown-objref-probe.txt
|
||||
analysis\proxy\managed-registerengine2-callback-com-iunknown-self-transfer-probe.txt
|
||||
```
|
||||
|
||||
That OBJREF is backed by a real RPCSS-registered OXID/OID/IPID. With this form,
|
||||
`NmxSvc.exe` accepts a non-null callback pointer:
|
||||
|
||||
```text
|
||||
managed_register2_callback_hresult=0x00000000
|
||||
managed_unregister_after_callback_register_hresult=0x00000000
|
||||
```
|
||||
|
||||
A self-directed `TransferData` probe also returns `0x00000000`:
|
||||
|
||||
```text
|
||||
managed_callback_self_transfer_hresult=0x00000000
|
||||
```
|
||||
|
||||
No managed callback event was delivered during that self-transfer probe. This
|
||||
means the COM-registered patched OBJREF is currently a registration proof, not
|
||||
the final callback solution. The final managed implementation still needs an
|
||||
endpoint whose OXID/IPID can be resolved by COM and whose request dispatch is
|
||||
handled by managed code rather than the missing x64 `NmxSvcps.dll` proxy/stub.
|
||||
|
||||
### x64 type-library marshaling for INmxSvcCallback
|
||||
|
||||
The x64 callback marshal failure can be removed without AVEVA's 32-bit
|
||||
`NmxSvcps.dll` by registering type-library metadata for the callback interface
|
||||
and using the Windows standard automation proxy/stub:
|
||||
|
||||
```text
|
||||
analysis\scripts\register_x64_callback_typelib.ps1
|
||||
analysis\proxy\typelib\NmxComHarness.tlb
|
||||
analysis\proxy\callback-marshal-after-typelib-probe.txt
|
||||
```
|
||||
|
||||
The script exports `src\NmxComHarness\bin\Release\net481\NmxComHarness.exe` to
|
||||
a TLB, registers it with `LoadTypeLibEx(REGKIND_REGISTER)`, and sets:
|
||||
|
||||
```text
|
||||
HKLM\SOFTWARE\Classes\Interface\{B49F92F7-C748-4169-8ECA-A0670B012746}
|
||||
ProxyStubClsid32 = {00020424-0000-0000-C000-000000000046}
|
||||
TypeLib = {4DBF23F3-069E-3D29-B67F-4C7850F588B3}, Version 1.0
|
||||
NumMethods = 5
|
||||
```
|
||||
|
||||
After that registration, the x64 managed process can call
|
||||
`CoMarshalInterface` for `INmxSvcCallback` directly. The probe now emits a
|
||||
standard 366-byte OBJREF for the callback IID instead of
|
||||
`REGDB_E_CLASSNOTREG`.
|
||||
|
||||
Using the real marshaled callback OBJREF, `NmxSvc.exe` accepts non-null
|
||||
`RegisterEngine2`, `Connect`, `AddSubscriberEngine`, `TransferData`, and
|
||||
`UnRegisterEngine` calls:
|
||||
|
||||
```text
|
||||
analysis\proxy\managed-registerengine2-callback-com-real-probe.txt
|
||||
analysis\proxy\x86-registerengine2-self-transfer-callback-probe.txt
|
||||
```
|
||||
|
||||
The same synthetic self-transfer route does not produce a callback in the x86
|
||||
harness either. Therefore the absence of a callback event in the current probe
|
||||
is caused by the synthetic NMX message/session body, not by inability to marshal
|
||||
the callback interface after the type-library registration.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Transport correlation
|
||||
|
||||
This note records the current boundary between the native adapter body format
|
||||
and localhost transport.
|
||||
|
||||
## Combined captures
|
||||
|
||||
The combined runner starts Npcap loopback capture, then launches the harness
|
||||
under Frida:
|
||||
|
||||
```text
|
||||
analysis\scripts\run_frida_loopback_capture.ps1
|
||||
```
|
||||
|
||||
Helper scripts:
|
||||
|
||||
```text
|
||||
analysis\scripts\map_frida_to_tcp.py
|
||||
analysis\scripts\parse_dcerpc_streams.py
|
||||
analysis\scripts\decode_mixed_local_stream.py
|
||||
```
|
||||
|
||||
## Capture 043
|
||||
|
||||
```text
|
||||
captures\043-frida-loopback-write-test-int-115
|
||||
```
|
||||
|
||||
This writes `TestChildObject.TestInt = 115`. It proved exact Frida adapter
|
||||
bodies are not copied verbatim to TCP, but the scalar value `115` was ambiguous
|
||||
because it also matched DCE/RPC call IDs in the same window.
|
||||
|
||||
## Capture 044
|
||||
|
||||
```text
|
||||
captures\044-frida-loopback-write-test-int-123456789
|
||||
```
|
||||
|
||||
This writes a distinctive value:
|
||||
|
||||
```text
|
||||
TestChildObject.TestInt = 123456789
|
||||
```
|
||||
|
||||
Results:
|
||||
|
||||
| Needle | Result |
|
||||
| --- | --- |
|
||||
| raw little-endian `123456789` | not found anywhere in the full pcap payload scan |
|
||||
| exact 40-byte Frida `PutRequest` body | not found in reassembled TCP streams |
|
||||
| exact 86-byte Frida `TransferData` body | not found in reassembled TCP streams |
|
||||
| exact 88-byte Frida callback body | not found in reassembled TCP streams |
|
||||
| mixed `127.0.0.1:57415 <-> 57433` stream | parsed, raw value not found |
|
||||
| DCE/RPC `::1:49704` streams | parsed 452 PDUs, raw value not found in request/response stubs |
|
||||
|
||||
Generated files:
|
||||
|
||||
```text
|
||||
captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv
|
||||
captures\044-frida-loopback-write-test-int-123456789\dcerpc-stream-pdus.tsv
|
||||
captures\044-frida-loopback-write-test-int-123456789\mixed-stream-57415-to-57433.tsv
|
||||
captures\044-frida-loopback-write-test-int-123456789\mixed-stream-57433-to-57415.tsv
|
||||
```
|
||||
|
||||
## Implication
|
||||
|
||||
The `CNmxAdapter::PutRequest` and `CNmxAdapter::TransferData` buffers are an
|
||||
internal adapter representation, not the TCP wire format. The wire transport
|
||||
does not expose the write value as plain little-endian scalar bytes for this
|
||||
distinctive-value capture.
|
||||
|
||||
The next reverse-engineering step is to decode the structural bridge between
|
||||
adapter bodies and transport messages:
|
||||
|
||||
1. Correlate Frida call timestamps to DCE/RPC call IDs and mixed-stream record
|
||||
windows.
|
||||
2. Decode DCE/RPC NDR stubs for the observed context/opnum pairs.
|
||||
3. Hook deeper in `NmxSvc.exe` around `CNmxControler::TransferData` and
|
||||
`CNmxService::TransferData` so both sides of the adapter-to-service boundary
|
||||
can be compared before TCP serialization.
|
||||
Reference in New Issue
Block a user