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>
314 lines
14 KiB
Markdown
314 lines
14 KiB
Markdown
# 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:
|
|
|
|
- [docs/MXAccess-Reverse-Engineering.md](docs/MXAccess-Reverse-Engineering.md)
|
|
- [docs/MXAccess-Public-API.md](docs/MXAccess-Public-API.md)
|
|
- [docs/Managed-LMX-NMX-Capture-Plan.md](docs/Managed-LMX-NMX-Capture-Plan.md)
|
|
- [docs/Capture-Run-2026-04-25.md](docs/Capture-Run-2026-04-25.md)
|
|
- [docs/Loopback-Protocol-Findings.md](docs/Loopback-Protocol-Findings.md)
|
|
- [docs/NMX-COM-Contracts.md](docs/NMX-COM-Contracts.md)
|
|
- [docs/DotNet10-Native-Library-Plan.md](docs/DotNet10-Native-Library-Plan.md)
|
|
- [docs/Ghidra-Headless-Analysis.md](docs/Ghidra-Headless-Analysis.md)
|
|
- [docs/Transport-Correlation.md](docs/Transport-Correlation.md)
|
|
|
|
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.
|