fe2a6db786
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>
398 lines
13 KiB
Markdown
398 lines
13 KiB
Markdown
# 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
|
|
```
|
|
|