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>
481 lines
15 KiB
JavaScript
481 lines
15 KiB
JavaScript
// Frida hooks generated from headless Ghidra RVAs.
|
|
// Usage: frida -f <MxTraceHarness.exe> -l analysis/frida/mx-nmx-trace.js -- <harness args>
|
|
|
|
const maxDump = 4096;
|
|
const installed = {};
|
|
|
|
function now() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function emit(event) {
|
|
event.time = now();
|
|
console.log(JSON.stringify(event));
|
|
}
|
|
|
|
function hexDumpSafe(ptr, length) {
|
|
try {
|
|
if (ptr.isNull() || length <= 0) return "";
|
|
const capped = Math.min(length, maxDump);
|
|
return ptr.readByteArray(capped);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function toHex(arrayBuffer) {
|
|
if (arrayBuffer === null || arrayBuffer === "") return "";
|
|
const bytes = new Uint8Array(arrayBuffer);
|
|
let out = [];
|
|
for (let i = 0; i < bytes.length; i++) out.push(bytes[i].toString(16).padStart(2, "0"));
|
|
return out.join(" ");
|
|
}
|
|
|
|
function dumpBytes(ptrValue, length) {
|
|
return toHex(hexDumpSafe(ptrValue, length));
|
|
}
|
|
|
|
function readStdWString(base, offset) {
|
|
try {
|
|
const obj = base.add(offset);
|
|
const length = obj.add(0x10).readU32();
|
|
const capacity = obj.add(0x14).readU32();
|
|
const data = capacity < 8 ? obj : obj.readPointer();
|
|
if (length > 1024 || data.isNull()) {
|
|
return { length, capacity, value: "" };
|
|
}
|
|
|
|
return { length, capacity, value: data.readUtf16String(length) };
|
|
} catch (e) {
|
|
return { error: e.message };
|
|
}
|
|
}
|
|
|
|
function readBstr(ptrValue) {
|
|
try {
|
|
if (ptrValue.isNull()) return "";
|
|
return ptrValue.readUtf16String();
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function readMxHandle(ptrValue) {
|
|
try {
|
|
if (ptrValue.isNull()) return null;
|
|
return {
|
|
raw: dumpBytes(ptrValue, 20),
|
|
w0: ptrValue.add(0).readU32(),
|
|
w1: ptrValue.add(4).readU32(),
|
|
w2: ptrValue.add(8).readU32(),
|
|
w3: ptrValue.add(12).readU32(),
|
|
w4: ptrValue.add(16).readU32()
|
|
};
|
|
} catch (e) {
|
|
return { error: e.message };
|
|
}
|
|
}
|
|
|
|
function readPreboundReference(ptrValue) {
|
|
try {
|
|
if (ptrValue.isNull()) return null;
|
|
const err = ptrValue.add(0xac).readPointer();
|
|
return {
|
|
ptr: ptrValue.toString(),
|
|
referenceString: readStdWString(ptrValue, 0x18),
|
|
contextString: readStdWString(ptrValue, 0x34),
|
|
auxString: readStdWString(ptrValue, 0x70),
|
|
mxReference: ptrValue.add(0x50).readPointer().toString(),
|
|
flags10: ptrValue.add(0x10).readU32(),
|
|
word14: ptrValue.add(0x14).readU32(),
|
|
word4c: ptrValue.add(0x4c).readU32(),
|
|
word54: ptrValue.add(0x54).readU32(),
|
|
word58: ptrValue.add(0x58).readU32(),
|
|
word5c: ptrValue.add(0x5c).readU32(),
|
|
word60: ptrValue.add(0x60).readU32(),
|
|
word64: ptrValue.add(0x64).readU32(),
|
|
word68: ptrValue.add(0x68).readU32(),
|
|
word6c: ptrValue.add(0x6c).readU32(),
|
|
worda0: ptrValue.add(0xa0).readU32(),
|
|
worda4: ptrValue.add(0xa4).readU32(),
|
|
status: ptrValue.add(0xa8).readU32(),
|
|
flagb0: ptrValue.add(0xb0).readU8(),
|
|
errorText: readBstr(err),
|
|
raw: dumpBytes(ptrValue, 0xb4)
|
|
};
|
|
} catch (e) {
|
|
return { error: e.message, ptr: ptrValue.toString() };
|
|
}
|
|
}
|
|
|
|
function argValue(args, index) {
|
|
try {
|
|
return args[index].toString();
|
|
} catch (e) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function intArg(args, index) {
|
|
try {
|
|
return args[index].toInt32();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function uintArg(args, index) {
|
|
try {
|
|
return args[index].toUInt32();
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function ptrArg(args, index) {
|
|
try {
|
|
return args[index];
|
|
} catch (e) {
|
|
return ptr("0");
|
|
}
|
|
}
|
|
|
|
function hook(moduleName, rva, name, handlers) {
|
|
const key = moduleName + "!" + name;
|
|
if (installed[key]) return;
|
|
const module = Process.findModuleByName(moduleName);
|
|
if (module === null) return;
|
|
const address = module.base.add(rva);
|
|
Interceptor.attach(address, handlers(address, module));
|
|
installed[key] = true;
|
|
emit({ event: "hook.installed", module: moduleName, name, base: module.base.toString(), rva: "0x" + rva.toString(16), address: address.toString() });
|
|
}
|
|
|
|
function hookPlainArgs(moduleName, rva, name, argCount) {
|
|
hook(moduleName, rva, name, function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
let values = [];
|
|
for (let i = 0; i < argCount; i++) values.push(argValue(args, i));
|
|
emit({
|
|
event: "call.enter",
|
|
module: moduleName,
|
|
name,
|
|
address: address.toString(),
|
|
ecx: this.context.ecx ? this.context.ecx.toString() : "",
|
|
args: values
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({ event: "call.leave", module: moduleName, name, retval: retval.toString() });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookAuthenticateUser() {
|
|
hook("LmxProxy.dll", 0x1399f, "CLMXProxyServer.AuthenticateUser", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.userIdOut = ptrArg(args, 4);
|
|
const password = readBstr(ptrArg(args, 3));
|
|
emit({
|
|
event: "call.enter",
|
|
module: "LmxProxy.dll",
|
|
name: "CLMXProxyServer.AuthenticateUser",
|
|
address: address.toString(),
|
|
ecx: this.context.ecx ? this.context.ecx.toString() : "",
|
|
serverHandle: intArg(args, 1),
|
|
user: readBstr(ptrArg(args, 2)),
|
|
passwordLength: password.length,
|
|
userIdOut: this.userIdOut.toString()
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
let userId = null;
|
|
try {
|
|
if (!this.userIdOut.isNull()) userId = this.userIdOut.readS32();
|
|
} catch (e) {
|
|
userId = null;
|
|
}
|
|
|
|
emit({
|
|
event: "call.leave",
|
|
module: "LmxProxy.dll",
|
|
name: "CLMXProxyServer.AuthenticateUser",
|
|
retval: retval.toString(),
|
|
userId
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookLmxPrebindReference() {
|
|
hook("Lmx.dll", 0xea780, "MxConnection.PrebindReference", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.self = ptrArg(args, 0);
|
|
this.out = ptrArg(args, 2);
|
|
emit({
|
|
event: "lmx.prebind.enter",
|
|
module: "Lmx.dll",
|
|
name: "MxConnection.PrebindReference",
|
|
self: this.self.toString(),
|
|
outPtr: this.out.toString(),
|
|
referencePtr: ptrArg(args, 1).toString(),
|
|
reference: ptrArg(args, 1).readUtf16String()
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
let handle = null;
|
|
try {
|
|
if (!this.out.isNull()) handle = this.out.readS32();
|
|
} catch (e) {
|
|
handle = null;
|
|
}
|
|
emit({ event: "lmx.prebind.leave", module: "Lmx.dll", name: "MxConnection.PrebindReference", handle });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookLmxUserRegisterPreboundReference() {
|
|
hook("Lmx.dll", 0xe1920, "MxConnection.UserRegisterPreboundReference", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.self = ptrArg(args, 0);
|
|
this.out = ptrArg(args, 4);
|
|
emit({
|
|
event: "lmx.user-register-prebound.enter",
|
|
module: "Lmx.dll",
|
|
name: "MxConnection.UserRegisterPreboundReference",
|
|
self: this.self.toString(),
|
|
preboundHandle: intArg(args, 1),
|
|
callback: argValue(args, 2),
|
|
userData: intArg(args, 3),
|
|
outPtr: this.out.toString()
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
let mxReferenceHandle = null;
|
|
try {
|
|
if (!this.out.isNull()) mxReferenceHandle = this.out.readS32();
|
|
} catch (e) {
|
|
mxReferenceHandle = null;
|
|
}
|
|
emit({
|
|
event: "lmx.user-register-prebound.leave",
|
|
module: "Lmx.dll",
|
|
name: "MxConnection.UserRegisterPreboundReference",
|
|
retval: retval.toString(),
|
|
mxReferenceHandle
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookLmxReferenceHandleReader() {
|
|
hook("Lmx.dll", 0x5f730, "IMxReference.GetMxHandle", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.out = ptrArg(args, 0);
|
|
this.ref = this.context.ecx ? this.context.ecx : ptr("0");
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "lmx.mxhandle.read",
|
|
module: "Lmx.dll",
|
|
name: "IMxReference.GetMxHandle",
|
|
referencePtr: this.ref.toString(),
|
|
outPtr: this.out.toString(),
|
|
handle: readMxHandle(this.out),
|
|
retval: retval.toString()
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookLmxFixupMxHandle() {
|
|
hook("Lmx.dll", 0x8f8b0, "AccessManager.FixUpMxHandle", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.out = ptrArg(args, 0);
|
|
emit({
|
|
event: "lmx.fixup-mxhandle.enter",
|
|
module: "Lmx.dll",
|
|
name: "AccessManager.FixUpMxHandle",
|
|
accessManager: this.context.ecx ? this.context.ecx.toString() : "",
|
|
outPtr: this.out.toString(),
|
|
inWords: [uintArg(args, 1), uintArg(args, 2), uintArg(args, 3), uintArg(args, 4), uintArg(args, 5), uintArg(args, 6)]
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "lmx.fixup-mxhandle.leave",
|
|
module: "Lmx.dll",
|
|
name: "AccessManager.FixUpMxHandle",
|
|
outPtr: this.out.toString(),
|
|
handle: readMxHandle(this.out),
|
|
retval: retval.toString()
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookLmxResolveReference() {
|
|
hook("Lmx.dll", 0x113d40, "PreboundReference.Resolve", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.prebound = this.context.ecx ? this.context.ecx : ptr("0");
|
|
emit({
|
|
event: "lmx.prebound-resolve.enter",
|
|
module: "Lmx.dll",
|
|
name: "PreboundReference.Resolve",
|
|
prebound: readPreboundReference(this.prebound)
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "lmx.prebound-resolve.leave",
|
|
module: "Lmx.dll",
|
|
name: "PreboundReference.Resolve",
|
|
prebound: readPreboundReference(this.prebound),
|
|
retval: retval.toString()
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookLmxResolveCallbacks() {
|
|
hook("Lmx.dll", 0x1155a0, "PreboundReference.OnPlatformResolveReferenceResults", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.prebound = this.context.ecx ? this.context.ecx : ptr("0");
|
|
this.reference = this.context.edx ? this.context.edx : ptr("0");
|
|
emit({
|
|
event: "lmx.platform-resolve-results.enter",
|
|
module: "Lmx.dll",
|
|
name: "PreboundReference.OnPlatformResolveReferenceResults",
|
|
prebound: readPreboundReference(this.prebound),
|
|
referencePtr: this.reference.toString()
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "lmx.platform-resolve-results.leave",
|
|
module: "Lmx.dll",
|
|
name: "PreboundReference.OnPlatformResolveReferenceResults",
|
|
prebound: readPreboundReference(this.prebound),
|
|
retval: retval.toString()
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
hook("Lmx.dll", 0x114a90, "PreboundReference.OnSetAttributeResult", function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
this.prebound = this.context.ecx ? this.context.ecx : ptr("0");
|
|
emit({
|
|
event: "lmx.set-attribute-result.enter",
|
|
module: "Lmx.dll",
|
|
name: "PreboundReference.OnSetAttributeResult",
|
|
prebound: readPreboundReference(this.prebound),
|
|
correlationId: this.context.edx ? this.context.edx.toString() : "",
|
|
pValue: argValue(args, 0),
|
|
status: argValue(args, 3)
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({
|
|
event: "lmx.set-attribute-result.leave",
|
|
module: "Lmx.dll",
|
|
name: "PreboundReference.OnSetAttributeResult",
|
|
prebound: readPreboundReference(this.prebound),
|
|
retval: retval.toString()
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function hookNmxPutRequest(moduleName, rva, name, ex) {
|
|
hook(moduleName, rva, name, function (address, module) {
|
|
return {
|
|
onEnter(args) {
|
|
// Ghidra sees this as a C++ method. On x86 thiscall, ECX is likely `this`
|
|
// and args[0] is the first stack argument. We log broad argument state
|
|
// and dump plausible size/payload pairs for later alignment.
|
|
const candidates = [];
|
|
for (let sizeIndex = 3; sizeIndex <= 8; sizeIndex++) {
|
|
const size = uintArg(args, sizeIndex);
|
|
const dataPtr = ptrArg(args, sizeIndex + 1);
|
|
if (size !== null && size > 0 && size <= 65536 && !dataPtr.isNull()) {
|
|
candidates.push({
|
|
sizeIndex,
|
|
ptrIndex: sizeIndex + 1,
|
|
size,
|
|
ptr: dataPtr.toString(),
|
|
hex: toHex(hexDumpSafe(dataPtr, size))
|
|
});
|
|
}
|
|
}
|
|
let values = [];
|
|
for (let i = 0; i < 10; i++) values.push(argValue(args, i));
|
|
emit({
|
|
event: "nmx.enter",
|
|
module: moduleName,
|
|
name,
|
|
address: address.toString(),
|
|
ecx: this.context.ecx ? this.context.ecx.toString() : "",
|
|
args: values,
|
|
candidates
|
|
});
|
|
},
|
|
onLeave(retval) {
|
|
emit({ event: "nmx.leave", module: moduleName, name, retval: retval.toString() });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function installKnownHooks() {
|
|
hookPlainArgs("LmxProxy.dll", 0x12c0c, "CLMXProxyServer.Write.variantA", 10);
|
|
hookPlainArgs("LmxProxy.dll", 0x13280, "CLMXProxyServer.Write.variantB", 13);
|
|
hookPlainArgs("LmxProxy.dll", 0x12f24, "CLMXProxyServer.WriteSecured.variantA", 10);
|
|
hookPlainArgs("LmxProxy.dll", 0x135fe, "CLMXProxyServer.WriteSecured.variantB", 14);
|
|
hookPlainArgs("LmxProxy.dll", 0x1121d, "CLMXProxyServer.AddBufferedItem", 5);
|
|
hookPlainArgs("LmxProxy.dll", 0x0fc80, "CLMXProxyServer.SetBufferedUpdateInterval", 3);
|
|
hookPlainArgs("LmxProxy.dll", 0x142b4, "CLMXProxyServer.AdviseSupervisory", 5);
|
|
hookPlainArgs("LmxProxy.dll", 0x163c0, "CProxy_ILMXProxyServerEvents2.Fire_OnBufferedDataChange", 8);
|
|
hookPlainArgs("LmxProxy.dll", 0x16b50, "CUserConnectionCallback.OnSetAttributeResult", 4);
|
|
hookPlainArgs("LmxProxy.dll", 0x16d4b, "CUserConnectionCallback.OperationComplete", 4);
|
|
hookAuthenticateUser();
|
|
|
|
hookNmxPutRequest("NmxAdptr.dll", 0x10996, "CNmxAdapter.TransferData", false);
|
|
hookNmxPutRequest("NmxAdptr.dll", 0x112da, "CNmxAdapter.ProcessDataReceived", false);
|
|
hookNmxPutRequest("NmxAdptr.dll", 0x15169, "CNmxAdapter.PutRequest", false);
|
|
hookNmxPutRequest("NmxAdptr.dll", 0x159c3, "CNmxAdapter.PutRequestEx", true);
|
|
|
|
hookLmxPrebindReference();
|
|
hookLmxUserRegisterPreboundReference();
|
|
hookLmxReferenceHandleReader();
|
|
hookLmxFixupMxHandle();
|
|
hookLmxResolveReference();
|
|
hookLmxResolveCallbacks();
|
|
}
|
|
|
|
const timer = setInterval(function () {
|
|
installKnownHooks();
|
|
if (installed["LmxProxy.dll!CLMXProxyServer.Write.variantA"] && installed["NmxAdptr.dll!CNmxAdapter.PutRequest"]) {
|
|
clearInterval(timer);
|
|
}
|
|
}, 100);
|
|
|
|
installKnownHooks();
|