Files
histsdk/scripts/frida/aahclient-exports.js
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:

- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass

Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.

Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:31:48 -04:00

216 lines
5.3 KiB
JavaScript

'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);
}