Files
mxaccess/analysis/frida/mx-nmx-trace.js
T
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
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>
2026-05-05 06:21:00 -04:00

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();