Files
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

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.