Joseph Doherty 321b7963a4 [M5] mxaccess-asb: F25 step 6 — Connect/AuthenticateMe handshake
Critical-path piece that turns a fresh TCP stream into an
authenticated session. With this slice landed, an `AsbClient` can
now do `send_preamble().await? -> connect().await? -> register_items()`
end-to-end against a peer.

Operations API additions:
* `build_connect_request_body(connection_id, public_key)` — first op
  on a fresh session. **Unsigned** (no ConnectionValidator header)
  because the authenticator hasn't received the service key yet.
  Wire shape: `<ConnectRequest xmlns="…messages/20111111">
    <ConnectionId>{guid-text}</ConnectionId>
    <ConsumerPublicKey><Data>{pubkey-bytes}</Data></ConsumerPublicKey>
  </ConnectRequest>` per `AsbContracts.cs:78-86`.
* `build_authenticate_me_request_body(data, iv)` — second op,
  **one-way + signed with `forceHmac=true`** per `MxAsbDataClient.cs
  :106-111`. Carries the encrypted `local_pub || remote_pub` blob
  produced by F23's `create_authentication_data()`.
* `ConnectResponse { service_public_key, service_authentication_data,
  connection_lifetime }` + `AuthenticationDataBytes { data, iv }`.
* `decode_connect_response(body, dict)` — extracts ServicePublicKey
  (required), optional ServiceAuthenticationData, optional
  ConnectionLifetime. The lifetime's `:V2` suffix is what F23
  inspects to toggle Apollo (raw AES) vs Baktun (deflate-then-AES)
  encryption.

Client API addition:
* `AsbClient::connect()` — orchestrates the full handshake:
  1. Build + send ConnectRequest (unsigned) carrying our DH public
     key + connection-id GUID.
  2. Decode ConnectResponse.
  3. `authenticator.accept_connect_response(...)` — feeds the
     service public key + lifetime into F23 so it derives the
     shared secret and picks Apollo/Baktun.
  4. `authenticator.create_authentication_data()` — encrypts
     `local_pub || remote_pub` under the derived AES key.
  5. Send AuthenticateMeRequest (one-way, signed with HMAC-SHA1
     forced).
  Returns the `ConnectResponse` so callers can inspect the
  negotiated connection lifetime.

6 new tests:
* ConnectRequest carries hyphenated GUID + raw public-key bytes.
* AuthenticateMe carries Data + IV bytes in order.
* ConnectResponse round-trip with all optional fields populated.
* ConnectResponse round-trip without optional fields.
* ConnectResponse decoder surfaces MissingField when
  ServicePublicKey is absent.
* End-to-end client::connect handshake via `tokio::io::duplex`
  peer that synthesises a ConnectResponse using bob's public key
  (so DH shared-secret derivation actually works) and drains the
  AuthenticateMe one-way SizedEnvelope.

Wire-byte caveat documented inline: WCF XML serialization may add
`xsi:type` attributes / distinct namespaces around <PublicKey> /
<AuthenticationData>; this builder ships the simplest plausible
shape and the live-probe iteration will reconcile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:47:35 -04:00

AVEVA MXAccess reverse-engineering notes

This folder documents the local AVEVA/Wonderware MXAccess stack installed on this machine, using the primary runtime DLL:

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

Primary documentation:

Current executable capture harness:

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

Managed codec work-in-progress:

src\MxNativeCodec\MxNativeCodec.csproj

.NET 10 x64 NMX client/probe scaffold:

src\MxNativeClient\MxNativeClient.csproj src\MxNativeClient.Probe\MxNativeClient.Probe.csproj src\MxNativeClient.Tests\MxNativeClient.Tests.csproj

This .NET 10 library currently implements a template-based encoder/decoder for the observed CNmxAdapter::PutRequest write bodies. It preserves unknown tag/session fields from a captured body and replaces the typed value slot plus write index. The companion console test project round-trips the real Frida bytes for bool, int, float, double, string, datetime, and the observed int/bool/float/double/string array bodies. It also includes the first observed INmxService2.TransferData envelope codec (46-byte header + PutRequest body). The client scaffold now includes managed OBJREF parsing, NMX procedure metadata, ORPC structures, IRemUnknown::RemQueryInterface message composition, and DCE/RPC PDU primitives tested against captured bytes. It also has a managed unauthenticated IObjectExporter::ResolveOxid probe that reaches RPCSS and currently confirms the expected ERROR_ACCESS_DENIED auth blocker. A reference Impacket probe now proves the packet-private DCOM path end to end: it activates NmxSvc.NmxService, receives an INmxService2 IPID, and calls GetPartnerVersion successfully from a 64-bit process without loading the AVEVA x86 proxy. The managed scaffold now goes further: it implements managed NTLMv2 packet-integrity signing, resolves the OXID, performs IRemUnknown::RemQueryInterface, and calls INmxService2.GetPartnerVersion successfully from .NET 10 x64. The first service-specific scalar codec is in src\MxNativeClient\NmxService2Messages.cs.

