Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
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>
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
// 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);
|
||||
Reference in New Issue
Block a user