c95824a65d
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>
216 lines
5.3 KiB
JavaScript
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);
|
|
}
|