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,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)
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user