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>
232 lines
5.8 KiB
JavaScript
232 lines
5.8 KiB
JavaScript
// 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)
|
|
});
|
|
}
|
|
});
|