fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
380 lines
10 KiB
JavaScript
380 lines
10 KiB
JavaScript
// Frida hooks for the 32-bit MIDL COM proxy path used by NmxSvcps.dll.
|
|
// Usage:
|
|
// frida -f src/MxTraceHarness/bin/Release/net481/MxTraceHarness.exe \
|
|
// -l analysis/frida/nmx-com-proxy-trace.js -- --scenario=register --duration=3
|
|
|
|
const maxDump = 8192;
|
|
const installed = {};
|
|
let lastBstrMarshalBuffer = ptr("0");
|
|
let lastBstrMarshalLength = 0;
|
|
|
|
function now() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function emit(event) {
|
|
event.time = now();
|
|
console.log(JSON.stringify(event));
|
|
}
|
|
|
|
function toHex(arrayBuffer) {
|
|
if (arrayBuffer === null || arrayBuffer === "") return "";
|
|
const bytes = new Uint8Array(arrayBuffer);
|
|
const out = [];
|
|
for (let i = 0; i < bytes.length; i++) out.push(bytes[i].toString(16).padStart(2, "0"));
|
|
return out.join(" ");
|
|
}
|
|
|
|
function dump(ptrValue, length) {
|
|
try {
|
|
if (ptrValue.isNull() || length <= 0) return "";
|
|
return toHex(ptrValue.readByteArray(Math.min(length, maxDump)));
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readU32(ptrValue, offset) {
|
|
try {
|
|
return ptrValue.add(offset).readU32();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readPtr(ptrValue, offset) {
|
|
try {
|
|
return ptrValue.add(offset).readPointer();
|
|
} catch (e) {
|
|
return ptr("0");
|
|
}
|
|
}
|
|
|
|
function moduleNameFor(address) {
|
|
try {
|
|
const module = Process.findModuleByAddress(address);
|
|
return module === null ? "" : module.name;
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function readUtf16(ptrValue, maxChars) {
|
|
try {
|
|
if (ptrValue.isNull()) return "";
|
|
return ptrValue.readUtf16String(maxChars);
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function describePossibleBstr(ptrValue) {
|
|
try {
|
|
if (ptrValue.isNull()) return null;
|
|
const byteLength = ptrValue.sub(4).readU32();
|
|
if (byteLength > 4096 || (byteLength % 2) !== 0) return null;
|
|
return {
|
|
ptr: ptrValue.toString(),
|
|
byteLength,
|
|
charLength: byteLength / 2,
|
|
text: readUtf16(ptrValue, byteLength / 2)
|
|
};
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function dumpNdrVarArgs(args) {
|
|
const values = [];
|
|
for (let i = 2; i < 12; i++) {
|
|
let value = ptr("0");
|
|
try {
|
|
value = args[i];
|
|
} catch (e) {
|
|
value = ptr("0");
|
|
}
|
|
|
|
const item = {
|
|
index: i,
|
|
value: value.toString(),
|
|
module: moduleNameFor(value),
|
|
asU32: value.toUInt32()
|
|
};
|
|
|
|
const bstr = describePossibleBstr(value);
|
|
if (bstr !== null) {
|
|
item.bstr = bstr;
|
|
}
|
|
|
|
values.push(item);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
function dumpStackWords(context, count) {
|
|
const values = [];
|
|
let esp = ptr("0");
|
|
try {
|
|
esp = context.esp;
|
|
} catch (e) {
|
|
return values;
|
|
}
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const address = esp.add(i * Process.pointerSize);
|
|
let value = ptr("0");
|
|
try {
|
|
value = address.readPointer();
|
|
} catch (e) {
|
|
value = ptr("0");
|
|
}
|
|
|
|
const item = {
|
|
index: i,
|
|
address: address.toString(),
|
|
value: value.toString(),
|
|
module: moduleNameFor(value),
|
|
asU32: value.toUInt32()
|
|
};
|
|
|
|
const bstr = describePossibleBstr(value);
|
|
if (bstr !== null) {
|
|
item.bstr = bstr;
|
|
}
|
|
|
|
values.push(item);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
function hookExport(moduleName, exportName, callbacks) {
|
|
const key = moduleName + "!" + exportName;
|
|
if (installed[key]) return;
|
|
|
|
let address = null;
|
|
try {
|
|
const module = Process.findModuleByName(moduleName);
|
|
if (module !== null && typeof module.findExportByName === "function") {
|
|
address = module.findExportByName(exportName);
|
|
}
|
|
if (address === null && typeof Module.findExportByName === "function") {
|
|
address = Module.findExportByName(moduleName, exportName);
|
|
}
|
|
} catch (e) {
|
|
address = null;
|
|
}
|
|
|
|
if (address === null) {
|
|
emit({ event: "hook.missing", module: moduleName, name: exportName });
|
|
return;
|
|
}
|
|
|
|
Interceptor.attach(address, callbacks(address));
|
|
installed[key] = true;
|
|
emit({ event: "hook.installed", module: moduleName, name: exportName, address: address.toString() });
|
|
}
|
|
|
|
function dumpRpcMessage(pRpcMsg) {
|
|
// 32-bit RPC_MESSAGE layout:
|
|
// Handle, DataRepresentation, Buffer, BufferLength, ProcNum, TransferSyntax,
|
|
// RpcInterfaceInformation, ReservedForRuntime, ManagerEpv, ImportContext, RpcFlags.
|
|
const buffer = readPtr(pRpcMsg, 8);
|
|
const bufferLength = readU32(pRpcMsg, 12);
|
|
const procNum = readU32(pRpcMsg, 16);
|
|
const rpcInterfaceInfo = readPtr(pRpcMsg, 24);
|
|
|
|
return {
|
|
rpcMessage: pRpcMsg.toString(),
|
|
buffer: buffer.toString(),
|
|
bufferLength,
|
|
procNum,
|
|
rpcInterfaceInfo: rpcInterfaceInfo.toString(),
|
|
rpcInterfaceModule: moduleNameFor(rpcInterfaceInfo),
|
|
hex: bufferLength === null ? null : dump(buffer, bufferLength)
|
|
};
|
|
}
|
|
|
|
function dumpStubMessage(pStubMsg) {
|
|
// Common 32-bit MIDL_STUB_MESSAGE prefix. This is intentionally broad; the
|
|
// important fields for this investigation are the RPC_MESSAGE pointer and
|
|
// the active buffer range used by the generated NmxSvcps proxy.
|
|
const rpcMsg = readPtr(pStubMsg, 0);
|
|
const buffer = readPtr(pStubMsg, 4);
|
|
const bufferStart = readPtr(pStubMsg, 8);
|
|
const bufferEnd = readPtr(pStubMsg, 12);
|
|
const bufferLength = readU32(pStubMsg, 20);
|
|
let activeLength = 0;
|
|
try {
|
|
activeLength = bufferEnd.sub(bufferStart).toInt32();
|
|
} catch (e) {
|
|
activeLength = 0;
|
|
}
|
|
|
|
return {
|
|
stubMessage: pStubMsg.toString(),
|
|
rpcMessage: rpcMsg.toString(),
|
|
buffer: buffer.toString(),
|
|
bufferStart: bufferStart.toString(),
|
|
bufferEnd: bufferEnd.toString(),
|
|
bufferLength,
|
|
activeLength,
|
|
bufferHex: activeLength > 0 ? dump(bufferStart, activeLength) : "",
|
|
rpc: rpcMsg.isNull() ? null : dumpRpcMessage(rpcMsg)
|
|
};
|
|
}
|
|
|
|
function installHooks() {
|
|
hookExport("oleaut32.dll", "BSTR_UserMarshal", function () {
|
|
return {
|
|
onEnter(args) {
|
|
this.buffer = args[1];
|
|
this.bstrSlot = args[2];
|
|
let bstr = ptr("0");
|
|
try {
|
|
bstr = this.bstrSlot.readPointer();
|
|
} catch (e) {
|
|
bstr = ptr("0");
|
|
}
|
|
emit({
|
|
event: "bstr.usermarshal.enter",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
flags: args[0].toString(),
|
|
buffer: this.buffer.toString(),
|
|
bstrSlot: this.bstrSlot.toString(),
|
|
bstr: describePossibleBstr(bstr)
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
let length = 0;
|
|
try {
|
|
length = retval.sub(this.buffer).toInt32();
|
|
} catch (e) {
|
|
length = 0;
|
|
}
|
|
lastBstrMarshalBuffer = this.buffer;
|
|
lastBstrMarshalLength = length;
|
|
emit({
|
|
event: "bstr.usermarshal.leave",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
buffer: this.buffer ? this.buffer.toString() : "",
|
|
retval: retval.toString(),
|
|
marshaledLength: length,
|
|
marshaledHex: length > 0 ? dump(this.buffer, length) : ""
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
hookExport("rpcrt4.dll", "NdrInterfacePointerMarshall", function () {
|
|
return {
|
|
onEnter(args) {
|
|
this.stubMsg = args[0];
|
|
emit({
|
|
event: "ndr.interfaceptr.marshal.enter",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
interfacePointer: args[1].toString(),
|
|
format: args[2].toString(),
|
|
formatPrefix: dump(args[2], 32),
|
|
stub: dumpStubMessage(this.stubMsg)
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "ndr.interfaceptr.marshal.leave",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
retval: retval.toString(),
|
|
stub: this.stubMsg ? dumpStubMessage(this.stubMsg) : null
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
hookExport("rpcrt4.dll", "NdrClientCall2", function () {
|
|
return {
|
|
onEnter(args) {
|
|
this.format = args[1];
|
|
this.varargs = dumpNdrVarArgs(args);
|
|
emit({
|
|
event: "ndr.client.enter",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
stubDesc: args[0].toString(),
|
|
procFormat: this.format.toString(),
|
|
procFormatPrefix: dump(this.format, 64),
|
|
varargs: this.varargs,
|
|
stack: dumpStackWords(this.context, 24)
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
let surroundingStubHex = "";
|
|
if (lastBstrMarshalBuffer && !lastBstrMarshalBuffer.isNull()) {
|
|
try {
|
|
surroundingStubHex = dump(lastBstrMarshalBuffer.sub(36), 192);
|
|
} catch (e) {
|
|
surroundingStubHex = null;
|
|
}
|
|
}
|
|
emit({
|
|
event: "ndr.client.leave",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
procFormat: this.format ? this.format.toString() : "",
|
|
retval: retval.toString(),
|
|
lastBstrMarshalLength,
|
|
surroundingStubHex
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
hookExport("rpcrt4.dll", "NdrProxySendReceive", function () {
|
|
return {
|
|
onEnter(args) {
|
|
emit({
|
|
event: "ndr.proxy.sendreceive.enter",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
thisPtr: args[0].toString(),
|
|
stub: dumpStubMessage(args[1])
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "ndr.proxy.sendreceive.leave",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
retval: retval.toString()
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
hookExport("rpcrt4.dll", "I_RpcSendReceive", function () {
|
|
return {
|
|
onEnter(args) {
|
|
emit({
|
|
event: "rpc.sendreceive.enter",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
rpc: dumpRpcMessage(args[0])
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "rpc.sendreceive.leave",
|
|
callerModule: moduleNameFor(this.returnAddress),
|
|
retval: retval.toString()
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
emit({ event: "script.loaded", process: Process.id, arch: Process.arch, pointerSize: Process.pointerSize });
|
|
installHooks();
|
|
const retryTimer = setInterval(function () {
|
|
installHooks();
|
|
if (installed["oleaut32.dll!BSTR_UserMarshal"]
|
|
&& installed["rpcrt4.dll!NdrInterfacePointerMarshall"]
|
|
&& installed["rpcrt4.dll!NdrClientCall2"]) {
|
|
clearInterval(retryTimer);
|
|
}
|
|
}, 100);
|