c95824a65d
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:
- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass
Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.
Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
282 lines
12 KiB
Markdown
282 lines
12 KiB
Markdown
# Frida Hook Pass: aahClientManaged Integrated Read
|
||
|
||
Scenario:
|
||
|
||
```powershell
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Attach-AahClientManagedFridaCapture.ps1 -TagName OtOpcUaParityTest_001.Counter -LookbackMinutes 1440 -MaxRows 1
|
||
```
|
||
|
||
Artifacts:
|
||
|
||
- Script: `scripts\frida\aahclientmanaged-open-query.js`
|
||
- Runner: `scripts\Attach-AahClientManagedFridaCapture.ps1`
|
||
- Latest capture: `docs\reverse-engineering\frida-aahclientmanaged-attach-read-latest.ndjson`
|
||
|
||
## Result
|
||
|
||
The attach-based workflow is the reliable Frida path for this mixed CLR/native
|
||
assembly:
|
||
|
||
1. Start Windows PowerShell.
|
||
2. Load `current\aahClientManaged.dll`.
|
||
3. Sleep briefly before creating `HistorianAccess`.
|
||
4. Attach Frida to the already-loaded process.
|
||
5. Install hooks at candidate RVAs.
|
||
6. Continue the native integrated read.
|
||
|
||
Frida confirmed:
|
||
|
||
- target process architecture: `x64`
|
||
- loaded module: `aahClientManaged.dll`
|
||
- module base for latest run: `0x7ffd4a600000`
|
||
- module size: `17166336`
|
||
- path: `C:\Users\dohertj2\Desktop\histsdk\current\aahClientManaged.dll`
|
||
|
||
Hooks were installed for candidate method RVAs including:
|
||
|
||
- `CClientInfo.SerializeOpenConnectionInParams*`
|
||
- `CHistoryConnectionWCF.OpenConnection*`
|
||
- `HistorianClient.StartQuery`
|
||
- `HistorianClient.StartDataQuery`
|
||
- `ClientApp.StartDataQuery`
|
||
- `Query.StartDataQuery`
|
||
- `CRetrievalConnectionWCF.StartQuery2`
|
||
- `QueryColumnSelector.SelectNonSummaryColumns`
|
||
- `QueryColumnSelector.Save<SCrtMemFile>`
|
||
- `QueryColumnSelector.GetColumnSelectorFlags`
|
||
- `HistorianClient.GetNextRow<DataQueryResultRow>`
|
||
- `HistorianClient.GetNextRow<EventQueryResultRow>`
|
||
|
||
No hook entry/leave events fired during the successful read.
|
||
|
||
## Interpretation
|
||
|
||
The `methods` command RVAs are not reliable executable hook targets for the
|
||
successful path. They are CLR/mixed-mode method body locations or stubs, not the
|
||
actual runtime addresses invoked after CLR/JIT/mixed-mode dispatch. Frida can
|
||
see the module and can install some interceptors at `base + RVA`, but those
|
||
addresses are not reached by the wrapper scenario.
|
||
|
||
The successful read still proves the scenario:
|
||
|
||
- integrated auth opens
|
||
- `GetConnectionStatus` settles
|
||
- `HistoryQuery.StartQuery` succeeds
|
||
- `MoveNext` returns one row for `OtOpcUaParityTest_001.Counter`
|
||
|
||
An expanded pass based on decompiled call targets produced the same result:
|
||
hooks installed for MethodDef RVAs such as `HistorianClient.StartDataQuery`
|
||
(`0x4160C4`), `Query.StartDataQuery` (`0x41CACC`),
|
||
`QueryColumnSelector.SelectNonSummaryColumns` (`0x1EE34`), and
|
||
`HistorianClient.GetNextRow<DataQueryResultRow>` (`0x42F818`), but no
|
||
entry/leave callbacks fired. Treat MethodDef RVAs from the mixed-mode assembly
|
||
as insufficient for Frida interception without resolving the actual CLR/native
|
||
dispatch target.
|
||
|
||
Runtime method-pointer dumps from
|
||
`tools\AVEVA.Historian.NativeTraceHarness --dump-method-pointers ...` provide a
|
||
better target class, but not stable RVAs. `RuntimeHelpers.PrepareMethod` plus
|
||
`MethodHandle.GetFunctionPointer()` exposes process-local CLR/JIT entry
|
||
addresses for methods such as `<Module>.HistorianClient.StartDataQuery`,
|
||
`<Module>.CRetrievalConnectionWCF.StartQuery2`, and
|
||
`<Module>.HistorianClient.GetNextRow<class DataQueryResultRow>`. The current
|
||
artifacts mark all of these as `FunctionPointerInModule = false`, so they must
|
||
be discovered in the same process that will be hooked.
|
||
|
||
`scripts\Attach-NativeTraceHarnessRuntimePointerCapture.ps1` now automates that
|
||
same-process attempt. It starts `NativeTraceHarness`, writes a runtime pointer
|
||
snapshot immediately before `StartQuery`, pauses, generates a temporary Frida
|
||
script with the absolute addresses, and attaches to the still-paused process.
|
||
The latest local direct history run installed 37 absolute hooks from
|
||
`runtime-method-pointers-before-history-start-latest.json`; the read succeeded,
|
||
but no hook `enter`/`leave` callbacks fired. This rules out raw Frida
|
||
interception of `MethodHandle.GetFunctionPointer()` addresses as the next
|
||
primary capture route.
|
||
|
||
## Decompiled Query Call Facts
|
||
|
||
`ArchestrA.HistoryQuery.StartQuery`:
|
||
|
||
- Calls `BaseQuery.GetClient`.
|
||
- Calls `EndQuery` before starting a new query.
|
||
- Converts tag names to a native UTF-16 pointer vector.
|
||
- Builds a selected-column stream as:
|
||
- `ushort` value `1`
|
||
- 8-byte `QueryColumnSelector` payload
|
||
- Calls `HistorianClient.StartDataQuery` with retrieval mode, query format `0`,
|
||
summary type `0`, tag count, tag pointer vector, UTC FILETIME start/end,
|
||
resolution, deadbands, time zone `UTC`, version/interpolation/timestamp/
|
||
quality/value-selector/aggregation enum values, option/filter buffers,
|
||
selected-column byte count and pointer, max states, output query handle, and
|
||
`SError`.
|
||
|
||
`ArchestrA.EventQuery.StartQuery`:
|
||
|
||
- Calls `BaseQuery.GetClient`.
|
||
- Calls `EndQuery` before starting a new event query.
|
||
- Normalizes `EventCount = 0` to `100000` and enables a continue-query mode.
|
||
- Calls `HistorianClient.StartEventQuery` with UTC FILETIME start/end, event
|
||
count, skip count, event order as `ushort`, query type value `1`, filter
|
||
pointer, time zone `UTC`, output query handle, and `SError`.
|
||
|
||
## Confirmed Managed Contract Shape
|
||
|
||
Decompilation confirms these WCF byte-buffer contracts:
|
||
|
||
- `HistoryServiceContract.IHistoryServiceContract2.OpenConnection2`
|
||
- `HistoryServiceContract.IHistoryServiceContract2.ExchangeKey`
|
||
- `HistoryServiceContract.IHistoryServiceContract2.ValidateClientCredential`
|
||
- `RetrievalServiceContract.IRetrievalServiceContract2.StartQuery2`
|
||
- `RetrievalServiceContract.IRetrievalServiceContract2.GetNextQueryResultBuffer2`
|
||
- `RetrievalServiceContract.IRetrievalServiceContract2.EndQuery2`
|
||
|
||
`StartQuery2` signature:
|
||
|
||
```text
|
||
bool StartQuery2(
|
||
uint clientHandle,
|
||
ushort queryRequestType,
|
||
uint requestSize,
|
||
byte[] pRequestBuff,
|
||
out uint responseSize,
|
||
out byte[] pResponseBuff,
|
||
ref uint queryHandle,
|
||
out uint errSize,
|
||
out byte[] err)
|
||
```
|
||
|
||
`GetNextQueryResultBuffer2` signature:
|
||
|
||
```text
|
||
bool GetNextQueryResultBuffer2(
|
||
uint clientHandle,
|
||
uint queryHandle,
|
||
out uint resultSize,
|
||
out byte[] pResultBuff,
|
||
out uint errSize,
|
||
out byte[] err)
|
||
```
|
||
|
||
## Next Hook Direction
|
||
|
||
Use profiler/API-boundary interception rather than raw Frida function-pointer
|
||
interception:
|
||
|
||
- CLR profiler / managed method rewrite for `HistoryServiceContract.*` and
|
||
`RetrievalServiceContract.*` calls.
|
||
- API Monitor or Detours-style hooks at the lower native/WCF DLL boundary.
|
||
- Focus first on capturing managed byte-array arguments to:
|
||
- `OpenConnection2`
|
||
- `ExchangeKey`
|
||
- `ValidateClientCredential`
|
||
- `StartQuery2`
|
||
- `GetNextQueryResultBuffer2`
|
||
|
||
## WCF Diagnostics Attempt
|
||
|
||
`tools\AVEVA.Historian.NativeTraceHarness` attempted classic .NET Framework
|
||
`System.ServiceModel` diagnostics and message logging while running the same
|
||
successful integrated read. No `.svclog` file was produced, even though the
|
||
scenario succeeded. This is negative evidence that the native wrapper path does
|
||
not expose a managed WCF client pipeline in the harness AppDomain that can be
|
||
captured with ordinary WCF config diagnostics. A profiler/method-rewrite hook
|
||
remains the likely buffer-capture route.
|
||
|
||
## Event Add-Tag Hook Pass
|
||
|
||
Scenario:
|
||
|
||
```powershell
|
||
.\tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe --scenario event --server-name localhost --tcp-port 32568 --connection-wait-seconds 15 --pre-open-sleep-seconds 15 --max-rows 1 --lookback-minutes 1440
|
||
```
|
||
|
||
Artifacts:
|
||
|
||
- Script: `scripts\frida\aahclientmanaged-open-query.js`
|
||
- Latest capture: `docs\reverse-engineering\frida-aahclientmanaged-event-addtag-latest.ndjson`
|
||
- Native result: `docs\reverse-engineering\native-event-addtag-frida-child.json`
|
||
|
||
The native event query still succeeded and returned one event row. The
|
||
expanded hook list installed hooks for the event default-tag path:
|
||
|
||
- `HistorianAccess.CreateDefaultEventTag` at method RVA `0x43c2d4`
|
||
- `HistorianAccess.AddTagInternal` at method RVA `0x43be68`
|
||
- `HistorianClient.AddHistorianTag` at method RVA `0x417c18`
|
||
- `HistorianClient.ConvertEventTagToTagMetadata` at method RVA `0x417b68`
|
||
- `CTagMetadata.Save<SByteStream<SCrtMemFile>>` at method RVA `0x1044dc`
|
||
|
||
The capture has hook-install events and some hook-install failures for other
|
||
candidates, but still no enter/leave events. This confirms the same limitation
|
||
as the history pass: MethodDef RVAs from this mixed-mode assembly are not the
|
||
actual runtime entry points for the successful wrapper path. The next capture
|
||
mechanism needs CLR method rewriting/profiling or a lower native boundary such
|
||
as Winsock/API Monitor, not raw `base + RVA` Frida interceptors.
|
||
|
||
## Event Winsock/IPС Pass
|
||
|
||
`tools\AVEVA.Historian.NativeTraceHarness` now supports
|
||
`--pre-load-sleep-seconds`, allowing Frida to attach before
|
||
`aahClientManaged.dll` is loaded. Running the event scenario with that preload
|
||
pause produced:
|
||
|
||
- `docs\reverse-engineering\winsock-event-preload-localhost-latest.ndjson`
|
||
- native event open succeeded
|
||
- `StartQuery` succeeded
|
||
- one event row returned
|
||
- no Historian-port `connect`/`send`/`recv` events
|
||
- no tracked named-pipe or interesting file `ReadFile`/`WriteFile` payloads
|
||
|
||
For the local event scenario, the successful wrapper path is therefore not
|
||
visible through the current Winsock or named-pipe hook set. Keep the preload
|
||
pause because it is useful for future lower-level hooks, but the next byte
|
||
capture should target CLR method arguments, API Monitor at the native/WCF DLL
|
||
boundary, or a real remote/relay path where traffic leaves the process.
|
||
|
||
The remote relay path does expose event-mode transport, but it still stops at
|
||
the security layer. With endpoint-host rewriting enabled, event mode sends
|
||
`/HistCert` over `application/ssl-tls`, then repeats `/Hist-Integrated` over
|
||
`application/negotiate` with NTLMSSP messages. The server returns a short reject
|
||
record before the native harness reaches connected state, so this path has
|
||
endpoint/security evidence but still no query payload evidence.
|
||
|
||
## aahClient Export Hook Pass
|
||
|
||
`scripts\frida\aahclient-exports.js` and
|
||
`scripts\Attach-NativeTraceHarnessAahClientExportCapture.ps1` hook the
|
||
procedural `mdas_*` exports from `aahClient.dll` if that DLL is loaded. The
|
||
first local direct history run attached before `aahClientManaged.dll` load and
|
||
the native read succeeded, but Frida never observed `aahClient.dll` being loaded
|
||
and installed no export hooks. A separate `-DumpLoadedModules` run of the older
|
||
PowerShell harness also showed only `aahClientManaged.dll` among the current
|
||
AVEVA DLL set during a successful wrapper read.
|
||
|
||
That rules out `aahClient.dll` exports as the immediate capture boundary for
|
||
the active wrapper path. The `mdas_*` exports may still describe a separate
|
||
native client ABI, but they are not the calls currently made by
|
||
`HistoryQuery.StartQuery` in this harness.
|
||
|
||
## System Boundary Hook Pass
|
||
|
||
`scripts\frida\aahclientmanaged-system-boundary.js` and
|
||
`scripts\Attach-NativeTraceHarnessSystemBoundaryCapture.ps1` hook the imported
|
||
system/API boundary used by `aahClientManaged.dll`: file I/O, `NtCreateFile`,
|
||
`NtReadFile`, `NtWriteFile`, `NtDeviceIoControlFile`, DNS, exported Winsock
|
||
connect/send/recv APIs, `WSAIoctl`, `mswsock` extension exports, Secur32,
|
||
Crypt32, and NetAPI.
|
||
|
||
Local direct history and same-machine remote-IP reads still succeeded without
|
||
file/pipe/socket/security callbacks beyond hook installation. This reinforces
|
||
that the local Historian path is optimized below the query surface we need.
|
||
|
||
The Debian relay run adds one sharper fact. The relay accepted connections from
|
||
the Windows host, and the Windows TCP owner monitor attributed the established
|
||
connection to the `AVEVA.Historian.NativeTraceHarness` PID. Even with hooks
|
||
installed in that same PID before `OpenConnection`, no exported Winsock,
|
||
`WSAIoctl`, `mswsock`, or `NtDeviceIoControlFile` callbacks fired. The relay
|
||
connection still reset before the harness reached `ConnectedToServer = true`.
|
||
|
||
That makes raw Frida export hooks insufficient for the remaining transport
|
||
capture. The next local capture mechanism should be ETW/netsh/WFP/kernel-level
|
||
network tracing, API Monitor/Detours below the wrapper, or CLR profiler/IL
|
||
instrumentation inside the mixed-mode assembly.
|