Generated static-analysis artifacts are under analysis/:

  • analysis/decompiled-mxaccess/ - decompiled C# for ArchestrA.MXAccess.dll.
  • analysis/interop/ - imported interop assemblies generated from native type libraries.
  • analysis/decompiled-interop/ - decompiled type-library imports for Lmx.dll, LmxProxy.dll, NmxAdptr.dll, NmxSvc.exe, and MXAccess32.tlb.

The main finding is that ArchestrA.MXAccess.dll is not the implementation of the LMX/NMX protocol. It is a .NET COM interop assembly imported from LMXPROXYLib. The real runtime path is a 32-bit COM/native stack:

ArchestrA.MXAccess.dll -> LmxProxy.dll -> Lmx.dll / NmxAdptr.dll -> NmxSvc.exe

That explains the current net48/x86 constraint and shapes the possible paths to a modern .NET or Rust interface.

Current loopback captures show that the native path uses DCE/RPC on ::1:49704 plus at least one separate compact localhost binary stream around write/write-complete activity. See docs/Loopback-Protocol-Findings.md for the captured interface UUIDs, opnum shape, and write-window correlation.

Latest controlled captures:

captures\021-loopback-write-test-int-sequence-103-105 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\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 captures\045-service-boundary-write-test-int-123456790 captures\046-service-boundary-write-test-int-123456791

The loopback int sequence writes TestChildObject.TestInt to 103, 104, and 105 in one advised session. The local stream framing is stable, but the requested int values are not isolated as plain int32 payloads in the decoded write-window pcap records. The Frida traces then find the values at the native boundary: CLMXProxyServer receives the COM VARIANT, CNmxAdapter::PutRequest carries the typed body, and CNmxAdapter::TransferData wraps that body.

The current scalar write matrix is saved at:

analysis\frida\write-body-matrix.tsv

The current array write matrix is saved at:

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

The current write-mode matrix is saved at:

analysis\frida\write-mode-matrix.tsv

The decoded NmxSvcps.dll MIDL procedure table is saved at:

analysis\proxy\nmxsvcps-procedures.tsv

The .NET 10 x64 remote-style IUnknown OBJREF probe output is saved at:

analysis\proxy\nmxservice-objref-context2.txt

The reference DCOM activation and INmxService2.GetPartnerVersion probe output is saved at:

analysis\proxy\dcom-inmxservice2-getpartner-probe.txt

The managed OXID, RemQueryInterface, and INmxService2.GetPartnerVersion probe output is saved at:

analysis\proxy\managed-remqi-and-getpartner-probe.txt

The first Frida-to-TCP correlation output is:

captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv

The non-ambiguous correlation capture is:

captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv

The matrices cover bool, int, float, double, string, datetime, and the matching array forms, plus timestamped Write2 and secured/verified public write behavior. Capture 046 proves the same 86-byte write body reaches CNmxService.TransferData inside NmxSvc.exe, but the .NET 10 x64 probe proves ordinary COM interop fails on first method call because the only installed MIDL proxy/stub is 32-bit (NmxSvcps.dll). The latest managed probe proves the service can still be reached through managed packet-integrity ORPC. The next blocker for the full managed x64 library is callback/interface-pointer marshaling for RegisterEngine2, the TransferData byte-array method, then status/error, add-item, advise, remove-item, and general tag metadata synthesis.

The TransferData byte-array method is now encoded and live-probed. The service returns an NMX application HRESULT for the unregistered control probe, which means the ORPC/NDR call shape is accepted. The callback marshal probe confirms Windows cannot export INmxSvcCallback from x64 on this machine because the proxy/stub is not registered for 64-bit; a managed callback object exporter is required. The callback method codecs for DataReceived and StatusReceived are now in src\MxNativeClient\NmxSvcCallbackMessages.cs; the remaining callback work is the managed object endpoint/OBJREF exporter.

RegisterEngine2 marshaling is now decoded far enough for a managed .NET 10 x64 null-callback registration lifecycle. The x86 direct harness and Frida captures show the required 0x72657355 BSTR user-marshal marker, the BSTR wire format, and the callback MInterfacePointer wrapper. The live managed probe is:

analysis\proxy\managed-registerengine2-null-callback-probe.txt

It returns non-failing COM success codes for both register and unregister. The remaining RegisterEngine2 work is generating a managed callback OBJREF and serving INmxSvcCallback/IRemUnknown from a managed DCE/RPC endpoint.

A synthetic managed callback OBJREF currently fails before NmxSvc.exe connects to the advertised listener, which points to missing RPCSS/OXID registration. A COM-runtime-registered x64 IUnknown OBJREF patched to the callback IID is accepted for non-null RegisterEngine2:

analysis\proxy\managed-registerengine2-callback-com-iunknown-objref-probe.txt

That proves the non-null callback argument can be accepted in .NET 10 x64, but it does not yet prove callback method dispatch without the missing x64 NmxSvcps.dll stub.

The x64 callback marshal issue can be solved with a registered type library and the Windows standard automation proxy/stub:

analysis\scripts\register_x64_callback_typelib.ps1

