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,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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user