Files
histsdk/docs/reverse-engineering/frida-aahclientmanaged-hook-pass.md
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
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>
2026-05-04 06:31:48 -04:00

282 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.