After that setup, CoMarshalInterface(INmxSvcCallback) succeeds from .NET 10 x64 and NmxSvc.exe accepts the real marshaled callback OBJREF:

analysis\proxy\managed-registerengine2-callback-com-real-probe.txt

The same synthetic self-transfer path produces no callback in the x86 direct harness, so the next blocker is the NMX add/advise/session message bodies that actually trigger data/status callbacks.

That next layer is now being decoded from focused subscription capture:

captures\058-frida-subscribe-testint

The capture shows AdviseSupervisory sending a 39-byte CNmxAdapter.PutRequest body wrapped in an 85-byte TransferData body. The corresponding unadvise body uses the same item-control shape with command byte 0x21 instead of 0x1f, wrapped in an 83-byte TransferData body whose envelope kind is 3. Incoming ProcessDataReceived bodies use a related length-prefixed service-to-adapter shape and carry status/data commands including 0x32 and 0x33. The generic frame classifier is now checked into:

src\MxNativeCodec\NmxObservedFrame.cs

The callback codec now accepts both observed service-to-adapter forms: the 4-byte length-prefixed buffers and the direct 46-byte-header buffers seen in the Write2 callback capture. It decodes 0x32 subscription status records and 0x33 data update records into typed status/detail, quality, FILETIME timestamps, wire kind, correlation IDs, and scalar values for the observed bool, int, float, double, string, scalar arrays, datetime arrays, and multi-record date/status updates:

src\MxNativeCodec\NmxSubscriptionMessage.cs

The generated envelope encoder now reproduces the captured advise and unadvise TransferData bodies exactly:

src\MxNativeCodec\NmxTransferEnvelope.cs

Follow-up captures for TestBool, TestString, and the array attributes show that the item-control body contains an MxHandle projection. The parser/encoder is:

src\MxNativeCodec\NmxItemControlMessage.cs

Capture 061 hooks Lmx.dll around IMxReference.GetMxHandle and AccessManager.FixUpMxHandle. It proves the resolved TestInt handle is a 20-byte structure:

01 00 01 00 02 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00

The LMX typelib names this as MxAutomationObjectHandle followed by MxAttributeHandle:

Offset Field Value for TestChildObject.TestInt
0 galaxy 1
2 platform 1
4 engine 2
6 object 5
8 object signature 0xd736
10 primitive id 2
12 attribute id 155
14 property/category id 10
16 attribute signature 0xda3e
18 attribute index 0 (-1 for arrays)

The NMX item-control request projects bytes 6 through 19 of this handle, then appends tail 0x00000003. The signatures are reproducible in managed code: CRC-16/IBM over the lowercase UTF-16LE object or attribute name (testchildobject -> 0xd736, testint -> 0xda3e). The managed handle type and synthesizer are:

src\MxNativeCodec\MxReferenceHandle.cs

Normal write bodies are also generated from the same handle projection plus the GR data type. The generator reproduces the observed normal bool, int, float, double, string, datetime, and array write bodies, plus the captured timestamped int Write2 body. The GR resolver and generator also cover the captured secured/verified test tags (security_classification 2 and 3); their public route is still the normal write body shape:

src\MxNativeCodec\NmxWriteMessage.cs

The Galaxy Repository resolver is now live-tested against the local ZB database and reproduces that same handle for TestChildObject.TestInt without calling the x86 LMX resolver:

src\MxNativeClient\GalaxyRepositoryTagResolver.cs

The reusable .NET 10 x64 managed service client is now checked into:

src\MxNativeClient\ManagedNmxService2Client.cs

A first consumer-facing session facade now wraps the low-level pieces:

src\MxNativeClient\MxNativeSession.cs

It opens/registers a managed callback engine, resolves Galaxy Repository tag metadata, exposes WriteAsync, timestamped Write2Async, metadata browse, transient subscription ReadAsync, SubscribeAsync/Unsubscribe, and raises typed callback records decoded by NmxSubscriptionMessage.

The facade usage notes and probe commands are documented in:

docs\MxNativeSession-API.md

The remaining parity gaps against the full ILMXProxyServer5 MXAccess surface are tracked method-by-method in:

docs\DotNet10-Native-Library-Plan.md

It moves the proven managed DCOM path out of the probe and exposes activation, OXID resolution, IRemUnknown::RemQueryInterface, RegisterEngine2, Connect, subscriber engine calls, TransferData, generated AdviseSupervisory and UnAdvise bodies, generated normal Write and timestamped Write2 bodies, GetPartnerVersion, and unregister without loading the AVEVA x86 proxy/stub. The probe now has a guarded managed subscribe path; SSPI auth faults at ResolveOxid with 0x00000721, so the valid end-to-end subscription probe remains the managed NTLM runtime-auth path. When callbacks arrive, the probe prints the typed subscription records using the same managed callback decoder.

S
Description
No description provided
Readme MIT 33 MiB
Languages
C# 73.8%
Rust 23.6%
Python 1.1%
JavaScript 0.4%
HTML 0.4%
Other 0.7%