Files
mxaccess/docs/MXAccess-Reverse-Engineering.md
T
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
Layout:
- src/                    .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
                          MxAsbClient, probes, tests, harnesses. Executable spec.
- design/                 Architectural plan for the Rust port (M0–M6), error
                          model, protocol invariants, risks (R1–R16), adversarial
                          review log (review.md).
- rust/                   Rust workspace. M0 skeleton + M1 codec parity.
                          mxaccess-codec: 215 unit tests + 2 cross-implementation
                          parity tests (byte-identical against .NET reference).
                          Other crates are M0 stubs awaiting M2+.
- captures/               Frida + netsh + pcap evidence per CLAUDE.md
                          ("captures are evidence, not throwaway logs").
- analysis/               Decompiled C# (frida/proxy/decompiled-*),
                          Ghidra exports for native DLLs (`exports/` only —
                          working state at `projects/` and AVEVA's input
                          binaries at `input/` are gitignored).
- docs/                   Reverse-engineering reference docs.
- tools/                  Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
                          Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/      Rust CI: fmt + build + test + clippy on Windows.
- LICENSE                 MIT (Joseph Doherty, 2026).

Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly

Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)

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

13 KiB

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:

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:

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:

{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:

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:

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:

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:

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:

CLSID {AE24BD51-2E80-44CC-905B-E5446C942BEB}
  LocalServer32 = "C:\Program Files (x86)\ArchestrA\Framework\Bin\NmxSvc.exe"
  ProgID = NmxSvc.NmxService.1

Key INmxService2 methods:

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:

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:

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:

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:

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:

analysis/decompiled-mxaccess/
analysis/interop/
analysis/decompiled-interop/

Installed tools used:

dotnet tool install --global ilspycmd
python pefile
Windows SDK TlbImp.exe