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>
This commit is contained in:
dohertj2
2026-05-04 06:31:48 -04:00
commit c95824a65d
230 changed files with 38666 additions and 0 deletions
@@ -0,0 +1,63 @@
param(
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[int]$AttachDelaySeconds = 3,
[string]$OutputPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$fridaScript = Join-Path $PSScriptRoot "frida\aahclientmanaged-open-query.js"
$nativeReadScript = Join-Path $PSScriptRoot "Test-AahClientManagedReadIntegrated.ps1"
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\frida-aahclientmanaged-attach-read-$stamp.ndjson"
}
$outputDirectory = Split-Path -Parent $OutputPath
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$powershell = Join-Path $env:WINDIR "System32\WindowsPowerShell\v1.0\powershell.exe"
$childOut = Join-Path $env:TEMP ("histsdk-native-read-{0}.out.log" -f ([Guid]::NewGuid().ToString("N")))
$childErr = Join-Path $env:TEMP ("histsdk-native-read-{0}.err.log" -f ([Guid]::NewGuid().ToString("N")))
$childArgs = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $nativeReadScript,
"-TagName", $TagName,
"-LookbackMinutes", $LookbackMinutes.ToString(),
"-MaxRows", $MaxRows.ToString(),
"-ConnectionWaitSeconds", "15",
"-PreReadSleepSeconds", $AttachDelaySeconds.ToString(),
"-DumpLoadedModules"
)
Write-Host "Starting native read process and pausing $AttachDelaySeconds second(s) after aahClientManaged.dll load."
$process = Start-Process -FilePath $powershell -ArgumentList $childArgs -WorkingDirectory $repoRoot -RedirectStandardOutput $childOut -RedirectStandardError $childErr -PassThru -WindowStyle Hidden
try {
Start-Sleep -Seconds 1
Write-Host "Attaching Frida to PID $($process.Id). Capture: $OutputPath"
& frida -q -p $process.Id -l $fridaScript 2>&1 | Tee-Object -FilePath $OutputPath
if (-not $process.HasExited) {
$process.WaitForExit(30000) | Out-Null
}
}
finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
"--- native stdout ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childOut -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
"--- native stderr ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childErr -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
Remove-Item -LiteralPath $childOut, $childErr -ErrorAction SilentlyContinue
}
Write-Host "Capture complete: $OutputPath"
@@ -0,0 +1,80 @@
param(
[ValidateSet("history", "event")]
[string]$Scenario = "history",
[string]$ServerName = "localhost",
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[string]$RetrievalMode = "Full",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[UInt64]$ResolutionTicks = 0,
[switch]$DirectConnection,
[int]$PreLoadSleepSeconds = 10,
[int]$ConnectionWaitSeconds = 15,
[string]$OutputPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$fridaScript = Join-Path $PSScriptRoot "frida\aahclient-exports.js"
$harness = Join-Path $repoRoot "tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe"
if (-not (Test-Path -LiteralPath $harness)) {
throw "Missing harness executable. Run dotnet build .\Histsdk.slnx first."
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\aahclient-export-frida-$Scenario-$stamp.ndjson"
}
$outputDirectory = Split-Path -Parent $OutputPath
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$childOut = Join-Path $env:TEMP ("histsdk-aahclient-export-{0}.out.log" -f ([Guid]::NewGuid().ToString("N")))
$childErr = Join-Path $env:TEMP ("histsdk-aahclient-export-{0}.err.log" -f ([Guid]::NewGuid().ToString("N")))
$args = @(
"--scenario", $Scenario,
"--server-name", $ServerName,
"--tag", $TagName,
"--retrieval-mode", $RetrievalMode,
"--lookback-minutes", $LookbackMinutes.ToString(),
"--max-rows", $MaxRows.ToString(),
"--connection-wait-seconds", $ConnectionWaitSeconds.ToString(),
"--pre-load-sleep-seconds", $PreLoadSleepSeconds.ToString()
)
if ($ResolutionTicks -gt 0) {
$args += @("--resolution-ticks", $ResolutionTicks.ToString())
}
if ($DirectConnection) {
$args += "--direct-connection"
}
Write-Host "Starting native trace harness with $PreLoadSleepSeconds second pre-load pause."
$process = Start-Process -FilePath $harness -ArgumentList $args -WorkingDirectory $repoRoot -RedirectStandardOutput $childOut -RedirectStandardError $childErr -PassThru -WindowStyle Hidden
try {
Start-Sleep -Seconds 1
Write-Host "Attaching aahClient.dll export Frida capture to PID $($process.Id). Capture: $OutputPath"
& frida -q -p $process.Id -l $fridaScript 2>&1 | Tee-Object -FilePath $OutputPath
if (-not $process.HasExited) {
$process.WaitForExit(30000) | Out-Null
}
}
finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
"--- native stdout ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childOut -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
"--- native stderr ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childErr -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
Remove-Item -LiteralPath $childOut, $childErr -ErrorAction SilentlyContinue
}
Write-Host "Capture complete: $OutputPath"
@@ -0,0 +1,256 @@
param(
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[string]$ServerName = "localhost",
[ValidateSet("history", "event")]
[string]$Scenario = "history",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[int]$ConnectionWaitSeconds = 15,
[int]$AttachWindowSeconds = 30,
[switch]$DirectConnection,
[string]$OutputPath = $null,
[string]$PointerPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$harness = Join-Path $repoRoot "tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe"
if (-not (Test-Path -LiteralPath $harness)) {
throw "Missing harness executable. Run dotnet build .\Histsdk.slnx first."
}
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\runtime-pointer-frida-$Scenario-$stamp.ndjson"
}
if ([string]::IsNullOrWhiteSpace($PointerPath)) {
$PointerPath = Join-Path $repoRoot "docs\reverse-engineering\runtime-method-pointers-before-$Scenario-start-latest.json"
}
$outputDirectory = Split-Path -Parent $OutputPath
$pointerDirectory = Split-Path -Parent $PointerPath
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
New-Item -ItemType Directory -Force -Path $pointerDirectory | Out-Null
Remove-Item -LiteralPath $PointerPath -Force -ErrorAction SilentlyContinue
$childOut = Join-Path $env:TEMP ("histsdk-runtime-pointer-{0}.out.log" -f ([Guid]::NewGuid().ToString("N")))
$childErr = Join-Path $env:TEMP ("histsdk-runtime-pointer-{0}.err.log" -f ([Guid]::NewGuid().ToString("N")))
$fridaScript = Join-Path $env:TEMP ("histsdk-runtime-pointer-{0}.js" -f ([Guid]::NewGuid().ToString("N")))
$childArgs = @(
"--scenario", $Scenario,
"--server-name", $ServerName,
"--tag", $TagName,
"--lookback-minutes", $LookbackMinutes.ToString(),
"--max-rows", $MaxRows.ToString(),
"--connection-wait-seconds", $ConnectionWaitSeconds.ToString(),
"--runtime-method-pointer-output", $PointerPath,
"--runtime-method-pointer-filters", "StartDataQuery;StartQuery;GetNextRow;StartEventQuery",
"--pre-start-sleep-seconds", $AttachWindowSeconds.ToString()
)
if ($DirectConnection) {
$childArgs += "--direct-connection"
}
Write-Host "Starting native trace harness. It will pause $AttachWindowSeconds second(s) before StartQuery."
$process = Start-Process -FilePath $harness -ArgumentList $childArgs -WorkingDirectory $repoRoot -RedirectStandardOutput $childOut -RedirectStandardError $childErr -PassThru -WindowStyle Hidden
try {
$deadline = (Get-Date).AddSeconds([Math]::Max($ConnectionWaitSeconds + 20, 30))
while (-not (Test-Path -LiteralPath $PointerPath)) {
if ($process.HasExited) {
throw "Harness exited before writing runtime pointer snapshot."
}
if ((Get-Date) -gt $deadline) {
throw "Timed out waiting for runtime pointer snapshot: $PointerPath"
}
Start-Sleep -Milliseconds 250
}
$snapshot = Get-Content -LiteralPath $PointerPath -Raw | ConvertFrom-Json
$targets = @()
foreach ($group in $snapshot.MethodPointers) {
foreach ($method in $group.Methods) {
if ($null -eq $method.FunctionPointer -or $null -ne $method.PrepareError) {
continue
}
$targets += [pscustomobject]@{
Name = "$($method.DeclaringType).$($method.Name)"
Address = $method.FunctionPointer
MetadataToken = $method.MetadataToken
Filter = $group.Filter
}
}
}
$targets = $targets | Sort-Object Address -Unique
if ($targets.Count -eq 0) {
throw "Runtime pointer snapshot did not contain hookable function pointers."
}
$targetJson = $targets | ConvertTo-Json -Depth 6 -Compress
$script = @"
'use strict';
const targets = $targetJson;
const maxDumpBytes = 256;
function emit(kind, payload) {
payload.kind = kind;
payload.pid = Process.id;
payload.tid = Process.getCurrentThreadId();
payload.timestamp = new Date().toISOString();
console.log('FRIDA_EVENT ' + JSON.stringify(payload));
}
function toHex(bytes) {
const parts = [];
for (let i = 0; i < bytes.length; i++) {
const text = bytes[i].toString(16);
parts.push(text.length === 1 ? '0' + text : text);
}
return parts.join('');
}
function asciiPreview(bytes) {
let text = '';
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i];
text += b >= 32 && b <= 126 ? String.fromCharCode(b) : '.';
}
return text;
}
function utf16Preview(bytes) {
let text = '';
const count = Math.min(bytes.length - (bytes.length % 2), 96);
for (let i = 0; i < count; i += 2) {
const code = bytes[i] | (bytes[i + 1] << 8);
text += code >= 32 && code <= 126 ? String.fromCharCode(code) : '.';
}
return text;
}
function isReadablePointer(value) {
if (value.isNull() || value.compare(ptr('0x10000')) < 0) {
return false;
}
const range = Process.findRangeByAddress(value);
return range !== null && range.protection.indexOf('r') !== -1;
}
function dumpPointer(value) {
const range = Process.findRangeByAddress(value);
const available = range === null ? 0 : Math.min(maxDumpBytes, range.base.add(range.size).sub(value).toNumber());
if (available <= 0) {
return null;
}
const raw = Memory.readByteArray(value, available);
if (raw === null) {
return null;
}
const bytes = Array.from(new Uint8Array(raw));
return {
address: value.toString(),
rangeBase: range.base.toString(),
rangeSize: range.size,
protection: range.protection,
byteCount: bytes.length,
hexPrefix: toHex(bytes.slice(0, Math.min(bytes.length, 96))),
asciiPrefix: asciiPreview(bytes.slice(0, Math.min(bytes.length, 96))),
utf16Prefix: utf16Preview(bytes.slice(0, Math.min(bytes.length, 192)))
};
}
function inspectArgs(args, count) {
const inspected = [];
for (let i = 0; i < count; i++) {
const value = args[i];
const item = { index: i, value: value.toString() };
if (isReadablePointer(value)) {
try {
item.memory = dumpPointer(value);
} catch (e) {
item.memoryError = String(e);
}
}
inspected.push(item);
}
return inspected;
}
emit('runtime-pointer-startup', {
arch: Process.arch,
platform: Process.platform,
targetCount: targets.length,
targets: targets
});
for (const target of targets) {
const address = ptr(target.Address);
try {
Interceptor.attach(address, {
onEnter(args) {
this.target = target;
this.argsSnapshot = inspectArgs(args, 14);
emit('enter', {
function: target.Name,
address: target.Address,
metadataToken: target.MetadataToken,
filter: target.Filter,
args: this.argsSnapshot
});
},
onLeave(retval) {
emit('leave', {
function: this.target.Name,
address: this.target.Address,
retval: retval.toString(),
args: this.argsSnapshot
});
}
});
emit('hooked', target);
} catch (e) {
emit('hook-error', {
function: target.Name,
address: target.Address,
metadataToken: target.MetadataToken,
error: String(e)
});
}
}
"@
Set-Content -LiteralPath $fridaScript -Value $script -Encoding ASCII
Write-Host "Attaching Frida to PID $($process.Id). Capture: $OutputPath"
& frida -q -p $process.Id -l $fridaScript 2>&1 | Tee-Object -FilePath $OutputPath
if (-not $process.HasExited) {
$process.WaitForExit(30000) | Out-Null
}
}
finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
"--- pointer snapshot ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $PointerPath -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
"--- native stdout ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childOut -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
"--- native stderr ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childErr -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
Remove-Item -LiteralPath $childOut, $childErr, $fridaScript -ErrorAction SilentlyContinue
}
Write-Host "Capture complete: $OutputPath"
@@ -0,0 +1,81 @@
param(
[ValidateSet("history", "event")]
[string]$Scenario = "history",
[string]$ServerName = "localhost",
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[string]$RetrievalMode = "Full",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[UInt64]$ResolutionTicks = 0,
[switch]$DirectConnection,
[int]$PreLoadSleepSeconds = 10,
[int]$ConnectionWaitSeconds = 15,
[int]$FridaTimeoutSeconds = 90,
[string]$OutputPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$fridaScript = Join-Path $PSScriptRoot "frida\aahclientmanaged-system-boundary.js"
$harness = Join-Path $repoRoot "tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe"
if (-not (Test-Path -LiteralPath $harness)) {
throw "Missing harness executable. Run dotnet build .\Histsdk.slnx first."
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\system-boundary-frida-$Scenario-$stamp.ndjson"
}
$outputDirectory = Split-Path -Parent $OutputPath
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$childOut = Join-Path $env:TEMP ("histsdk-system-boundary-{0}.out.log" -f ([Guid]::NewGuid().ToString("N")))
$childErr = Join-Path $env:TEMP ("histsdk-system-boundary-{0}.err.log" -f ([Guid]::NewGuid().ToString("N")))
$args = @(
"--scenario", $Scenario,
"--server-name", $ServerName,
"--tag", $TagName,
"--retrieval-mode", $RetrievalMode,
"--lookback-minutes", $LookbackMinutes.ToString(),
"--max-rows", $MaxRows.ToString(),
"--connection-wait-seconds", $ConnectionWaitSeconds.ToString(),
"--pre-load-sleep-seconds", $PreLoadSleepSeconds.ToString()
)
if ($ResolutionTicks -gt 0) {
$args += @("--resolution-ticks", $ResolutionTicks.ToString())
}
if ($DirectConnection) {
$args += "--direct-connection"
}
Write-Host "Starting native trace harness with $PreLoadSleepSeconds second pre-load pause."
$process = Start-Process -FilePath $harness -ArgumentList $args -WorkingDirectory $repoRoot -RedirectStandardOutput $childOut -RedirectStandardError $childErr -PassThru -WindowStyle Hidden
try {
Start-Sleep -Seconds 1
Write-Host "Attaching system-boundary Frida capture to PID $($process.Id). Capture: $OutputPath"
& frida -q -t $FridaTimeoutSeconds -p $process.Id -l $fridaScript -o $OutputPath
if (-not $process.HasExited) {
$process.WaitForExit(30000) | Out-Null
}
}
finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
"--- native stdout ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childOut -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
"--- native stderr ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childErr -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
Remove-Item -LiteralPath $childOut, $childErr -ErrorAction SilentlyContinue
}
Write-Host "Capture complete: $OutputPath"
@@ -0,0 +1,84 @@
param(
[ValidateSet("history", "event")]
[string]$Scenario = "history",
[string]$ServerName = "localhost",
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[string]$RetrievalMode = "Full",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[UInt64]$ResolutionTicks = 0,
[switch]$DirectConnection,
[int]$AttachDelaySeconds = 5,
[int]$PreLoadSleepSeconds = 0,
[string]$OutputPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$fridaScript = Join-Path $PSScriptRoot "frida\aahclientmanaged-winsock.js"
$harness = Join-Path $repoRoot "tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe"
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\winsock-$Scenario-$stamp.ndjson"
}
$outputDirectory = Split-Path -Parent $OutputPath
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
Push-Location $repoRoot
try {
dotnet build .\tools\AVEVA.Historian.NativeTraceHarness\AVEVA.Historian.NativeTraceHarness.csproj | Out-Host
$childOut = Join-Path $env:TEMP ("histsdk-native-trace-{0}.out.log" -f ([Guid]::NewGuid().ToString("N")))
$childErr = Join-Path $env:TEMP ("histsdk-native-trace-{0}.err.log" -f ([Guid]::NewGuid().ToString("N")))
$args = @(
"--scenario", $Scenario,
"--server-name", $ServerName,
"--tag", $TagName,
"--retrieval-mode", $RetrievalMode,
"--lookback-minutes", $LookbackMinutes.ToString(),
"--max-rows", $MaxRows.ToString(),
"--pre-load-sleep-seconds", $PreLoadSleepSeconds.ToString(),
"--pre-open-sleep-seconds", $AttachDelaySeconds.ToString()
)
if ($ResolutionTicks -gt 0) {
$args += @("--resolution-ticks", $ResolutionTicks.ToString())
}
if ($DirectConnection) {
$args += "--direct-connection"
}
Write-Host "Starting native trace harness with $AttachDelaySeconds second pre-open pause."
$process = Start-Process -FilePath $harness -ArgumentList $args -WorkingDirectory $repoRoot -RedirectStandardOutput $childOut -RedirectStandardError $childErr -PassThru -WindowStyle Hidden
try {
Start-Sleep -Seconds 1
Write-Host "Attaching Winsock Frida capture to PID $($process.Id). Capture: $OutputPath"
& frida -q -p $process.Id -l $fridaScript 2>&1 | Tee-Object -FilePath $OutputPath
if (-not $process.HasExited) {
$process.WaitForExit(30000) | Out-Null
}
}
finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
}
"--- native stdout ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childOut -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
"--- native stderr ---" | Tee-Object -FilePath $OutputPath -Append
Get-Content -LiteralPath $childErr -ErrorAction SilentlyContinue | Tee-Object -FilePath $OutputPath -Append
Remove-Item -LiteralPath $childOut, $childErr -ErrorAction SilentlyContinue
}
}
finally {
Pop-Location
}
Write-Host "Capture complete: $OutputPath"
@@ -0,0 +1,176 @@
param(
[string]$SshUser = "dohertj2",
[string]$SshHost = "10.100.0.35",
[string]$TargetHost = "10.100.0.48",
[int]$ListenPort = 32568,
[int]$TargetPort = 32568,
[ValidateSet("history", "event")]
[string]$Scenario = "history",
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[string]$OutputPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\system-boundary-via-debian-relay-$Scenario-$stamp.ndjson"
}
$localPy = Join-Path $env:TEMP "histsdk-simple-relay-$stamp.py"
$remotePy = "/tmp/histsdk-simple-relay-$stamp.py"
$remoteLog = "/tmp/histsdk-simple-relay-$stamp.log"
$sshTarget = "$SshUser@$SshHost"
$tcpOwnerLog = Join-Path $env:TEMP "histsdk-tcp-owner-$stamp.ndjson"
$relaySource = @'
import argparse, selectors, socket, sys, time
parser = argparse.ArgumentParser()
parser.add_argument("--listen-port", type=int, default=32568)
parser.add_argument("--target", required=True)
parser.add_argument("--target-port", type=int, default=32568)
args = parser.parse_args()
sel = selectors.DefaultSelector()
peers = {}
def log(text):
print(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), text, flush=True)
def close_pair(sock):
peer = peers.pop(sock, None)
if peer is None:
return
peers.pop(peer, None)
for item in (sock, peer):
try: sel.unregister(item)
except Exception: pass
try: item.close()
except Exception: pass
def relay(sock):
peer = peers.get(sock)
if peer is None:
return
try:
data = sock.recv(65536)
except Exception as exc:
log("recv_error " + repr(exc))
close_pair(sock)
return
if not data:
close_pair(sock)
return
try:
peer.sendall(data)
except Exception as exc:
log("send_error " + repr(exc))
close_pair(sock)
def accept(listener):
client, address = listener.accept()
client.setblocking(False)
upstream = socket.create_connection((args.target, args.target_port), timeout=8)
upstream.setblocking(False)
peers[client] = upstream
peers[upstream] = client
sel.register(client, selectors.EVENT_READ, relay)
sel.register(upstream, selectors.EVENT_READ, relay)
log("connect " + repr(address))
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("0.0.0.0", args.listen_port))
listener.listen(16)
listener.setblocking(False)
sel.register(listener, selectors.EVENT_READ, accept)
log("listening")
while True:
for key, _ in sel.select(timeout=1):
key.data(key.fileobj)
'@
Push-Location $repoRoot
try {
Set-Content -LiteralPath $localPy -Value $relaySource -Encoding UTF8
scp -q $localPy "${sshTarget}:$remotePy"
$remoteCommand = "nohup python3 $remotePy --listen-port $ListenPort --target $TargetHost --target-port $TargetPort > $remoteLog 2>&1 < /dev/null & echo `$!"
$relayPid = (ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget $remoteCommand).Trim()
Start-Sleep -Seconds 1
"--- relay startup log ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host
ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget "cat $remoteLog 2>/dev/null || true" |
Tee-Object -FilePath $OutputPath -Append |
Out-Host
$monitorJob = Start-Job -ScriptBlock {
param($RemoteAddress, $RemotePort, $LogPath)
$deadline = (Get-Date).AddSeconds(45)
while ((Get-Date) -lt $deadline) {
Get-NetTCPConnection -RemoteAddress $RemoteAddress -RemotePort $RemotePort -ErrorAction SilentlyContinue |
ForEach-Object {
$processName = $null
try {
$processName = (Get-Process -Id $_.OwningProcess -ErrorAction Stop).ProcessName
}
catch {
$processName = $null
}
[pscustomobject]@{
TimestampUtc = (Get-Date).ToUniversalTime().ToString("O")
LocalAddress = $_.LocalAddress
LocalPort = $_.LocalPort
RemoteAddress = $_.RemoteAddress
RemotePort = $_.RemotePort
State = $_.State.ToString()
OwningProcess = $_.OwningProcess
ProcessName = $processName
} | ConvertTo-Json -Compress
} | Add-Content -LiteralPath $LogPath
Start-Sleep -Milliseconds 50
}
} -ArgumentList $SshHost, $ListenPort, $tcpOwnerLog
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Attach-NativeTraceHarnessSystemBoundaryCapture.ps1 `
-Scenario $Scenario `
-ServerName $SshHost `
-TagName $TagName `
-LookbackMinutes $LookbackMinutes `
-MaxRows $MaxRows `
-PreLoadSleepSeconds 10 `
-OutputPath $OutputPath
Stop-Job -Job $monitorJob -ErrorAction SilentlyContinue
Receive-Job -Job $monitorJob -ErrorAction SilentlyContinue | Out-Null
Remove-Job -Job $monitorJob -Force -ErrorAction SilentlyContinue
"--- windows tcp owner monitor ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host
Get-Content -LiteralPath $tcpOwnerLog -ErrorAction SilentlyContinue |
Sort-Object -Unique |
Tee-Object -FilePath $OutputPath -Append |
Out-Host
"--- relay final log ---" | Tee-Object -FilePath $OutputPath -Append | Out-Host
ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget "cat $remoteLog 2>/dev/null || true" |
Tee-Object -FilePath $OutputPath -Append |
Out-Host
}
finally {
try {
if ($relayPid) {
ssh -o BatchMode=yes $sshTarget "kill $relayPid 2>/dev/null || true; rm -f $remotePy $remoteLog" | Out-Null
}
}
catch {
Write-Warning "Relay cleanup failed: $_"
}
Remove-Item -LiteralPath $localPy -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $tcpOwnerLog -ErrorAction SilentlyContinue
Pop-Location
}
@@ -0,0 +1,91 @@
param(
[ValidateSet("NativeRead", "ManagedValCl")]
[string]$Scenario = "NativeRead",
[string]$HostName = "localhost",
[UInt16]$Port = 32568,
[string]$TagName = $env:HISTORIAN_TEST_TAG,
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[int]$FridaTimeoutSeconds = 35,
[string]$OutputPath
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$fridaScript = Join-Path $PSScriptRoot "frida\aahclientaccesspoint-valcl-context.js"
if (-not (Test-Path -LiteralPath $fridaScript)) {
throw "Missing Frida script at $fridaScript"
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$OutputPath = Join-Path $repoRoot "artifacts\reverse-engineering\aahclientaccesspoint-valcl-$($Scenario.ToLowerInvariant())-$stamp.ndjson"
}
$OutputPath = [IO.Path]::GetFullPath($OutputPath)
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $OutputPath) | Out-Null
$serverProcess = Get-Process -Name "aahClientAccessPoint" -ErrorAction Stop | Select-Object -First 1
Write-Host "Attaching server ValCl context probe to PID $($serverProcess.Id). Capture: $OutputPath"
$fridaArgs = @(
"-q",
"-t", $FridaTimeoutSeconds.ToString(),
"-p", $serverProcess.Id.ToString(),
"-l", $fridaScript,
"-o", $OutputPath
)
$fridaJob = Start-Job -ScriptBlock {
param([string[]]$ArgsForFrida)
& frida @ArgsForFrida
} -ArgumentList (,$fridaArgs)
Start-Sleep -Seconds 2
try {
if ($Scenario -eq "NativeRead") {
if ([string]::IsNullOrWhiteSpace($TagName)) {
throw "A tag name is required for NativeRead. Pass -TagName or set HISTORIAN_TEST_TAG."
}
$harness = Join-Path $repoRoot "tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe"
if (-not (Test-Path -LiteralPath $harness)) {
dotnet build .\tools\AVEVA.Historian.NativeTraceHarness\AVEVA.Historian.NativeTraceHarness.csproj --nologo --verbosity:minimal | Out-Host
}
& $harness `
--scenario history `
--server-name $HostName `
--tcp-port $Port `
--tag $TagName `
--lookback-minutes $LookbackMinutes `
--max-rows $MaxRows `
--connection-wait-seconds 15 | Out-Host
}
else {
$probe = Join-Path $repoRoot "tools\AVEVA.Historian.NetFxWcfProbe\bin\Debug\net481\AVEVA.Historian.NetFxWcfProbe.exe"
if (-not (Test-Path -LiteralPath $probe)) {
dotnet build .\tools\AVEVA.Historian.NetFxWcfProbe\AVEVA.Historian.NetFxWcfProbe.csproj --nologo --verbosity:minimal | Out-Host
}
& $probe --endpoint "net.pipe://localhost/Hist" | Out-Host
}
}
finally {
Wait-Job $fridaJob -Timeout ([Math]::Max($FridaTimeoutSeconds, 5)) | Out-Null
$fridaOutput = Receive-Job $fridaJob -ErrorAction SilentlyContinue
if ($fridaOutput) {
$fridaOutput | Out-Host
$fridaSidecar = $OutputPath + ".frida.log"
$fridaOutput | Set-Content -LiteralPath $fridaSidecar -Encoding UTF8
if ($fridaOutput -match "Failed to attach|refused to load frida-agent") {
Write-Warning "Frida could not attach to aahClientAccessPoint. Run this script from an elevated PowerShell session to capture server helper calls."
}
}
Remove-Job $fridaJob -Force -ErrorAction SilentlyContinue
}
Write-Host "Server ValCl context capture written to $OutputPath"
+91
View File
@@ -0,0 +1,91 @@
param(
[string]$Server = "localhost",
[string]$Database = "ZB",
[int]$Limit = 25
)
$ErrorActionPreference = "Stop"
$query = @"
;WITH deployed_package_chain AS (
SELECT
g.gobject_id,
p.package_id,
p.derived_from_package_id,
0 AS depth
FROM gobject g
INNER JOIN package p
ON p.package_id = g.deployed_package_id
WHERE g.is_template = 0
AND g.deployed_package_id <> 0
UNION ALL
SELECT
dpc.gobject_id,
p.package_id,
p.derived_from_package_id,
dpc.depth + 1
FROM deployed_package_chain dpc
INNER JOIN package p
ON p.package_id = dpc.derived_from_package_id
WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
), ranked AS (
SELECT
dpc.gobject_id,
g.tag_name,
da.attribute_name,
g.tag_name + '.' + da.attribute_name
+ CASE WHEN da.is_array = 1 THEN '[]' ELSE '' END
AS full_tag_reference,
da.mx_data_type,
dt.description AS data_type_name,
da.is_array,
CASE WHEN EXISTS (
SELECT 1 FROM deployed_package_chain dpc2
INNER JOIN primitive_instance pi
ON pi.package_id = dpc2.package_id
AND pi.primitive_name = da.attribute_name
INNER JOIN primitive_definition pd
ON pd.primitive_definition_id = pi.primitive_definition_id
AND pd.primitive_name = 'HistoryExtension'
WHERE dpc2.gobject_id = dpc.gobject_id
) THEN 1 ELSE 0 END AS is_historized,
ROW_NUMBER() OVER (
PARTITION BY dpc.gobject_id, da.attribute_name
ORDER BY dpc.depth
) AS rn
FROM deployed_package_chain dpc
INNER JOIN dynamic_attribute da
ON da.package_id = dpc.package_id
INNER JOIN gobject g
ON g.gobject_id = dpc.gobject_id
INNER JOIN template_definition td
ON td.template_definition_id = g.template_definition_id
LEFT JOIN data_type dt
ON dt.mx_data_type = da.mx_data_type
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
AND da.attribute_name NOT LIKE '[_]%'
AND da.attribute_name NOT LIKE '%.Description'
AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
)
SELECT TOP ($Limit)
full_tag_reference,
tag_name,
attribute_name,
mx_data_type,
data_type_name
FROM ranked
WHERE rn = 1
AND is_historized = 1
AND is_array = 0
ORDER BY tag_name, attribute_name
OPTION (MAXRECURSION 100);
"@
$sqlFile = Join-Path $env:TEMP ("histsdk-historized-tags-{0}.sql" -f ([Guid]::NewGuid().ToString("N")))
try {
Set-Content -LiteralPath $sqlFile -Value $query -Encoding UTF8
sqlcmd -S $Server -d $Database -E -i $sqlFile -W -s "|"
}
finally {
Remove-Item -LiteralPath $sqlFile -ErrorAction SilentlyContinue
}
@@ -0,0 +1,50 @@
param(
[string]$HostName = "localhost",
[int]$Port = 32568
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
function Convert-SecureStringToPlainText {
param([Parameter(Mandatory = $true)][securestring]$SecureString)
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
try {
[Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
}
finally {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
Push-Location $repoRoot
try {
$suggestedUserName = "$env:COMPUTERNAME\$env:USERNAME"
$userName = Read-Host "Historian user [$suggestedUserName]"
if ([string]::IsNullOrWhiteSpace($userName)) {
$userName = $suggestedUserName
}
$securePassword = Read-Host "Historian password" -AsSecureString
$plainPassword = Convert-SecureStringToPlainText $securePassword
$env:HISTORIAN_USER = $userName
$env:HISTORIAN_PASSWORD = $plainPassword
Write-Host ""
Write-Host "Building latest harness..."
dotnet build .\Histsdk.slnx
Write-Host ""
Write-Host "Running managed Open2 probe against $HostName`:$Port..."
dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-open2 $HostName $Port
}
finally {
Remove-Item Env:HISTORIAN_USER -ErrorAction SilentlyContinue
Remove-Item Env:HISTORIAN_PASSWORD -ErrorAction SilentlyContinue
if (Get-Variable -Name plainPassword -Scope Local -ErrorAction SilentlyContinue) {
$plainPassword = $null
}
Pop-Location
}
@@ -0,0 +1,54 @@
param(
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[string]$OutputPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$fridaScript = Join-Path $PSScriptRoot "frida\aahclientmanaged-open-query.js"
$nativeReadScript = Join-Path $PSScriptRoot "Test-AahClientManagedReadIntegrated.ps1"
if (-not (Test-Path -LiteralPath $fridaScript)) {
throw "Missing Frida script at $fridaScript"
}
if (-not (Test-Path -LiteralPath $nativeReadScript)) {
throw "Missing native read script at $nativeReadScript"
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\frida-aahclientmanaged-integrated-read-$stamp.ndjson"
}
$outputDirectory = Split-Path -Parent $OutputPath
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$powershell = Join-Path $env:WINDIR "System32\WindowsPowerShell\v1.0\powershell.exe"
if (-not (Test-Path -LiteralPath $powershell)) {
$powershell = "powershell.exe"
}
$fridaArgs = @(
"-q",
"-f", $powershell,
"-l", $fridaScript,
"--",
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $nativeReadScript,
"-TagName", $TagName,
"-LookbackMinutes", $LookbackMinutes.ToString(),
"-MaxRows", $MaxRows.ToString(),
"-ConnectionWaitSeconds", "15"
)
Write-Host "Writing Frida capture to $OutputPath"
Write-Host "Target tag: $TagName"
& frida @fridaArgs 2>&1 | Tee-Object -FilePath $OutputPath
Write-Host "Capture complete: $OutputPath"
+224
View File
@@ -0,0 +1,224 @@
param(
[string]$SshUser = "dohertj2",
[string]$SshHost = "10.100.0.35",
[string]$RelayListenHost = "0.0.0.0",
[int]$RelayListenPort = 32568,
[string]$TargetHost = "10.100.0.48",
[int]$TargetPort = 32568,
[switch]$RewriteEndpointHost,
[ValidateSet("history", "event")]
[string]$Scenario = "history",
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[string]$RetrievalMode = "Full",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[UInt64]$ResolutionTicks = 0,
[string[]]$HarnessExtraArgs = @(),
[string]$OutputPath = $null,
[string]$HarnessOutputPath = $null
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $repoRoot "docs\reverse-engineering\debian-relay-$Scenario-$stamp.ndjson"
}
if ([string]::IsNullOrWhiteSpace($HarnessOutputPath)) {
$HarnessOutputPath = Join-Path $repoRoot "docs\reverse-engineering\native-trace-harness-via-debian-relay-$Scenario-$stamp.json"
}
$localPy = Join-Path $env:TEMP "histsdk-relay-$stamp.py"
$remotePy = "/tmp/histsdk-relay-$stamp.py"
$remoteLog = "/tmp/histsdk-relay-$stamp.ndjson"
$sshTarget = "$SshUser@$SshHost"
$relaySource = @'
import argparse, json, selectors, socket, time
parser = argparse.ArgumentParser()
parser.add_argument('--listen', default='0.0.0.0')
parser.add_argument('--listen-port', type=int, default=32568)
parser.add_argument('--target', required=True)
parser.add_argument('--target-port', type=int, default=32568)
parser.add_argument('--prefix', type=int, default=16)
parser.add_argument('--rewrite-from', default='')
parser.add_argument('--rewrite-to', default='')
args = parser.parse_args()
sel = selectors.DefaultSelector()
peers = {}
conn_id = 0
def emit(kind, **payload):
payload['kind'] = kind
payload['ts'] = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
print(json.dumps(payload, separators=(',', ':')), flush=True)
def safe_ascii(data):
out = []
for b in data:
if 32 <= b <= 126:
out.append(chr(b))
elif b in (9, 10, 13):
out.append(' ')
else:
out.append('.')
return ''.join(out)
def classify(data):
if data.startswith(b'\x00\x01\x00\x01\x02\x02') and b'net.tcp:/' in data:
return 'nettcp-preamble'
if data == b'\x0a':
return 'nettcp-preamble-ack'
if data.startswith(b'\x16\x03'):
return 'tls-record'
if len(data) >= 9 and data[5:13] == b'NTLMSSP\x00':
return 'ntlmssp'
if len(data) >= 11 and data[7:15] == b'NTLMSSP\x00':
return 'ntlmssp'
if data.startswith(b'\x09\x15application/'):
return 'nettcp-content-type'
return 'unknown'
def safe_detail(data):
kind = classify(data)
if kind in ('nettcp-preamble', 'nettcp-content-type'):
return safe_ascii(data)
return None
rewrite_from = args.rewrite_from.encode('ascii') if args.rewrite_from else b''
rewrite_to = args.rewrite_to.encode('ascii') if args.rewrite_to else b''
if rewrite_from and len(rewrite_from) != len(rewrite_to):
raise SystemExit('rewrite strings must have equal byte length')
def accept(sock):
global conn_id
client, addr = sock.accept()
client.setblocking(False)
upstream = socket.create_connection((args.target, args.target_port), timeout=8)
upstream.setblocking(False)
conn_id += 1
cid = conn_id
peers[client] = (upstream, cid, 'c2s')
peers[upstream] = (client, cid, 's2c')
sel.register(client, selectors.EVENT_READ, relay)
sel.register(upstream, selectors.EVENT_READ, relay)
emit('connect', id=cid, client=str(addr), target=f'{args.target}:{args.target_port}')
def close_pair(sock):
peer_info = peers.pop(sock, None)
if peer_info is None:
return
other, cid, direction = peer_info
peers.pop(other, None)
for s in (sock, other):
try: sel.unregister(s)
except Exception: pass
try: s.close()
except Exception: pass
emit('close', id=cid, direction=direction)
def relay(sock):
peer_info = peers.get(sock)
if peer_info is None:
return
other, cid, direction = peer_info
try:
data = sock.recv(65536)
except Exception as exc:
emit('recv_error', id=cid, direction=direction, error=repr(exc))
close_pair(sock)
return
if not data:
close_pair(sock)
return
if direction == 'c2s' and rewrite_from and rewrite_from in data:
data = data.replace(rewrite_from, rewrite_to)
emit('rewrite', id=cid, direction=direction, from_value=args.rewrite_from, to_value=args.rewrite_to)
detail = safe_detail(data)
event = {
'id': cid,
'direction': direction,
'bytes': len(data),
'classification': classify(data),
'prefix': data[:args.prefix].hex()
}
if detail is not None:
event['safe_ascii'] = detail
emit('data', **event)
try:
other.sendall(data)
except Exception as exc:
emit('send_error', id=cid, direction=direction, error=repr(exc))
close_pair(sock)
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((args.listen, args.listen_port))
listener.listen(16)
listener.setblocking(False)
sel.register(listener, selectors.EVENT_READ, accept)
emit('listening', listen=f'{args.listen}:{args.listen_port}', target=f'{args.target}:{args.target_port}')
while True:
for key, _ in sel.select(timeout=1):
key.data(key.fileobj)
'@
Push-Location $repoRoot
try {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $OutputPath) | Out-Null
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $HarnessOutputPath) | Out-Null
Set-Content -LiteralPath $localPy -Value $relaySource -Encoding UTF8
scp -q $localPy "${sshTarget}:$remotePy"
$rewriteArgs = ""
if ($RewriteEndpointHost) {
if ($SshHost.Length -ne $TargetHost.Length) {
throw "RewriteEndpointHost requires SshHost and TargetHost to have equal string length."
}
$rewriteArgs = " --rewrite-from $SshHost --rewrite-to $TargetHost"
}
$remoteCommand = "nohup python3 $remotePy --listen $RelayListenHost --listen-port $RelayListenPort --target $TargetHost --target-port $TargetPort$rewriteArgs > $remoteLog 2>&1 < /dev/null & echo `$!"
$relayPid = (ssh -o BatchMode=yes -o ConnectTimeout=8 $sshTarget $remoteCommand).Trim()
Start-Sleep -Seconds 1
ssh -o BatchMode=yes $sshTarget "cat $remoteLog" | Tee-Object -FilePath $OutputPath | Out-Host
$harnessArgs = @(
"--scenario", $Scenario,
"--server-name", $SshHost,
"--tag", $TagName,
"--retrieval-mode", $RetrievalMode,
"--lookback-minutes", $LookbackMinutes.ToString(),
"--max-rows", $MaxRows.ToString()
)
if ($ResolutionTicks -gt 0) {
$harnessArgs += @("--resolution-ticks", $ResolutionTicks.ToString())
}
if ($HarnessExtraArgs.Count -gt 0) {
$harnessArgs += $HarnessExtraArgs
}
& .\tools\AVEVA.Historian.NativeTraceHarness\bin\Debug\net481\AVEVA.Historian.NativeTraceHarness.exe @harnessArgs |
Set-Content -Path $HarnessOutputPath -Encoding UTF8
$harnessExit = $LASTEXITCODE
ssh -o BatchMode=yes $sshTarget "cat $remoteLog" | Set-Content -Path $OutputPath -Encoding UTF8
exit $harnessExit
}
finally {
try {
if ($relayPid) {
ssh -o BatchMode=yes $sshTarget "kill $relayPid 2>/dev/null || true; rm -f $remotePy" | Out-Null
}
}
catch {
Write-Warning "Relay cleanup failed: $_"
}
Remove-Item -LiteralPath $localPy -ErrorAction SilentlyContinue
Pop-Location
}
+89
View File
@@ -0,0 +1,89 @@
param(
[string]$SshUser = "dohertj2",
[string]$SshHost = "10.100.0.35",
[string]$TargetHost = "10.100.0.48",
[ValidateSet("history", "event")]
[string]$Scenario = "history",
[string]$TagName = "OtOpcUaParityTest_001.Counter",
[int]$LookbackMinutes = 1440,
[int]$MaxRows = 1,
[string]$OutputPrefix = $null
)
$ErrorActionPreference = "Stop"
$PSNativeCommandUseErrorActionPreference = $false
$repoRoot = Split-Path -Parent $PSScriptRoot
$stamp = Get-Date -Format "yyyyMMdd-HHmmss"
if ([string]::IsNullOrWhiteSpace($OutputPrefix)) {
$OutputPrefix = Join-Path $repoRoot "docs\reverse-engineering\pktmon-debian-relay-$Scenario-latest"
}
$outputDirectory = Split-Path -Parent $OutputPrefix
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$etlPath = "$OutputPrefix.etl"
$txtPath = "$OutputPrefix.txt"
$statsPath = "$OutputPrefix.stats.txt"
$relayPath = "$OutputPrefix.relay.ndjson"
$harnessPath = "$OutputPrefix.harness.json"
Push-Location $repoRoot
try {
cmd.exe /c "pktmon stop >nul 2>nul" | Out-Null
cmd.exe /c "pktmon filter remove >nul 2>nul" | Out-Null
pktmon filter add HistRelay -i $SshHost -p 32568 -t TCP | Out-Null
# 0x00e intentionally omits 0x010 raw-packet bytes. Keep metadata only.
pktmon start --capture --comp nics --flags 0x00e --file-name $etlPath --log-mode circular --file-size 64 | Out-Host
try {
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\Run-DebianHistorianRelayCapture.ps1 `
-SshUser $SshUser `
-SshHost $SshHost `
-TargetHost $TargetHost `
-Scenario $Scenario `
-TagName $TagName `
-LookbackMinutes $LookbackMinutes `
-MaxRows $MaxRows `
-OutputPath $relayPath `
-HarnessOutputPath $harnessPath
$harnessExit = $LASTEXITCODE
}
finally {
pktmon stop | Out-Host
}
pktmon etl2txt $etlPath --out $txtPath --brief | Out-Host
pktmon etl2txt $etlPath --stats-only 2>&1 | Set-Content -Path $statsPath -Encoding UTF8
Get-Content -Path $statsPath | Out-Host
Remove-Item -LiteralPath $etlPath -Force -ErrorAction SilentlyContinue
cmd.exe /c "pktmon filter remove >nul 2>nul" | Out-Null
$summary = [pscustomobject]@{
Operation = "PktmonDebianRelayCapture"
Scenario = $Scenario
SshHost = $SshHost
TargetHost = $TargetHost
HarnessExitCode = $harnessExit
PayloadBytesCaptured = $false
PktmonText = $txtPath
PktmonStats = $statsPath
RelayTranscript = $relayPath
HarnessOutput = $harnessPath
RawEtlDeleted = -not (Test-Path -LiteralPath $etlPath)
}
$summary | ConvertTo-Json -Depth 4 | Set-Content -Path "$OutputPrefix.summary.json" -Encoding UTF8
exit $harnessExit
}
finally {
try {
cmd.exe /c "pktmon stop >nul 2>nul" | Out-Null
cmd.exe /c "pktmon filter remove >nul 2>nul" | Out-Null
}
catch {
}
Pop-Location
}
+23
View File
@@ -0,0 +1,23 @@
param(
[int]$Port = 33268
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
Push-Location $repoRoot
try {
Write-Host "Building WCF capture server..."
dotnet build .\tools\AVEVA.Historian.WcfCaptureServer\AVEVA.Historian.WcfCaptureServer.csproj
Write-Host ""
Write-Host "Starting capture server. Leave this window open."
Write-Host "Endpoint: net.tcp://localhost:$Port/Hist"
Write-Host "Captured Open2 metadata will print as JSON with length and SHA-256 only."
Write-Host ""
& .\tools\AVEVA.Historian.WcfCaptureServer\bin\Debug\net481\AVEVA.Historian.WcfCaptureServer.exe $Port
}
finally {
Pop-Location
}
+255
View File
@@ -0,0 +1,255 @@
param(
[string]$HostName = "localhost",
[UInt16]$Port = 32568,
[string]$UserName = $null,
[int]$ConnectionWaitSeconds = 15,
[switch]$IntegratedSecurity,
[switch]$UseArchestrAUser
)
$ErrorActionPreference = "Stop"
function ConvertTo-PlainText {
param([Security.SecureString]$SecureString)
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
try {
[Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
}
finally {
if ($bstr -ne [IntPtr]::Zero) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
function Set-PropertyIfPresent {
param(
[object]$Object,
[string]$Name,
[object]$Value
)
$property = $Object.GetType().GetProperty($Name)
if ($null -ne $property -and $property.CanWrite) {
$property.SetValue($Object, $Value, $null)
}
}
function Get-PropertyText {
param(
[object]$Object,
[string]$Name
)
if ($null -eq $Object) {
return $null
}
$property = $Object.GetType().GetProperty($Name)
if ($null -eq $property -or -not $property.CanRead) {
return $null
}
$value = $property.GetValue($Object, $null)
if ($null -eq $value) {
return $null
}
return $value.ToString()
}
function Get-PropertyValue {
param(
[object]$Object,
[string]$Name
)
if ($null -eq $Object) {
return $null
}
$property = $Object.GetType().GetProperty($Name)
if ($null -eq $property -or -not $property.CanRead) {
return $null
}
return $property.GetValue($Object, $null)
}
function Get-UserNameShape {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return "empty"
}
if ($Value.Contains("\")) {
return "domain-or-machine-qualified"
}
if ($Value.Contains("@")) {
return "upn"
}
return "unqualified"
}
$repoRoot = Split-Path -Parent $PSScriptRoot
$dllDir = Join-Path $repoRoot "current"
$managedDll = Join-Path $dllDir "aahClientManaged.dll"
if (-not (Test-Path -LiteralPath $managedDll)) {
throw "Missing aahClientManaged.dll at $managedDll"
}
if (-not $IntegratedSecurity -and [string]::IsNullOrWhiteSpace($UserName)) {
$defaultUser = "{0}\{1}" -f $env:COMPUTERNAME, $env:USERNAME
$entered = Read-Host "Historian user [$defaultUser]"
if ([string]::IsNullOrWhiteSpace($entered)) {
$UserName = $defaultUser
}
else {
$UserName = $entered
}
}
$plainPassword = $null
if (-not $IntegratedSecurity) {
$securePassword = Read-Host "Historian password" -AsSecureString
$plainPassword = ConvertTo-PlainText $securePassword
}
[AppDomain]::CurrentDomain.add_AssemblyResolve({
param($sender, $eventArgs)
$assemblyName = New-Object Reflection.AssemblyName($eventArgs.Name)
$candidate = Join-Path $script:dllDir ($assemblyName.Name + ".dll")
if (Test-Path -LiteralPath $candidate) {
return [Reflection.Assembly]::LoadFrom($candidate)
}
return $null
})
Push-Location $dllDir
try {
$assembly = [Reflection.Assembly]::LoadFrom($managedDll)
$accessType = $assembly.GetType("ArchestrA.HistorianAccess", $true)
$argsType = $assembly.GetType("ArchestrA.HistorianConnectionArgs", $true)
$statusType = $assembly.GetType("ArchestrA.HistorianConnectionStatus", $true)
$errorType = $assembly.GetType("ArchestrA.HistorianAccessError", $true)
$connectionType = $assembly.GetType("ArchestrA.HistorianConnectionType", $true)
$argsObject = [Activator]::CreateInstance($argsType)
Set-PropertyIfPresent $argsObject "ServerName" $HostName
Set-PropertyIfPresent $argsObject "TcpPort" $Port
Set-PropertyIfPresent $argsObject "ReadOnly" $true
Set-PropertyIfPresent $argsObject "IntegratedSecurity" ([bool]$IntegratedSecurity)
Set-PropertyIfPresent $argsObject "UseArchestrAUser" ([bool]$UseArchestrAUser)
Set-PropertyIfPresent $argsObject "ConnectionType" ([Enum]::Parse($connectionType, "Process"))
if (-not $IntegratedSecurity) {
Set-PropertyIfPresent $argsObject "UserName" $UserName
Set-PropertyIfPresent $argsObject "Password" $plainPassword
}
$access = [Activator]::CreateInstance($accessType)
$errorObject = [Activator]::CreateInstance($errorType)
$openParameterTypes = [Type[]]@($argsType, $errorType.MakeByRefType())
$openMethod = $accessType.GetMethod("OpenConnection", $openParameterTypes)
if ($null -eq $openMethod) {
throw "Could not find HistorianAccess.OpenConnection(HistorianConnectionArgs, HistorianAccessError ByRef)."
}
$invokeArgs = New-Object "object[]" 2
$invokeArgs[0] = $argsObject
$invokeArgs[1] = $errorObject
$success = $false
$exceptionText = $null
try {
$success = [bool]$openMethod.Invoke($access, $invokeArgs)
$errorObject = $invokeArgs[1]
}
catch [Reflection.TargetInvocationException] {
$inner = $_.Exception.InnerException
if ($null -ne $inner) {
$exceptionText = "{0}: {1}" -f $inner.GetType().FullName, $inner.Message
}
else {
$exceptionText = $_.Exception.Message
}
}
$connected = $false
$pending = $false
$statusErrorOccurred = $false
$statusError = $null
$getStatusMethod = $accessType.GetMethod("GetConnectionStatus", [Type[]]@($statusType.MakeByRefType()))
if ($null -ne $getStatusMethod) {
$deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($ConnectionWaitSeconds, 1))
do {
$statusObject = [Activator]::CreateInstance($statusType)
$statusArgs = New-Object "object[]" 1
$statusArgs[0] = $statusObject
[void]$getStatusMethod.Invoke($access, $statusArgs)
$statusObject = $statusArgs[0]
$connected = [bool](Get-PropertyValue $statusObject "ConnectedToServer")
$pending = [bool](Get-PropertyValue $statusObject "Pending")
$statusErrorOccurred = [bool](Get-PropertyValue $statusObject "ErrorOccurred")
$statusError = Get-PropertyValue $statusObject "Error"
if (($connected -and -not $pending) -or $statusErrorOccurred -or (-not $connected -and -not $pending)) {
break
}
Start-Sleep -Milliseconds 250
} while ([DateTime]::UtcNow -lt $deadline)
}
$result = [ordered]@{
Operation = "aahClientManaged.OpenConnection"
HostName = $HostName
Port = $Port
IntegratedSecurity = [bool]$IntegratedSecurity
UseArchestrAUser = [bool]$UseArchestrAUser
UserNameShape = Get-UserNameShape $UserName
Success = $success
ConnectedToServer = $connected
ConnectionPending = $pending
ConnectionErrorOccurred = $statusErrorOccurred
Exception = $exceptionText
ErrorType = Get-PropertyText $errorObject "ErrorType"
ErrorCode = Get-PropertyText $errorObject "ErrorCode"
ErrorDescription = Get-PropertyText $errorObject "ErrorDescription"
ErrorServerName = Get-PropertyText $errorObject "ServerName"
ConnectionStatusErrorType = Get-PropertyText $statusError "ErrorType"
ConnectionStatusErrorCode = Get-PropertyText $statusError "ErrorCode"
ConnectionStatusErrorDescription = Get-PropertyText $statusError "ErrorDescription"
}
$result | ConvertTo-Json -Depth 4
if ($success) {
$closeError = [Activator]::CreateInstance($errorType)
$closeParameterTypes = [Type[]]@($errorType.MakeByRefType())
$closeMethod = $accessType.GetMethod("CloseConnection", $closeParameterTypes)
if ($null -ne $closeMethod) {
$closeArgs = New-Object "object[]" 1
$closeArgs[0] = $closeError
[void]$closeMethod.Invoke($access, $closeArgs)
}
}
$dispose = $access -as [IDisposable]
if ($null -ne $dispose) {
$dispose.Dispose()
}
}
finally {
if ($null -ne $plainPassword) {
$plainPassword = $null
}
Pop-Location
}
@@ -0,0 +1,279 @@
param(
[string]$HostName = "localhost",
[UInt16]$Port = 32568,
[string]$TagName = $env:HISTORIAN_TEST_TAG,
[int]$LookbackMinutes = 60,
[int]$MaxRows = 5,
[int]$ConnectionWaitSeconds = 15,
[int]$PreReadSleepSeconds = 0,
[switch]$DumpLoadedModules
)
$ErrorActionPreference = "Stop"
function Set-PropertyIfPresent {
param(
[object]$Object,
[string]$Name,
[object]$Value
)
$property = $Object.GetType().GetProperty($Name)
if ($null -ne $property -and $property.CanWrite) {
$property.SetValue($Object, $Value, $null)
}
}
function Get-PropertyValue {
param(
[object]$Object,
[string]$Name
)
if ($null -eq $Object) {
return $null
}
$property = $Object.GetType().GetProperty($Name)
if ($null -eq $property -or -not $property.CanRead) {
return $null
}
return $property.GetValue($Object, $null)
}
function Get-PropertyText {
param(
[object]$Object,
[string]$Name
)
$value = Get-PropertyValue $Object $Name
if ($null -eq $value) {
return $null
}
return $value.ToString()
}
function Invoke-ByRefMethod {
param(
[object]$Target,
[Reflection.MethodInfo]$Method,
[object[]]$Arguments
)
return $Method.Invoke($Target, $Arguments)
}
if ([string]::IsNullOrWhiteSpace($TagName)) {
$TagName = Read-Host "Historian test tag"
}
if ([string]::IsNullOrWhiteSpace($TagName)) {
throw "A tag name is required. Pass -TagName or set HISTORIAN_TEST_TAG."
}
$repoRoot = Split-Path -Parent $PSScriptRoot
$dllDir = Join-Path $repoRoot "current"
$managedDll = Join-Path $dllDir "aahClientManaged.dll"
if (-not (Test-Path -LiteralPath $managedDll)) {
throw "Missing aahClientManaged.dll at $managedDll"
}
[AppDomain]::CurrentDomain.add_AssemblyResolve({
param($sender, $eventArgs)
$assemblyName = New-Object Reflection.AssemblyName($eventArgs.Name)
$candidate = Join-Path $script:dllDir ($assemblyName.Name + ".dll")
if (Test-Path -LiteralPath $candidate) {
return [Reflection.Assembly]::LoadFrom($candidate)
}
return $null
})
Push-Location $dllDir
try {
$assembly = [Reflection.Assembly]::LoadFrom($managedDll)
if ($DumpLoadedModules) {
[Diagnostics.Process]::GetCurrentProcess().Modules |
Where-Object { $_.ModuleName -like '*aah*' -or $_.FileName -like '*histsdk*' -or $_.FileName -like '*AVEVA*' } |
Select-Object ModuleName, BaseAddress, ModuleMemorySize, FileName |
ConvertTo-Json -Depth 3
}
if ($PreReadSleepSeconds -gt 0) {
Start-Sleep -Seconds $PreReadSleepSeconds
}
$accessType = $assembly.GetType("ArchestrA.HistorianAccess", $true)
$connectionArgsType = $assembly.GetType("ArchestrA.HistorianConnectionArgs", $true)
$connectionStatusType = $assembly.GetType("ArchestrA.HistorianConnectionStatus", $true)
$connectionType = $assembly.GetType("ArchestrA.HistorianConnectionType", $true)
$historyQueryArgsType = $assembly.GetType("ArchestrA.HistoryQueryArgs", $true)
$errorType = $assembly.GetType("ArchestrA.HistorianAccessError", $true)
$retrievalModeType = $assembly.GetType("ArchestrA.HistorianRetrievalMode", $true)
$access = [Activator]::CreateInstance($accessType)
$connectionArgs = [Activator]::CreateInstance($connectionArgsType)
Set-PropertyIfPresent $connectionArgs "ServerName" $HostName
Set-PropertyIfPresent $connectionArgs "TcpPort" $Port
Set-PropertyIfPresent $connectionArgs "ReadOnly" $true
Set-PropertyIfPresent $connectionArgs "IntegratedSecurity" $true
Set-PropertyIfPresent $connectionArgs "ConnectionType" ([Enum]::Parse($connectionType, "Process"))
$openError = [Activator]::CreateInstance($errorType)
$openMethod = $accessType.GetMethod("OpenConnection", [Type[]]@($connectionArgsType, $errorType.MakeByRefType()))
$openArgs = New-Object "object[]" 2
$openArgs[0] = $connectionArgs
$openArgs[1] = $openError
$openSuccess = [bool](Invoke-ByRefMethod $access $openMethod $openArgs)
$openError = $openArgs[1]
$connected = $false
$pending = $false
$connectionErrorOccurred = $false
$connectionStatusError = $null
$getStatusMethod = $accessType.GetMethod("GetConnectionStatus", [Type[]]@($connectionStatusType.MakeByRefType()))
$deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($ConnectionWaitSeconds, 1))
do {
$status = [Activator]::CreateInstance($connectionStatusType)
$statusArgs = New-Object "object[]" 1
$statusArgs[0] = $status
[void](Invoke-ByRefMethod $access $getStatusMethod $statusArgs)
$status = $statusArgs[0]
$connected = [bool](Get-PropertyValue $status "ConnectedToServer")
$pending = [bool](Get-PropertyValue $status "Pending")
$connectionErrorOccurred = [bool](Get-PropertyValue $status "ErrorOccurred")
$connectionStatusError = Get-PropertyValue $status "Error"
if (($connected -and -not $pending) -or $connectionErrorOccurred -or (-not $connected -and -not $pending)) {
break
}
Start-Sleep -Milliseconds 250
} while ([DateTime]::UtcNow -lt $deadline)
$rows = New-Object System.Collections.Generic.List[object]
$startSuccess = $false
$moveErrorText = $null
$startError = $null
if ($openSuccess -and $connected) {
$query = $accessType.GetMethod("CreateHistoryQuery", [Type[]]@()).Invoke($access, @())
$queryType = $query.GetType()
$queryArgs = [Activator]::CreateInstance($historyQueryArgsType)
$tags = New-Object System.Collections.Specialized.StringCollection
[void]$tags.Add($TagName)
Set-PropertyIfPresent $queryArgs "TagNames" $tags
Set-PropertyIfPresent $queryArgs "StartDateTime" ([DateTime]::Now.AddMinutes(-1 * $LookbackMinutes))
Set-PropertyIfPresent $queryArgs "EndDateTime" ([DateTime]::Now)
Set-PropertyIfPresent $queryArgs "BatchSize" ([uint32][Math]::Max($MaxRows, 1))
Set-PropertyIfPresent $queryArgs "RetrievalMode" ([Enum]::Parse($retrievalModeType, "Full"))
$queryError = [Activator]::CreateInstance($errorType)
$startMethod = $queryType.GetMethod("StartQuery", [Type[]]@($historyQueryArgsType, $errorType.MakeByRefType()))
$startArgs = New-Object "object[]" 2
$startArgs[0] = $queryArgs
$startArgs[1] = $queryError
$startSuccess = [bool](Invoke-ByRefMethod $query $startMethod $startArgs)
$startError = $startArgs[1]
if ($startSuccess) {
$moveMethod = $queryType.GetMethod("MoveNext", [Type[]]@($errorType.MakeByRefType()))
for ($i = 0; $i -lt $MaxRows; $i++) {
$moveError = [Activator]::CreateInstance($errorType)
$moveArgs = New-Object "object[]" 1
$moveArgs[0] = $moveError
$hasRow = [bool](Invoke-ByRefMethod $query $moveMethod $moveArgs)
$moveError = $moveArgs[0]
if (-not $hasRow) {
$moveErrorText = Get-PropertyText $moveError "ErrorDescription"
break
}
$result = Get-PropertyValue $query "QueryResult"
$row = [ordered]@{
StartDateTime = (Get-PropertyValue $result "StartDateTime").ToString("O")
EndDateTime = (Get-PropertyValue $result "EndDateTime").ToString("O")
Quality = Get-PropertyValue $result "Quality"
OpcQuality = Get-PropertyValue $result "OpcQuality"
QualityDetail = Get-PropertyValue $result "QualityDetail"
Value = Get-PropertyValue $result "Value"
StringValuePresent = -not [string]::IsNullOrEmpty((Get-PropertyText $result "StringValue"))
PercentGood = Get-PropertyValue $result "PercentGood"
}
$rows.Add($row)
}
}
$endMethod = $queryType.GetMethod("EndQuery", [Type[]]@($errorType.MakeByRefType()))
if ($null -ne $endMethod) {
$endError = [Activator]::CreateInstance($errorType)
$endArgs = New-Object "object[]" 1
$endArgs[0] = $endError
[void](Invoke-ByRefMethod $query $endMethod $endArgs)
}
$queryDispose = $query -as [IDisposable]
if ($null -ne $queryDispose) {
$queryDispose.Dispose()
}
}
$resultObject = [ordered]@{
Operation = "aahClientManaged.HistoryQuery.Integrated"
HostName = $HostName
Port = $Port
IntegratedSecurity = $true
TagNameProvided = $true
LookbackMinutes = $LookbackMinutes
OpenSuccess = $openSuccess
OpenErrorType = Get-PropertyText $openError "ErrorType"
OpenErrorCode = Get-PropertyText $openError "ErrorCode"
OpenErrorDescription = Get-PropertyText $openError "ErrorDescription"
ConnectedToServer = $connected
ConnectionPending = $pending
ConnectionErrorOccurred = $connectionErrorOccurred
ConnectionStatusErrorType = Get-PropertyText $connectionStatusError "ErrorType"
ConnectionStatusErrorCode = Get-PropertyText $connectionStatusError "ErrorCode"
ConnectionStatusErrorDescription = Get-PropertyText $connectionStatusError "ErrorDescription"
StartQuerySuccess = $startSuccess
StartQueryErrorType = Get-PropertyText $startError "ErrorType"
StartQueryErrorCode = Get-PropertyText $startError "ErrorCode"
StartQueryErrorDescription = Get-PropertyText $startError "ErrorDescription"
MoveTerminalDescription = $moveErrorText
RowCount = $rows.Count
Rows = $rows
}
$resultObject | ConvertTo-Json -Depth 6
if ($DumpLoadedModules) {
[Diagnostics.Process]::GetCurrentProcess().Modules |
Where-Object { $_.ModuleName -like '*aah*' -or $_.FileName -like '*histsdk*' -or $_.FileName -like '*AVEVA*' } |
Select-Object ModuleName, BaseAddress, ModuleMemorySize, FileName |
ConvertTo-Json -Depth 3
}
if ($openSuccess) {
$closeError = [Activator]::CreateInstance($errorType)
$closeMethod = $accessType.GetMethod("CloseConnection", [Type[]]@($errorType.MakeByRefType()))
if ($null -ne $closeMethod) {
$closeArgs = New-Object "object[]" 1
$closeArgs[0] = $closeError
[void](Invoke-ByRefMethod $access $closeMethod $closeArgs)
}
}
$dispose = $access -as [IDisposable]
if ($null -ne $dispose) {
$dispose.Dispose()
}
}
finally {
Pop-Location
}
+81
View File
@@ -0,0 +1,81 @@
"""Inventory the writemessage event-flow capture: action URI + length + first bytes."""
import base64
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CAPTURE = REPO_ROOT / "artifacts" / "reverse-engineering" / "instrumented-wcf-writemessage" / "writemessage-capture-event-latest.ndjson"
# Match aa/<service>/<op> where service is Hist|Retr|Trx|Stor and op is alphanumeric chars.
ACTION_RE = re.compile(rb"aa/(?:Hist|Retr|Trx|Stor)/[A-Za-z0-9]+")
GUID_RE = re.compile(rb"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}")
PARAM_NAME_RE = re.compile(rb"[\x20-\x7E]{4,}")
def first_action(body: bytes) -> str:
m = ACTION_RE.search(body)
return m.group(0).decode("ascii") if m else "<no-action-found>"
def all_guids(body: bytes) -> list[str]:
return [g.decode() for g in GUID_RE.findall(body)]
def all_param_names(body: bytes) -> list[str]:
# Look for short ASCII runs that aren't the action URI / endpoint URL.
names = []
for m in PARAM_NAME_RE.finditer(body):
s = m.group(0).decode("ascii")
if any(skip in s for skip in ("aa/", "net.pipe://", "AVEVA", "DESKTOP-", "WORKGROUP", "NTLMSSP")):
continue
if 3 <= len(s) <= 40 and s.isprintable():
names.append(s)
return names
def main() -> int:
print(f"# Inventory of {CAPTURE.name}")
records = []
with CAPTURE.open(encoding="utf-8-sig") as fh:
for idx, line in enumerate(fh):
rec = json.loads(line)
body = base64.b64decode(rec["Base64"])
records.append((idx, rec, body))
# Pass 1: action URIs and lengths.
print()
print(f"{'#':>3} {'Length':>6} {'Action':<40} {'GUIDs (first 2)'}")
print("-" * 110)
for idx, rec, body in records:
action = first_action(body)
guids = all_guids(body)
guid_summary = ", ".join(guids[:2]) if guids else ""
print(f"{idx:>3} {rec['Length']:>6} {action:<40} {guid_summary}")
# Pass 2: detailed dump for the unknown records.
UNKNOWN = {6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 18, 20}
print()
print("# Detailed dump of unknown records (action + param names + first 96 bytes hex)")
for idx, rec, body in records:
if idx not in UNKNOWN:
continue
action = first_action(body)
params = all_param_names(body)
print()
print(f"=== Record {idx} (length={rec['Length']}, action={action}) ===")
print(f" Param-ish strings: {params}")
print(f" GUIDs found : {all_guids(body)}")
# First 128 bytes of hex split into rows of 32.
for off in range(0, min(160, len(body)), 32):
chunk = body[off:off + 32]
hex_part = " ".join(f"{b:02X}" for b in chunk)
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
print(f" {off:04X} {hex_part:<96} |{ascii_part}|")
return 0
if __name__ == "__main__":
sys.exit(main())
+71
View File
@@ -0,0 +1,71 @@
"""Decode ReadMessage capture (incoming WCF response bodies)."""
import base64
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CAPTURE = REPO_ROOT / "artifacts" / "reverse-engineering" / "instrumented-wcf-readmessage" / "readmessage-capture-event-latest.ndjson"
# WCF response bodies don't carry the action URI in the body itself (the request action is
# echoed differently). Look for known parameter names instead. WCF response wraps the result
# in <{OpName}Response> with parameter elements inside.
RESPONSE_NAME_RE = re.compile(rb"[A-Za-z][A-Za-z0-9]+Response")
GUID_RE = re.compile(rb"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}")
PARAM_RE = re.compile(rb"[\x20-\x7E]{4,}")
def main() -> int:
records = []
with CAPTURE.open(encoding="utf-8-sig") as fh:
for line in fh:
records.append(json.loads(line))
print(f"# {CAPTURE.name}: {len(records)} records")
print()
print(f"{'#':>3} {'Length':>6} {'Sha8':<10} {'Operation':<32} {'GUIDs(first2)'}")
print("-" * 120)
for idx, rec in enumerate(records):
body = base64.b64decode(rec["Base64"])
op_match = RESPONSE_NAME_RE.search(body)
op = op_match.group(0).decode() if op_match else "<no-Response-name>"
guids = [g.decode() for g in GUID_RE.findall(body)]
guid_summary = ", ".join(guids[:2]) if guids else ""
sha8 = rec["Sha256"][:8]
print(f"{idx:>3} {rec['Length']:>6} {sha8:<10} {op:<32} {guid_summary}")
# Detailed dump for the most interesting records.
INTEREST = {
"StartEventQuery": "after request to /Retr/StartEventQuery",
"GetNextEventQueryResultBuffer": "the event-row body we want",
"Open2": "session-establishing response",
"EnsT2": "what the server returns for EnsureTags2",
"EnsureTags2": "EnsT2 alt name",
"RTag2": "RegisterTags2 response",
"RegisterTags2": "RTag2 alt name",
"UpdC3": "UpdateClientStatus3 response",
"UpdateClientStatus3": "UpdC3 alt name",
}
print()
print("# Detailed dumps")
for idx, rec in enumerate(records):
body = base64.b64decode(rec["Base64"])
op_match = RESPONSE_NAME_RE.search(body)
if not op_match:
continue
op = op_match.group(0).decode().removesuffix("Response")
if not any(k in op for k in INTEREST):
continue
print()
print(f"=== Record {idx} {op} (length={rec['Length']}) ===")
for off in range(0, min(256, len(body)), 32):
chunk = body[off:off + 32]
hp = " ".join(f"{b:02X}" for b in chunk)
ap = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
print(f" {off:04X} {hp:<96} |{ap}|")
return 0
if __name__ == "__main__":
sys.exit(main())
+215
View File
@@ -0,0 +1,215 @@
'use strict';
const moduleName = 'aahClient.dll';
const maxDumpBytes = 256;
const interestingExportFragments = [
'mdas_OpenConnection',
'mdas_CloseConnection',
'mdas_StartDataRetrievalQuery',
'mdas_GetNextDataQueryResult',
'mdas_StartEventDataRetrievalQuery',
'mdas_GetNextEventDataQueryResult',
'mdas_StartBlockRetrievalQuery',
'mdas_GetNextBlockQueryResult',
'mdas_EndQuery'
];
let hooksInstalled = false;
function emit(kind, payload) {
payload.kind = kind;
payload.pid = Process.id;
payload.tid = Process.getCurrentThreadId();
payload.timestamp = new Date().toISOString();
console.log('FRIDA_EVENT ' + JSON.stringify(payload));
}
function toHex(bytes) {
const parts = [];
for (let i = 0; i < bytes.length; i++) {
const text = bytes[i].toString(16);
parts.push(text.length === 1 ? '0' + text : text);
}
return parts.join('');
}
function asciiPreview(bytes) {
let text = '';
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i];
text += b >= 32 && b <= 126 ? String.fromCharCode(b) : '.';
}
return text;
}
function utf16Preview(bytes) {
let text = '';
const count = Math.min(bytes.length - (bytes.length % 2), 128);
for (let i = 0; i < count; i += 2) {
const code = bytes[i] | (bytes[i + 1] << 8);
text += code >= 32 && code <= 126 ? String.fromCharCode(code) : '.';
}
return text;
}
function isReadablePointer(value) {
if (value.isNull() || value.compare(ptr('0x10000')) < 0) {
return false;
}
const range = Process.findRangeByAddress(value);
return range !== null && range.protection.indexOf('r') !== -1;
}
function dumpPointer(value) {
const range = Process.findRangeByAddress(value);
const available = range === null ? 0 : Math.min(maxDumpBytes, range.base.add(range.size).sub(value).toNumber());
if (available <= 0) {
return null;
}
const raw = Memory.readByteArray(value, available);
if (raw === null) {
return null;
}
const bytes = Array.from(new Uint8Array(raw));
return {
address: value.toString(),
rangeBase: range.base.toString(),
rangeSize: range.size,
protection: range.protection,
byteCount: bytes.length,
hexPrefix: toHex(bytes.slice(0, Math.min(bytes.length, 128))),
asciiPrefix: asciiPreview(bytes.slice(0, Math.min(bytes.length, 128))),
utf16Prefix: utf16Preview(bytes.slice(0, Math.min(bytes.length, 256)))
};
}
function inspectArgs(args, count) {
const inspected = [];
for (let i = 0; i < count; i++) {
const value = args[i];
const item = {
index: i,
value: value.toString()
};
if (isReadablePointer(value)) {
try {
item.memory = dumpPointer(value);
} catch (e) {
item.memoryError = String(e);
}
}
inspected.push(item);
}
return inspected;
}
function isInterestingExport(name) {
for (const fragment of interestingExportFragments) {
if (name.indexOf(fragment) !== -1) {
return true;
}
}
return false;
}
function installHooks() {
if (hooksInstalled) {
return true;
}
let module = null;
try {
module = Process.getModuleByName(moduleName);
} catch (e) {
return false;
}
hooksInstalled = true;
const exports = module.enumerateExports().filter(e => e.type === 'function' && isInterestingExport(e.name));
emit('module-loaded', {
module: module.name,
base: module.base.toString(),
size: module.size,
path: module.path,
exportCount: exports.length,
exports: exports.map(e => ({ name: e.name, address: e.address.toString() }))
});
for (const exported of exports) {
try {
Interceptor.attach(exported.address, {
onEnter(args) {
this.name = exported.name;
this.address = exported.address.toString();
this.argsSnapshot = inspectArgs(args, 18);
emit('enter', {
function: this.name,
address: this.address,
args: this.argsSnapshot
});
},
onLeave(retval) {
emit('leave', {
function: this.name,
address: this.address,
retval: retval.toString(),
args: this.argsSnapshot
});
}
});
emit('hooked', { function: exported.name, address: exported.address.toString() });
} catch (e) {
emit('hook-error', { function: exported.name, address: exported.address.toString(), error: String(e) });
}
}
return true;
}
function isInterestingModule(module) {
const name = module.name.toLowerCase();
const path = module.path.toLowerCase();
return name.indexOf('aah') !== -1 || path.indexOf('histsdk') !== -1 || path.indexOf('aveva') !== -1;
}
emit('startup', {
arch: Process.arch,
platform: Process.platform,
modules: Process.enumerateModules()
.filter(isInterestingModule)
.map(m => ({ name: m.name, base: m.base.toString(), size: m.size, path: m.path }))
});
try {
Process.attachModuleObserver({
onAdded(module) {
if (isInterestingModule(module)) {
emit('module-added', {
name: module.name,
base: module.base.toString(),
size: module.size,
path: module.path
});
}
installHooks();
}
});
} catch (e) {
emit('module-observer-error', { error: String(e) });
}
if (!installHooks()) {
const timer = setInterval(() => {
if (installHooks()) {
clearInterval(timer);
}
}, 50);
}
@@ -0,0 +1,231 @@
// Sanitized server-side AVEVA Historian ValCl context probe.
// Logs pointers, GUID bytes, token lengths, round flags, and return values only.
'use strict';
const moduleName = 'aahClientAccessPoint.exe';
const imageBase = ptr('0x00400000');
const moduleBase = Module.findBaseAddress(moduleName);
function emit(event) {
event.timestampUtc = new Date().toISOString();
console.log(JSON.stringify(event));
}
function addrFromVa(va) {
if (moduleBase === null) {
return null;
}
return moduleBase.add(ptr(va).sub(imageBase));
}
function safePtr(value) {
if (value === null || value === undefined) {
return null;
}
try {
return ptr(value).toString();
} catch (_) {
return null;
}
}
function readU8(pointer) {
try {
if (pointer.isNull()) return null;
return pointer.readU8();
} catch (_) {
return null;
}
}
function readU32(pointer) {
try {
if (pointer.isNull()) return null;
return pointer.readU32();
} catch (_) {
return null;
}
}
function readGuid(pointer) {
try {
if (pointer.isNull()) return null;
const bytes = pointer.readByteArray(16);
if (bytes === null) return null;
return Array.prototype.map.call(new Uint8Array(bytes), b => ('0' + b.toString(16)).slice(-2)).join('');
} catch (_) {
return null;
}
}
function readServerBufferSummary(buffer) {
try {
if (buffer.isNull()) {
return { buffer: safePtr(buffer) };
}
const data = buffer.add(0x48).readPointer();
const length = buffer.add(0x4c).readU32();
let roundByte = null;
let wrappedTokenLength = null;
if (!data.isNull() && length >= 5) {
roundByte = readU8(data);
wrappedTokenLength = readU32(data.add(1));
}
return {
buffer: safePtr(buffer),
data: safePtr(data),
length: length,
roundByte: roundByte,
wrappedTokenLength: wrappedTokenLength
};
} catch (error) {
return {
buffer: safePtr(buffer),
readError: String(error)
};
}
}
function hook(name, va, callbacks) {
const address = addrFromVa(va);
if (address === null) {
emit({ event: 'hook.error', name: name, reason: 'module-not-loaded', module: moduleName });
return;
}
try {
Interceptor.attach(address, callbacks);
emit({ event: 'hook.installed', name: name, va: va, address: address.toString() });
} catch (error) {
emit({ event: 'hook.error', name: name, va: va, address: address.toString(), reason: String(error) });
}
}
emit({
event: 'script.loaded',
module: moduleName,
moduleBase: moduleBase === null ? null : moduleBase.toString()
});
hook('CServerNode.ProcessServerToken', '0x00526E00', {
onEnter(args) {
this.thisPtr = this.context.ecx;
this.contextGuid = args[0];
this.serverBuffer = args[1];
this.continuePtr = args[2];
this.errorPtr = args[3];
emit({
event: 'ProcessServerToken.enter',
thisPtr: safePtr(this.thisPtr),
contextGuidPtr: safePtr(this.contextGuid),
contextGuidBytes: readGuid(this.contextGuid),
serverBuffer: readServerBufferSummary(this.serverBuffer),
continuePtr: safePtr(this.continuePtr),
errorPtr: safePtr(this.errorPtr)
});
},
onLeave(retval) {
emit({
event: 'ProcessServerToken.leave',
retval: retval.toInt32(),
continueValue: readU8(this.continuePtr)
});
}
});
hook('ContextSetup.0050FFC0', '0x0050FFC0', {
onEnter(args) {
this.thisPtr = this.context.ecx;
this.contextGuid = args[0];
emit({
event: 'ContextSetup.enter',
thisPtr: safePtr(this.thisPtr),
contextGuidPtr: safePtr(this.contextGuid),
contextGuidBytes: readGuid(this.contextGuid)
});
},
onLeave(retval) {
emit({ event: 'ContextSetup.leave', retval: retval.toInt32() });
}
});
hook('ContextLookup.00517AB0', '0x00517AB0', {
onEnter(args) {
this.thisPtr = this.context.ecx;
this.outPair = args[0];
this.contextGuid = args[1];
emit({
event: 'ContextLookup.enter',
thisPtr: safePtr(this.thisPtr),
outPair: safePtr(this.outPair),
contextGuidPtr: safePtr(this.contextGuid),
contextGuidBytes: readGuid(this.contextGuid)
});
},
onLeave(retval) {
let first = null;
let second = null;
try {
if (!this.outPair.isNull()) {
first = this.outPair.readPointer();
second = this.outPair.add(Process.pointerSize).readPointer();
}
} catch (_) {
first = null;
second = null;
}
emit({
event: 'ContextLookup.leave',
retval: safePtr(retval),
contextObject: safePtr(first),
contextSharedState: safePtr(second)
});
}
});
hook('AcquireCredentialsHelper.00505AE0', '0x00505AE0', {
onEnter(args) {
this.thisPtr = this.context.ecx;
this.errorPtr = args[0];
emit({
event: 'AcquireCredentialsHelper.enter',
contextObject: safePtr(this.thisPtr),
errorPtr: safePtr(this.errorPtr)
});
},
onLeave(retval) {
emit({ event: 'AcquireCredentialsHelper.leave', retval: retval.toInt32() });
}
});
hook('AcceptSecurityContextHelper.00505C00', '0x00505C00', {
onEnter(args) {
this.thisPtr = this.context.ecx;
this.firstRound = args[0].toInt32() & 0xff;
this.tokenLength = args[1].toUInt32();
this.tokenPtr = args[2];
this.continuePtr = args[3];
this.serverCredentialPtr = args[4];
this.errorPtr = args[5];
emit({
event: 'AcceptSecurityContextHelper.enter',
contextObject: safePtr(this.thisPtr),
firstRound: this.firstRound,
tokenLength: this.tokenLength,
tokenPtr: safePtr(this.tokenPtr),
continuePtr: safePtr(this.continuePtr),
serverCredentialPtr: safePtr(this.serverCredentialPtr),
errorPtr: safePtr(this.errorPtr)
});
},
onLeave(retval) {
emit({
event: 'AcceptSecurityContextHelper.leave',
retval: retval.toInt32(),
continueValue: readU8(this.continuePtr)
});
}
});
@@ -0,0 +1,318 @@
'use strict';
const moduleName = 'aahClientManaged.dll';
const knownModulePaths = [
moduleName,
'C:\\Users\\dohertj2\\Desktop\\histsdk\\current\\aahClientManaged.dll'
];
const maxDumpBytes = 256;
const targets = [
['CServiceUtility.SaveOpenConnectionParams', 0x2f0d54],
['CClientInfo.SerializeOpenConnectionInParams', 0x2f0928],
['CClientInfo.SerializeOpenConnectionInParams2', 0x2f0690],
['CClientInfo.SerializeOpenConnectionInParams2Content', 0x2f03a4],
['CClientInfo.SerializeOpenConnectionInParams3', 0x2f00c0],
['CClientInfo.SerializeOpenConnectionInParams3Content', 0x2efdac],
['CClientInfo.SerializeOpenConnectionInParams4', 0x2f2e88],
['CClientInfo.EncryptWithClientKey', 0x0],
['CHistoryConnectionWCF.GetClientKey', 0x2f3dc4],
['CHistoryConnectionWCF.OpenConnection', 0x2ec9c8],
['CHistoryConnectionWCF.OpenConnection2', 0x2fdeb8],
['CHistoryConnectionWCF.OpenConnection3', 0x2fedb4],
['CHistoryConnectionWCF.RegisterTags', 0x2f6f78],
['CHistoryConnectionWCF.ValidateClientCredential', 0x302e90],
['HistorianClient.OpenConnection', 0x4170e8],
['HistorianAccess.AddTagInternal', 0x43be68],
['HistorianAccess.CreateDefaultEventTag', 0x43c2d4],
['HistorianClient.AddHistorianTag', 0x417c18],
['HistorianClient.ConvertEventTagToTagMetadata', 0x417b68],
['HistorianClient.StartQuery', 0x415bbc],
['HistorianClient.StartEventQuery', 0x41811c],
['HistorianClient.StartDataQuery', 0x4160c4],
['CTagMetadata.Save<SByteStream<SCrtMemFile>>', 0x1044dc],
['ClientApp.StartDataQuery', 0x400f9c],
['ClientApp.StartEventQuery', 0x4015a4],
['Query.StartDataQuery', 0x41cacc],
['CRetrievalConnectionWCF.StartQuery2', 0x36eb48],
['CRetrievalConnectionWCF.StartEventQuery', 0x370324],
['HistoryQuery.StartQuery', 0x44012c],
['EventQuery.StartQuery', 0x43035c],
['Query.StartEventQuery', 0x41db4c],
['EventQueryFilters.Save<SCrtMemFile>', 0x41d38c],
['EventQueryRequest.Save<SCrtMemFile>', 0x41d48c],
['QueryColumnSelector.ctor.default', 0x1b94c],
['QueryColumnSelector.SelectNonSummaryColumns', 0x1ee34],
['QueryColumnSelector.Save<SCrtMemFile>', 0x41b8d8],
['QueryColumnSelector.GetColumnSelectorFlags', 0x41e110],
['HistorianClient.GetNextRow<DataQueryResultRow>', 0x42f818],
['DataQueryResultBuffer.GetNextRow<SByteStream<SCrtMemFile>>', 0x42f6f8],
['Query.GetNextRow', 0x42f744],
['HistorianClient.GetNextRow<EventQueryResultRow>', 0x430e10],
['EventQueryResultBuffer.GetNextRow<SByteStream<SCrtMemFile>>', 0x430a3c],
['Event.Query.GetNextRow', 0x430af4]
].filter(t => t[1] !== 0);
let startupLogged = false;
let hooksInstalled = false;
let getModuleHandleW = null;
function emit(kind, payload) {
payload.kind = kind;
payload.pid = Process.id;
payload.tid = Process.getCurrentThreadId();
payload.timestamp = new Date().toISOString();
console.log('FRIDA_EVENT ' + JSON.stringify(payload));
}
function isReadablePointer(value) {
if (value.isNull()) {
return false;
}
if (value.compare(ptr('0x10000')) < 0) {
return false;
}
const range = Process.findRangeByAddress(value);
return range !== null && range.protection.indexOf('r') !== -1;
}
function toHex(bytes) {
const parts = [];
for (let i = 0; i < bytes.length; i++) {
const text = bytes[i].toString(16);
parts.push(text.length === 1 ? '0' + text : text);
}
return parts.join('');
}
function asciiPreview(bytes) {
let text = '';
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i];
text += b >= 32 && b <= 126 ? String.fromCharCode(b) : '.';
}
return text;
}
function utf16Preview(bytes) {
let text = '';
const count = Math.min(bytes.length - (bytes.length % 2), 96);
for (let i = 0; i < count; i += 2) {
const code = bytes[i] | (bytes[i + 1] << 8);
text += code >= 32 && code <= 126 ? String.fromCharCode(code) : '.';
}
return text;
}
function dumpPointer(value) {
const range = Process.findRangeByAddress(value);
const available = range === null ? 0 : Math.min(maxDumpBytes, range.base.add(range.size).sub(value).toNumber());
if (available <= 0) {
return null;
}
const raw = Memory.readByteArray(value, available);
if (raw === null) {
return null;
}
const bytes = Array.from(new Uint8Array(raw));
return {
address: value.toString(),
rangeBase: range.base.toString(),
rangeSize: range.size,
protection: range.protection,
byteCount: bytes.length,
hexPrefix: toHex(bytes.slice(0, Math.min(bytes.length, 96))),
asciiPrefix: asciiPreview(bytes.slice(0, Math.min(bytes.length, 96))),
utf16Prefix: utf16Preview(bytes.slice(0, Math.min(bytes.length, 192)))
};
}
function inspectArgs(args, count) {
const inspected = [];
for (let i = 0; i < count; i++) {
const value = args[i];
const item = {
index: i,
value: value.toString()
};
if (isReadablePointer(value)) {
try {
item.memory = dumpPointer(value);
} catch (e) {
item.memoryError = String(e);
}
}
inspected.push(item);
}
return inspected;
}
function hookTarget(base, name, rva) {
const address = base.add(rva);
try {
Interceptor.attach(address, {
onEnter(args) {
this.name = name;
this.argsSnapshot = inspectArgs(args, 10);
emit('enter', {
function: name,
rva: '0x' + rva.toString(16),
address: address.toString(),
args: this.argsSnapshot
});
},
onLeave(retval) {
emit('leave', {
function: this.name,
retval: retval.toString(),
args: this.argsSnapshot
});
}
});
emit('hooked', { function: name, rva: '0x' + rva.toString(16), address: address.toString() });
} catch (e) {
emit('hook-error', { function: name, rva: '0x' + rva.toString(16), address: address.toString(), error: String(e) });
}
}
function installHooks() {
if (!startupLogged) {
startupLogged = true;
emit('startup', {
arch: Process.arch,
platform: Process.platform,
modules: Process.enumerateModules()
.filter(m => m.name.toLowerCase().indexOf('aah') !== -1 || m.path.toLowerCase().indexOf('histsdk') !== -1)
.map(m => ({ name: m.name, base: m.base.toString(), size: m.size, path: m.path }))
});
}
const modules = Process.enumerateModules().filter(m => m.name.toLowerCase() === moduleName.toLowerCase());
let module = modules.length > 0 ? modules[0] : null;
let base = module === null ? null : module.base;
if (base === null && getModuleHandleW !== null) {
for (const candidate of knownModulePaths) {
const handle = getModuleHandleW(Memory.allocUtf16String(candidate));
if (!handle.isNull()) {
base = handle;
emit('module-handle-found', { module: candidate, base: base.toString() });
break;
}
}
}
if (base === null) {
return false;
}
if (hooksInstalled) {
return true;
}
hooksInstalled = true;
emit('module-loaded', { module: moduleName, base: base.toString(), arch: Process.arch, platform: Process.platform });
for (const [name, rva] of targets) {
hookTarget(base, name, rva);
}
return true;
}
function isInterestingModule(module) {
const name = module.name.toLowerCase();
const path = module.path.toLowerCase();
return name.indexOf('aah') !== -1
|| name.indexOf('historian') !== -1
|| path.indexOf('histsdk') !== -1
|| path.indexOf('aveva') !== -1;
}
try {
Process.attachModuleObserver({
onAdded(module) {
if (isInterestingModule(module)) {
emit('module-added', {
name: module.name,
base: module.base.toString(),
size: module.size,
path: module.path
});
}
installHooks();
}
});
} catch (e) {
emit('module-observer-error', { error: String(e) });
}
try {
const kernel32 = Process.getModuleByName('kernel32.dll');
getModuleHandleW = new NativeFunction(kernel32.getExportByName('GetModuleHandleW'), 'pointer', ['pointer']);
const hookLoader = (exportName, pathArgIndex) => {
const fn = kernel32.getExportByName(exportName);
Interceptor.attach(fn, {
onEnter(args) {
this.exportName = exportName;
this.path = args[pathArgIndex].isNull() ? '' : args[pathArgIndex].readUtf16String();
},
onLeave(retval) {
const lower = (this.path || '').toLowerCase();
if (lower.indexOf('aah') !== -1 || lower.indexOf('historian') !== -1 || lower.indexOf('histsdk') !== -1 || lower.indexOf('aveva') !== -1) {
emit('load-library', { api: this.exportName, path: this.path, result: retval.toString() });
}
installHooks();
}
});
};
hookLoader('LoadLibraryW', 0);
hookLoader('LoadLibraryExW', 0);
} catch (e) {
emit('load-library-hook-error', { error: String(e) });
}
try {
const ntdll = Process.getModuleByName('ntdll.dll');
const ldrLoadDll = ntdll.getExportByName('LdrLoadDll');
Interceptor.attach(ldrLoadDll, {
onEnter(args) {
this.path = '';
try {
const unicodeString = args[2];
const length = unicodeString.readU16();
const buffer = unicodeString.add(Process.pointerSize * 2).readPointer();
if (!buffer.isNull() && length > 0) {
this.path = buffer.readUtf16String(length / 2);
}
} catch (e) {
this.path = '<read-error:' + String(e) + '>';
}
},
onLeave(retval) {
const lower = (this.path || '').toLowerCase();
if (lower.indexOf('aah') !== -1 || lower.indexOf('historian') !== -1 || lower.indexOf('histsdk') !== -1 || lower.indexOf('aveva') !== -1) {
emit('ldr-load-dll', { path: this.path, result: retval.toString() });
}
installHooks();
}
});
} catch (e) {
emit('ldr-load-dll-hook-error', { error: String(e) });
}
if (!installHooks()) {
const timer = setInterval(() => {
if (installHooks()) {
clearInterval(timer);
}
}, 50);
}
@@ -0,0 +1,863 @@
'use strict';
const maxPrefixBytes = 96;
const handles = new Map();
const sockets = new Map();
const pendingHooks = [];
const installedHooks = new Set();
let getPeerName = null;
function emit(kind, payload) {
payload.kind = kind;
payload.pid = Process.id;
payload.tid = Process.getCurrentThreadId();
payload.timestamp = new Date().toISOString();
console.log('FRIDA_SYS ' + JSON.stringify(payload));
}
function toHex(bytes) {
const parts = [];
for (let i = 0; i < bytes.length; i++) {
const text = bytes[i].toString(16);
parts.push(text.length === 1 ? '0' + text : text);
}
return parts.join('');
}
function asciiPreview(bytes) {
let text = '';
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i];
text += b >= 32 && b <= 126 ? String.fromCharCode(b) : '.';
}
return text;
}
function payloadSummary(buffer, length, includePrefix) {
if (includePrefix === undefined) {
includePrefix = true;
}
const byteCount = Math.max(0, Math.min(length, maxPrefixBytes));
if (!includePrefix || buffer.isNull() || byteCount === 0) {
return { byteCount: length, prefixByteCount: 0, prefixHex: '', asciiPrefix: '' };
}
try {
const raw = buffer.readByteArray(byteCount);
const bytes = Array.from(new Uint8Array(raw));
return {
byteCount: length,
prefixByteCount: byteCount,
prefixHex: toHex(bytes),
asciiPrefix: asciiPreview(bytes)
};
} catch (e) {
return { byteCount: length, prefixByteCount: 0, prefixHex: '', asciiPrefix: '', error: String(e) };
}
}
function socketKey(socket) {
return socket.toString();
}
function readPortNetworkOrder(address) {
const hi = address.readU8();
const lo = address.add(1).readU8();
return (hi << 8) | lo;
}
function parseSockaddr(address) {
if (address.isNull()) {
return null;
}
try {
const family = address.readU16();
if (family === 2) {
return {
family: 'IPv4',
address: [address.add(4).readU8(), address.add(5).readU8(), address.add(6).readU8(), address.add(7).readU8()].join('.'),
port: readPortNetworkOrder(address.add(2))
};
}
if (family === 23) {
const parts = [];
for (let i = 0; i < 16; i += 2) {
parts.push(((address.add(8 + i).readU8() << 8) | address.add(8 + i + 1).readU8()).toString(16));
}
return { family: 'IPv6', address: parts.join(':'), port: readPortNetworkOrder(address.add(2)) };
}
return { family: String(family), address: null, port: null };
} catch (e) {
return { error: String(e) };
}
}
function ensurePeer(socket) {
const key = socketKey(socket);
if (sockets.has(key) || getPeerName === null) {
return;
}
try {
const address = Memory.alloc(128);
const length = Memory.alloc(4);
length.writeU32(128);
if (getPeerName(socket, address, length) === 0) {
const peer = parseSockaddr(address);
if (peer !== null) {
sockets.set(key, peer);
}
}
} catch (_) {
}
}
function readWsaBuffers(lpBuffers, count) {
const result = [];
const stride = Process.pointerSize === 8 ? 16 : 8;
const pointerOffset = Process.pointerSize === 8 ? 8 : 4;
for (let i = 0; i < count; i++) {
const item = lpBuffers.add(i * stride);
const length = item.readU32();
const buffer = item.add(pointerOffset).readPointer();
result.push(payloadSummary(buffer, length));
}
return result;
}
function readSecBufferDesc(secBufferDesc) {
if (secBufferDesc.isNull()) {
return null;
}
try {
const version = secBufferDesc.readU32();
const bufferCount = secBufferDesc.add(4).readU32();
const buffersPointer = secBufferDesc.add(Process.pointerSize === 8 ? 8 : 8).readPointer();
const buffers = [];
const stride = Process.pointerSize === 8 ? 16 : 12;
const pointerOffset = Process.pointerSize === 8 ? 8 : 8;
for (let i = 0; i < Math.min(bufferCount, 8); i++) {
const item = buffersPointer.add(i * stride);
const length = item.readU32();
const type = item.add(4).readU32();
const buffer = item.add(pointerOffset).readPointer();
buffers.push({
index: i,
type,
payload: payloadSummary(buffer, length)
});
}
return { version, bufferCount, buffers };
} catch (e) {
return { error: String(e) };
}
}
function shouldCaptureFilePayload(path) {
if (path === undefined || path === null) {
return false;
}
return path.indexOf('\\pipe\\') !== -1 || path.indexOf('\\Device\\Afd') !== -1;
}
function hookExport(moduleName, exportName, callbacks) {
const key = moduleName.toLowerCase() + '!' + exportName;
if (installedHooks.has(key)) {
return true;
}
try {
let module = null;
const targetModuleName = moduleName.toLowerCase();
for (const candidate of Process.enumerateModules()) {
if (candidate.name.toLowerCase() === targetModuleName) {
module = candidate;
break;
}
}
if (module === null) {
module = Process.getModuleByName(moduleName);
}
const address = module.getExportByName(exportName);
Interceptor.attach(address, callbacks);
installedHooks.add(key);
emit('hooked', { api: exportName, module: moduleName, address: address.toString() });
return true;
} catch (e) {
if (String(e).indexOf('unable to find module') !== -1) {
if (pendingHooks.filter(h => h.key === key).length === 0) {
pendingHooks.push({ key, moduleName, exportName, callbacks });
emit('hook-pending', { api: exportName, module: moduleName });
}
return false;
}
if (String(e).indexOf('unable to find export') !== -1) {
installedHooks.add(key);
emit('hook-missing-export', { api: exportName, module: moduleName, error: String(e) });
return false;
}
emit('hook-error', { api: exportName, module: moduleName, error: String(e) });
return false;
}
}
function installPendingHooks() {
for (const hook of pendingHooks.slice()) {
hookExport(hook.moduleName, hook.exportName, hook.callbacks);
}
}
function readUnicodeString(unicodeString) {
if (unicodeString.isNull()) {
return '';
}
try {
const length = unicodeString.readU16();
const bufferOffset = Process.pointerSize === 8 ? 8 : 4;
const buffer = unicodeString.add(bufferOffset).readPointer();
if (buffer.isNull() || length === 0) {
return '';
}
return buffer.readUtf16String(length / 2);
} catch (e) {
return '<unicode-read-error:' + String(e) + '>';
}
}
function readObjectAttributesName(objectAttributes) {
if (objectAttributes.isNull()) {
return '';
}
try {
const objectNameOffset = Process.pointerSize === 8 ? 16 : 8;
const unicodeString = objectAttributes.add(objectNameOffset).readPointer();
return readUnicodeString(unicodeString);
} catch (e) {
return '<object-attributes-read-error:' + String(e) + '>';
}
}
function rememberHandle(handle, path, api) {
if (!handle.equals(ptr('-1')) && !handle.isNull()) {
handles.set(handle.toString(), path || '<empty>');
emit('handle-open', { api, handle: handle.toString(), path: path || '<empty>' });
}
}
function forgetHandle(handle, api, retval) {
const key = handle.toString();
const path = handles.get(key);
if (path !== undefined) {
emit('handle-close', { api, handle: key, path, retval: retval.toString() });
handles.delete(key);
}
}
emit('startup', {
arch: Process.arch,
platform: Process.platform,
modules: Process.enumerateModules()
.filter(m => {
const lower = m.name.toLowerCase() + ' ' + m.path.toLowerCase();
return lower.indexOf('aah') !== -1 || lower.indexOf('historian') !== -1 || lower.indexOf('histsdk') !== -1;
})
.map(m => ({ name: m.name, base: m.base.toString(), size: m.size, path: m.path }))
});
try {
Process.attachModuleObserver({
onAdded(module) {
const lower = module.name.toLowerCase() + ' ' + module.path.toLowerCase();
if (lower.indexOf('aah') !== -1 || lower.indexOf('historian') !== -1 || lower.indexOf('histsdk') !== -1 || lower.indexOf('secur') !== -1 || lower.indexOf('netapi') !== -1) {
emit('module-added', { name: module.name, base: module.base.toString(), size: module.size, path: module.path });
}
installPendingHooks();
}
});
} catch (e) {
emit('module-observer-error', { error: String(e) });
}
const pendingTimer = setInterval(() => {
installPendingHooks();
if (pendingHooks.filter(h => !installedHooks.has(h.key)).length === 0) {
clearInterval(pendingTimer);
}
}, 100);
hookExport('kernel32.dll', 'CreateFileW', {
onEnter(args) {
this.path = args[0].isNull() ? '' : args[0].readUtf16String();
},
onLeave(retval) {
rememberHandle(retval, this.path, 'CreateFileW');
}
});
hookExport('kernel32.dll', 'CloseHandle', {
onEnter(args) {
this.handle = args[0];
},
onLeave(retval) {
forgetHandle(this.handle, 'CloseHandle', retval);
}
});
hookExport('kernel32.dll', 'ReadFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.buffer = args[1];
this.requested = args[2].toInt32();
this.readPointer = args[3];
},
onLeave(retval) {
if (this.path === undefined) {
return;
}
let read = null;
try {
read = this.readPointer.isNull() ? null : this.readPointer.readU32();
} catch (_) {
read = null;
}
emit('read-file', {
api: 'ReadFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.requested,
resultBytes: read,
retval: retval.toInt32(),
payload: payloadSummary(this.buffer, Math.max(0, read === null ? 0 : read), shouldCaptureFilePayload(this.path))
});
}
});
hookExport('kernel32.dll', 'WriteFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.buffer = args[1];
this.requested = args[2].toInt32();
this.writtenPointer = args[3];
},
onLeave(retval) {
if (this.path === undefined) {
return;
}
let written = null;
try {
written = this.writtenPointer.isNull() ? null : this.writtenPointer.readU32();
} catch (_) {
written = null;
}
emit('write-file', {
api: 'WriteFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.requested,
resultBytes: written,
retval: retval.toInt32(),
payload: payloadSummary(this.buffer, Math.max(0, written === null ? this.requested : written), shouldCaptureFilePayload(this.path))
});
}
});
hookExport('ntdll.dll', 'NtCreateFile', {
onEnter(args) {
this.fileHandlePointer = args[0];
this.path = readObjectAttributesName(args[2]);
},
onLeave(retval) {
if (retval.toInt32() < 0) {
return;
}
try {
rememberHandle(this.fileHandlePointer.readPointer(), this.path, 'NtCreateFile');
} catch (e) {
emit('handle-open-error', { api: 'NtCreateFile', path: this.path, error: String(e) });
}
}
});
hookExport('ntdll.dll', 'NtReadFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.ioStatusBlock = args[4];
this.buffer = args[5];
this.requested = args[6].toInt32();
},
onLeave(retval) {
if (this.path === undefined) {
return;
}
let transferred = null;
try {
transferred = this.ioStatusBlock.add(Process.pointerSize).readPointer().toUInt32();
} catch (_) {
transferred = retval.toInt32() >= 0 ? this.requested : 0;
}
emit('nt-read-file', {
api: 'NtReadFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.requested,
resultBytes: transferred,
ntstatus: retval.toString(),
payload: payloadSummary(this.buffer, Math.max(0, transferred || 0), shouldCaptureFilePayload(this.path))
});
}
});
hookExport('ntdll.dll', 'NtWriteFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.ioStatusBlock = args[4];
this.buffer = args[5];
this.requested = args[6].toInt32();
},
onLeave(retval) {
if (this.path === undefined) {
return;
}
let transferred = null;
try {
transferred = this.ioStatusBlock.add(Process.pointerSize).readPointer().toUInt32();
} catch (_) {
transferred = this.requested;
}
emit('nt-write-file', {
api: 'NtWriteFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.requested,
resultBytes: transferred,
ntstatus: retval.toString(),
payload: payloadSummary(this.buffer, Math.max(0, transferred || this.requested), shouldCaptureFilePayload(this.path))
});
}
});
hookExport('ntdll.dll', 'NtDeviceIoControlFile', {
onEnter(args) {
this.handle = args[0];
this.ioStatusBlock = args[4];
this.ioControlCode = args[5].toUInt32();
this.inputBuffer = args[6];
this.inputLength = args[7].toInt32();
this.outputBuffer = args[8];
this.outputLength = args[9].toInt32();
},
onLeave(retval) {
let transferred = null;
try {
transferred = this.ioStatusBlock.add(Process.pointerSize).readPointer().toUInt32();
} catch (_) {
transferred = null;
}
emit('nt-device-io-control', {
api: 'NtDeviceIoControlFile',
handle: this.handle.toString(),
path: handles.get(this.handle.toString()),
ioControlCode: '0x' + this.ioControlCode.toString(16),
inputBytes: this.inputLength,
outputBytes: this.outputLength,
transferredBytes: transferred,
ntstatus: retval.toString(),
input: payloadSummary(this.inputBuffer, Math.max(0, this.inputLength), shouldCaptureFilePayload(handles.get(this.handle.toString()))),
output: payloadSummary(this.outputBuffer, retval.toInt32() >= 0 ? Math.max(0, transferred === null ? this.outputLength : transferred) : 0, shouldCaptureFilePayload(handles.get(this.handle.toString())))
});
}
});
hookExport('ws2_32.dll', 'GetAddrInfoW', {
onEnter(args) {
this.nodeName = args[0].isNull() ? '' : args[0].readUtf16String();
this.serviceName = args[1].isNull() ? '' : args[1].readUtf16String();
},
onLeave(retval) {
emit('dns', { api: 'GetAddrInfoW', nodeName: this.nodeName, serviceName: this.serviceName, retval: retval.toInt32() });
}
});
try {
const ws2 = Process.getModuleByName('ws2_32.dll');
getPeerName = new NativeFunction(ws2.getExportByName('getpeername'), 'int', ['pointer', 'pointer', 'pointer']);
} catch (e) {
emit('getpeername-error', { error: String(e) });
}
hookExport('ws2_32.dll', 'connect', {
onEnter(args) {
this.socket = args[0];
this.peer = parseSockaddr(args[1]);
},
onLeave(retval) {
if (this.peer !== null) {
sockets.set(socketKey(this.socket), this.peer);
}
emit('socket-connect', { api: 'connect', socket: socketKey(this.socket), peer: this.peer, retval: retval.toInt32() });
}
});
hookExport('ws2_32.dll', 'WSAConnect', {
onEnter(args) {
this.socket = args[0];
this.peer = parseSockaddr(args[1]);
},
onLeave(retval) {
if (this.peer !== null) {
sockets.set(socketKey(this.socket), this.peer);
}
emit('socket-connect', { api: 'WSAConnect', socket: socketKey(this.socket), peer: this.peer, retval: retval.toInt32() });
}
});
hookExport('ws2_32.dll', 'send', {
onEnter(args) {
this.socket = args[0];
this.buffer = args[1];
this.length = args[2].toInt32();
},
onLeave(retval) {
ensurePeer(this.socket);
emit('socket-send', {
api: 'send',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
requestedBytes: this.length,
resultBytes: retval.toInt32(),
payload: payloadSummary(this.buffer, Math.max(0, Math.min(this.length, retval.toInt32())))
});
}
});
hookExport('ws2_32.dll', 'recv', {
onEnter(args) {
this.socket = args[0];
this.buffer = args[1];
},
onLeave(retval) {
const length = retval.toInt32();
if (length <= 0) {
return;
}
ensurePeer(this.socket);
emit('socket-recv', {
api: 'recv',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
resultBytes: length,
payload: payloadSummary(this.buffer, length)
});
}
});
hookExport('ws2_32.dll', 'WSASend', {
onEnter(args) {
this.socket = args[0];
this.buffers = args[1];
this.count = args[2].toInt32();
this.sentPointer = args[3];
this.summaries = readWsaBuffers(this.buffers, this.count);
},
onLeave(retval) {
let sent = null;
try {
sent = this.sentPointer.isNull() ? null : this.sentPointer.readU32();
} catch (_) {
sent = null;
}
ensurePeer(this.socket);
emit('socket-send', {
api: 'WSASend',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
retval: retval.toInt32(),
resultBytes: sent,
buffers: this.summaries
});
}
});
hookExport('ws2_32.dll', 'WSARecv', {
onEnter(args) {
this.socket = args[0];
this.buffers = args[1];
this.count = args[2].toInt32();
this.receivedPointer = args[3];
},
onLeave(retval) {
let received = null;
try {
received = this.receivedPointer.isNull() ? null : this.receivedPointer.readU32();
} catch (_) {
received = null;
}
ensurePeer(this.socket);
emit('socket-recv', {
api: 'WSARecv',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
retval: retval.toInt32(),
resultBytes: received,
buffers: readWsaBuffers(this.buffers, this.count)
});
}
});
hookExport('ws2_32.dll', 'WSAIoctl', {
onEnter(args) {
this.socket = args[0];
this.controlCode = args[1].toUInt32();
this.inputBuffer = args[2];
this.inputLength = args[3].toInt32();
this.outputBuffer = args[4];
this.outputLength = args[5].toInt32();
this.bytesReturnedPointer = args[6];
},
onLeave(retval) {
let bytesReturned = null;
try {
bytesReturned = this.bytesReturnedPointer.isNull() ? null : this.bytesReturnedPointer.readU32();
} catch (_) {
bytesReturned = null;
}
ensurePeer(this.socket);
emit('socket-ioctl', {
api: 'WSAIoctl',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
controlCode: '0x' + this.controlCode.toString(16),
inputBytes: this.inputLength,
outputBytes: this.outputLength,
bytesReturned,
retval: retval.toInt32(),
input: payloadSummary(this.inputBuffer, Math.max(0, this.inputLength)),
output: payloadSummary(this.outputBuffer, Math.max(0, bytesReturned === null ? 0 : bytesReturned))
});
}
});
hookExport('mswsock.dll', 'ConnectEx', {
onEnter(args) {
this.socket = args[0];
this.peer = parseSockaddr(args[1]);
this.sendBuffer = args[3];
this.sendBytes = args[4].toInt32();
},
onLeave(retval) {
if (this.peer !== null) {
sockets.set(socketKey(this.socket), this.peer);
}
emit('socket-connect', {
api: 'ConnectEx',
socket: socketKey(this.socket),
peer: this.peer,
retval: retval.toInt32(),
initialPayload: payloadSummary(this.sendBuffer, Math.max(0, this.sendBytes))
});
}
});
hookExport('mswsock.dll', 'TransmitPackets', {
onEnter(args) {
this.socket = args[0];
this.packetArray = args[1];
this.elementCount = args[2].toInt32();
this.sendSize = args[3].toInt32();
},
onLeave(retval) {
ensurePeer(this.socket);
emit('socket-send', {
api: 'TransmitPackets',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
elementCount: this.elementCount,
sendSize: this.sendSize,
retval: retval.toInt32()
});
}
});
hookExport('secur32.dll', 'AcquireCredentialsHandleW', {
onEnter(args) {
this.principal = args[0].isNull() ? '' : args[0].readUtf16String();
this.packageName = args[1].isNull() ? '' : args[1].readUtf16String();
this.credentialUse = args[2].toUInt32();
},
onLeave(retval) {
emit('secur32', {
api: 'AcquireCredentialsHandleW',
packageName: this.packageName,
principalPresent: this.principal.length > 0,
credentialUse: this.credentialUse,
status: retval.toString()
});
}
});
hookExport('sspicli.dll', 'AcquireCredentialsHandleW', {
onEnter(args) {
this.principal = args[0].isNull() ? '' : args[0].readUtf16String();
this.packageName = args[1].isNull() ? '' : args[1].readUtf16String();
this.credentialUse = args[2].toUInt32();
},
onLeave(retval) {
emit('secur32', {
api: 'AcquireCredentialsHandleW',
module: 'sspicli.dll',
packageName: this.packageName,
principalPresent: this.principal.length > 0,
credentialUse: this.credentialUse,
status: retval.toString()
});
}
});
hookExport('secur32.dll', 'InitializeSecurityContextW', {
onEnter(args) {
this.target = args[2].isNull() ? '' : args[2].readUtf16String();
this.requestFlags = args[3].toUInt32();
this.inputToken = readSecBufferDesc(args[6]);
this.outputTokenPointer = args[9];
},
onLeave(retval) {
emit('secur32', {
api: 'InitializeSecurityContextW',
target: this.target,
requestFlags: this.requestFlags,
inputToken: this.inputToken,
outputToken: readSecBufferDesc(this.outputTokenPointer),
status: retval.toString()
});
}
});
hookExport('sspicli.dll', 'InitializeSecurityContextW', {
onEnter(args) {
this.target = args[2].isNull() ? '' : args[2].readUtf16String();
this.requestFlags = args[3].toUInt32();
this.inputToken = readSecBufferDesc(args[6]);
this.outputTokenPointer = args[9];
},
onLeave(retval) {
emit('secur32', {
api: 'InitializeSecurityContextW',
module: 'sspicli.dll',
target: this.target,
requestFlags: this.requestFlags,
inputToken: this.inputToken,
outputToken: readSecBufferDesc(this.outputTokenPointer),
status: retval.toString()
});
}
});
hookExport('secur32.dll', 'AcceptSecurityContext', {
onEnter(args) {
this.requestFlags = args[3].toUInt32();
},
onLeave(retval) {
emit('secur32', { api: 'AcceptSecurityContext', requestFlags: this.requestFlags, status: retval.toString() });
}
});
hookExport('sspicli.dll', 'AcceptSecurityContext', {
onEnter(args) {
this.requestFlags = args[3].toUInt32();
},
onLeave(retval) {
emit('secur32', { api: 'AcceptSecurityContext', module: 'sspicli.dll', requestFlags: this.requestFlags, status: retval.toString() });
}
});
hookExport('secur32.dll', 'QuerySecurityContextToken', {
onLeave(retval) {
emit('secur32', { api: 'QuerySecurityContextToken', status: retval.toString() });
}
});
hookExport('sspicli.dll', 'QuerySecurityContextToken', {
onLeave(retval) {
emit('secur32', { api: 'QuerySecurityContextToken', module: 'sspicli.dll', status: retval.toString() });
}
});
hookExport('crypt32.dll', 'CryptBinaryToStringW', {
onEnter(args) {
this.inputBytes = args[1].toUInt32();
this.flags = args[2].toUInt32();
this.outputLengthPointer = args[4];
},
onLeave(retval) {
let outputChars = null;
try {
outputChars = this.outputLengthPointer.isNull() ? null : this.outputLengthPointer.readU32();
} catch (_) {
outputChars = null;
}
emit('crypt32', { api: 'CryptBinaryToStringW', inputBytes: this.inputBytes, flags: this.flags, outputChars, retval: retval.toInt32() });
}
});
hookExport('crypt32.dll', 'CryptStringToBinaryW', {
onEnter(args) {
this.inputChars = args[1].toUInt32();
this.flags = args[2].toUInt32();
this.outputLengthPointer = args[4];
},
onLeave(retval) {
let outputBytes = null;
try {
outputBytes = this.outputLengthPointer.isNull() ? null : this.outputLengthPointer.readU32();
} catch (_) {
outputBytes = null;
}
emit('crypt32', { api: 'CryptStringToBinaryW', inputChars: this.inputChars, flags: this.flags, outputBytes, retval: retval.toInt32() });
}
});
hookExport('netapi32.dll', 'NetUserGetLocalGroups', {
onEnter(args) {
this.server = args[0].isNull() ? '' : args[0].readUtf16String();
this.user = args[1].isNull() ? '' : args[1].readUtf16String();
},
onLeave(retval) {
emit('netapi', { api: 'NetUserGetLocalGroups', server: this.server, user: this.user, status: retval.toString() });
}
});
+506
View File
@@ -0,0 +1,506 @@
'use strict';
const historianPort = 32568;
const maxPrefixBytes = 16;
const sockets = new Map();
const handles = new Map();
let getPeerName = null;
function emit(kind, payload) {
payload.kind = kind;
payload.pid = Process.id;
payload.tid = Process.getCurrentThreadId();
payload.timestamp = new Date().toISOString();
console.log('FRIDA_NET ' + JSON.stringify(payload));
}
function socketKey(socket) {
return socket.toString();
}
function readPortNetworkOrder(address) {
const hi = address.readU8();
const lo = address.add(1).readU8();
return (hi << 8) | lo;
}
function parseSockaddr(address) {
if (address.isNull()) {
return null;
}
try {
const family = address.readU16();
if (family === 2) {
const port = readPortNetworkOrder(address.add(2));
const a = [
address.add(4).readU8(),
address.add(5).readU8(),
address.add(6).readU8(),
address.add(7).readU8()
].join('.');
return { family: 'IPv4', address: a, port };
}
if (family === 23) {
const port = readPortNetworkOrder(address.add(2));
const parts = [];
for (let i = 0; i < 16; i += 2) {
const value = (address.add(8 + i).readU8() << 8) | address.add(8 + i + 1).readU8();
parts.push(value.toString(16));
}
return { family: 'IPv6', address: parts.join(':'), port };
}
return { family: String(family), address: null, port: null };
} catch (e) {
return { error: String(e) };
}
}
function toHex(bytes) {
const parts = [];
for (let i = 0; i < bytes.length; i++) {
const text = bytes[i].toString(16);
parts.push(text.length === 1 ? '0' + text : text);
}
return parts.join('');
}
function payloadSummary(buffer, length) {
const byteCount = Math.max(0, Math.min(length, maxPrefixBytes));
if (buffer.isNull() || byteCount === 0) {
return { byteCount: length, prefixByteCount: 0, prefixHex: '' };
}
try {
const raw = Memory.readByteArray(buffer, byteCount);
return {
byteCount: length,
prefixByteCount: byteCount,
prefixHex: toHex(Array.from(new Uint8Array(raw)))
};
} catch (e) {
return { byteCount: length, prefixByteCount: 0, prefixHex: '', error: String(e) };
}
}
function isHistorianSocket(socket) {
ensurePeer(socket);
const peer = sockets.get(socketKey(socket));
return peer !== undefined && peer.port === historianPort;
}
function ensurePeer(socket) {
const key = socketKey(socket);
if (sockets.has(key) || getPeerName === null) {
return;
}
try {
const address = Memory.alloc(128);
const length = Memory.alloc(4);
length.writeU32(128);
const result = getPeerName(socket, address, length);
if (result === 0) {
const peer = parseSockaddr(address);
if (peer !== null) {
sockets.set(key, peer);
if (peer.port === historianPort) {
emit('peer-discovered', { socket: key, peer });
}
}
}
} catch (e) {
emit('peer-discovery-error', { socket: key, error: String(e) });
}
}
function hookExport(moduleName, exportName, callbacks) {
try {
const module = Process.getModuleByName(moduleName);
const address = module.getExportByName(exportName);
Interceptor.attach(address, callbacks);
emit('hooked', { api: exportName, module: moduleName, address: address.toString() });
} catch (e) {
emit('hook-error', { api: exportName, module: moduleName, error: String(e) });
}
}
function isInterestingPath(path) {
const lower = (path || '').toLowerCase();
return lower.indexOf('\\pipe\\') !== -1
|| lower.indexOf('historian') !== -1
|| lower.indexOf('aveva') !== -1
|| lower.indexOf('wonderware') !== -1
|| lower.indexOf('aah') !== -1
|| lower.indexOf('ww') !== -1;
}
function readUnicodeString(unicodeString) {
if (unicodeString.isNull()) {
return '';
}
try {
const length = unicodeString.readU16();
const bufferOffset = Process.pointerSize === 8 ? 8 : 4;
const buffer = unicodeString.add(bufferOffset).readPointer();
if (buffer.isNull() || length === 0) {
return '';
}
return buffer.readUtf16String(length / 2);
} catch (e) {
return '<unicode-read-error:' + String(e) + '>';
}
}
function readObjectAttributesName(objectAttributes) {
if (objectAttributes.isNull()) {
return '';
}
try {
const objectNameOffset = Process.pointerSize === 8 ? 16 : 8;
const unicodeString = objectAttributes.add(objectNameOffset).readPointer();
return readUnicodeString(unicodeString);
} catch (e) {
return '<object-attributes-read-error:' + String(e) + '>';
}
}
emit('startup', { arch: Process.arch, platform: Process.platform, historianPort });
try {
const ws2 = Process.getModuleByName('ws2_32.dll');
getPeerName = new NativeFunction(ws2.getExportByName('getpeername'), 'int', ['pointer', 'pointer', 'pointer']);
} catch (e) {
emit('getpeername-error', { error: String(e) });
}
hookExport('ws2_32.dll', 'connect', {
onEnter(args) {
this.socket = args[0];
this.peer = parseSockaddr(args[1]);
},
onLeave(retval) {
if (retval.toInt32() === 0 && this.peer !== null) {
sockets.set(socketKey(this.socket), this.peer);
}
if (this.peer !== null && this.peer.port === historianPort) {
emit('connect', {
socket: socketKey(this.socket),
peer: this.peer,
retval: retval.toInt32()
});
}
}
});
hookExport('ws2_32.dll', 'WSAConnect', {
onEnter(args) {
this.socket = args[0];
this.peer = parseSockaddr(args[1]);
},
onLeave(retval) {
if (retval.toInt32() === 0 && this.peer !== null) {
sockets.set(socketKey(this.socket), this.peer);
}
if (this.peer !== null && this.peer.port === historianPort) {
emit('connect', {
api: 'WSAConnect',
socket: socketKey(this.socket),
peer: this.peer,
retval: retval.toInt32()
});
}
}
});
hookExport('ws2_32.dll', 'closesocket', {
onEnter(args) {
this.socket = args[0];
this.peer = sockets.get(socketKey(this.socket));
},
onLeave(retval) {
if (this.peer !== undefined && this.peer.port === historianPort) {
emit('close', { socket: socketKey(this.socket), peer: this.peer, retval: retval.toInt32() });
}
sockets.delete(socketKey(this.socket));
}
});
hookExport('ws2_32.dll', 'send', {
onEnter(args) {
this.socket = args[0];
this.buffer = args[1];
this.length = args[2].toInt32();
},
onLeave(retval) {
if (isHistorianSocket(this.socket)) {
emit('send', {
api: 'send',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
requestedBytes: this.length,
resultBytes: retval.toInt32(),
payload: payloadSummary(this.buffer, Math.max(0, Math.min(this.length, retval.toInt32())))
});
}
}
});
hookExport('ws2_32.dll', 'recv', {
onEnter(args) {
this.socket = args[0];
this.buffer = args[1];
},
onLeave(retval) {
const length = retval.toInt32();
if (length > 0 && isHistorianSocket(this.socket)) {
emit('recv', {
api: 'recv',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
resultBytes: length,
payload: payloadSummary(this.buffer, length)
});
}
}
});
function readWsaBuffers(lpBuffers, count) {
const result = [];
const stride = Process.pointerSize === 8 ? 16 : 8;
const pointerOffset = Process.pointerSize === 8 ? 8 : 4;
for (let i = 0; i < count; i++) {
const item = lpBuffers.add(i * stride);
const length = item.readU32();
const buffer = item.add(pointerOffset).readPointer();
result.push(payloadSummary(buffer, length));
}
return result;
}
hookExport('ws2_32.dll', 'WSASend', {
onEnter(args) {
this.socket = args[0];
this.buffers = args[1];
this.count = args[2].toInt32();
this.sentPointer = args[3];
this.summaries = isHistorianSocket(this.socket) ? readWsaBuffers(this.buffers, this.count) : [];
},
onLeave(retval) {
if (isHistorianSocket(this.socket)) {
let sent = null;
try {
sent = this.sentPointer.isNull() ? null : this.sentPointer.readU32();
} catch (_) {
sent = null;
}
emit('send', {
api: 'WSASend',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
retval: retval.toInt32(),
resultBytes: sent,
buffers: this.summaries
});
}
}
});
hookExport('ws2_32.dll', 'WSARecv', {
onEnter(args) {
this.socket = args[0];
this.buffers = args[1];
this.count = args[2].toInt32();
this.receivedPointer = args[3];
},
onLeave(retval) {
if (isHistorianSocket(this.socket)) {
let received = null;
try {
received = this.receivedPointer.isNull() ? null : this.receivedPointer.readU32();
} catch (_) {
received = null;
}
emit('recv', {
api: 'WSARecv',
socket: socketKey(this.socket),
peer: sockets.get(socketKey(this.socket)),
retval: retval.toInt32(),
resultBytes: received,
buffers: readWsaBuffers(this.buffers, this.count)
});
}
}
});
hookExport('kernel32.dll', 'CreateFileW', {
onEnter(args) {
this.path = args[0].isNull() ? '' : args[0].readUtf16String();
this.interesting = isInterestingPath(this.path);
},
onLeave(retval) {
if (this.interesting && !retval.equals(ptr('-1'))) {
handles.set(retval.toString(), this.path);
emit('ipc-open', {
api: 'CreateFileW',
handle: retval.toString(),
path: this.path
});
}
}
});
hookExport('kernel32.dll', 'CloseHandle', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
},
onLeave(retval) {
if (this.path !== undefined) {
emit('ipc-close', {
api: 'CloseHandle',
handle: this.handle.toString(),
path: this.path,
retval: retval.toInt32()
});
handles.delete(this.handle.toString());
}
}
});
hookExport('kernel32.dll', 'WriteFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.buffer = args[1];
this.length = args[2].toInt32();
this.writtenPointer = args[3];
},
onLeave(retval) {
if (this.path !== undefined) {
let written = null;
try {
written = this.writtenPointer.isNull() ? null : this.writtenPointer.readU32();
} catch (_) {
written = null;
}
emit('ipc-write', {
api: 'WriteFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.length,
resultBytes: written,
retval: retval.toInt32(),
payload: payloadSummary(this.buffer, Math.max(0, written === null ? this.length : written))
});
}
}
});
hookExport('kernel32.dll', 'ReadFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.buffer = args[1];
this.length = args[2].toInt32();
this.readPointer = args[3];
},
onLeave(retval) {
if (this.path !== undefined) {
let read = null;
try {
read = this.readPointer.isNull() ? null : this.readPointer.readU32();
} catch (_) {
read = null;
}
emit('ipc-read', {
api: 'ReadFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.length,
resultBytes: read,
retval: retval.toInt32(),
payload: payloadSummary(this.buffer, Math.max(0, read === null ? 0 : read))
});
}
}
});
hookExport('ntdll.dll', 'NtCreateFile', {
onEnter(args) {
this.fileHandlePointer = args[0];
this.path = readObjectAttributesName(args[2]);
this.interesting = isInterestingPath(this.path);
},
onLeave(retval) {
if (this.interesting && retval.toInt32() >= 0) {
try {
const handle = this.fileHandlePointer.readPointer();
handles.set(handle.toString(), this.path);
emit('ipc-open', {
api: 'NtCreateFile',
handle: handle.toString(),
path: this.path,
ntstatus: retval.toString()
});
} catch (e) {
emit('ipc-open-error', { api: 'NtCreateFile', path: this.path, error: String(e) });
}
}
}
});
hookExport('ntdll.dll', 'NtWriteFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.buffer = args[5];
this.length = args[6].toInt32();
},
onLeave(retval) {
if (this.path !== undefined) {
emit('ipc-write', {
api: 'NtWriteFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.length,
ntstatus: retval.toString(),
payload: payloadSummary(this.buffer, this.length)
});
}
}
});
hookExport('ntdll.dll', 'NtReadFile', {
onEnter(args) {
this.handle = args[0];
this.path = handles.get(this.handle.toString());
this.buffer = args[5];
this.length = args[6].toInt32();
},
onLeave(retval) {
if (this.path !== undefined) {
emit('ipc-read', {
api: 'NtReadFile',
handle: this.handle.toString(),
path: this.path,
requestedBytes: this.length,
ntstatus: retval.toString(),
payload: payloadSummary(this.buffer, retval.toInt32() >= 0 ? this.length : 0)
});
}
}
});