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:
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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() });
|
||||
}
|
||||
});
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user