Files
histsdk/docs/reverse-engineering/frida-aahclientmanaged-hook-pass.md
T
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

12 KiB
Raw Blame History

Frida Hook Pass: aahClientManaged Integrated Read

Scenario:

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:

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:

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:

.\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.