// Frida hooks generated from headless Ghidra RVAs. // Usage: frida -f -l analysis/frida/mx-nmx-trace.js -- // // F46 — Suspend / Activate instrumentation procedure // --------------------------------------------------- // The `mx.suspend.*` and `mx.activate.*` events below close the wire-side gap // left by capture 077 (`captures/077-frida-suspend-advised-scanstate/`). The // hooks attach to `LmxProxy.dll!CLMXProxyServer.Suspend` (RVA 0x13d9c, FUN_10013d9c) // and `LmxProxy.dll!CLMXProxyServer.Activate` (RVA 0x14028, FUN_10014028) — the // two RVAs were extracted from `analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv` // (rows tagged `CLMXProxyServer::Suspend - Server Handle` and // `CLMXProxyServer::Activate - Server Handle`). The export table does NOT // expose `Resume` or `Reactivate` symbols anywhere in `LmxProxy.dll`, // `Lmx.dll`, or the `ILMXProxyServer5` interface — verified against // `analysis/ghidra/exports/LmxProxy.dll.ghidra.md` and the decompiled // interface at `analysis/decompiled-mxaccess/ArchestrA/MxAccess/ILMXProxyServer5.cs`. // // To re-run capture 077 with the new hooks active (left for the maintainer // on the live AVEVA host): // // 1. Rebuild the x86 trace harness: // msbuild src\MxTraceHarness\MxTraceHarness.csproj /p:Configuration=Release // 2. Suspend-advised scenario: // frida ^ // -f src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe ^ // -l analysis\frida\mx-nmx-trace.js ^ // -- --scenario=suspend-advised ^ // --tag=TestChildObject.ScanState ^ // --write-delay-ms=1000 ^ // --duration=3 ^ // --log=captures\NNN-frida-suspend-activate-instrumented\harness.log ^ // --client=MxFridaTrace-NNN // 3. Activate-advised scenario (re-runs Suspend then Activate): // frida ^ // -f src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe ^ // -l analysis\frida\mx-nmx-trace.js ^ // -- --scenario=activate-advised ^ // --tag=TestChildObject.ScanState ^ // --write-delay-ms=1000 ^ // --duration=3 ^ // --log=captures\NNN-frida-suspend-activate-instrumented\harness.log ^ // --client=MxFridaTrace-NNN // 4. Save the resulting `frida-events.tsv` (plus `harness.log`, // `frida-command.txt`, `frida.stdout.jsonl`) under // `captures/NNN-frida-suspend-activate-instrumented/` (next free NNN). // 5. Grep for `mx.suspend.begin|mx.suspend.end|mx.activate.begin|mx.activate.end` // in the new TSV. If any matching `nmx.enter` / `lmx.*` events appear in // the same time window — typed decode the body and update // `analysis/proxy/nmxsvcps-procedures.tsv` + `docs/M6-buffered-evidence.md`. // If no NMX traffic accompanies the hook fires — Suspend/Activate are // confirmed client-side-only and R5 in `design/70-risks-and-open-questions.md` // moves to "fully settled — client-side only". 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 readMxStatusOut(ptrValue) { // MxStatus on the wire is 4 × int16 = 8 bytes: // short Success, short Category, short DetectedBy, short Detail. // See src/MxNativeCodec/MxStatus.cs and the .NET reference's // `out MxStatus pMxStatus` parameter on ILMXProxyServer5.{Suspend,Activate}. try { if (ptrValue.isNull()) return null; return { raw: dumpBytes(ptrValue, 8), success: ptrValue.add(0).readS16(), category: ptrValue.add(2).readS16(), detectedBy: ptrValue.add(4).readS16(), detail: ptrValue.add(6).readS16() }; } catch (e) { return { error: e.message }; } } function hookSuspendActivate(rva, name, eventVerb) { // CLMXProxyServer::Suspend / Activate are __stdcall member methods: // HRESULT Suspend(int hLMXServerHandle, int hItem, MxStatus* pMxStatusOut) // After Frida's __stdcall lowering, args[0] = this (because the prologue // pushes ECX into the stack frame the same way AdviseSupervisory does at // RVA 0x142b4), args[1] = serverHandle, args[2] = itemHandle, // args[3] = MxStatus* out. Mirrors the AdviseSupervisory hookPlainArgs // shape but with typed out-param decoding (cf. hookAuthenticateUser). hook("LmxProxy.dll", rva, name, function (address, module) { return { onEnter(args) { this.statusOut = ptrArg(args, 3); this.serverHandle = intArg(args, 1); this.itemHandle = intArg(args, 2); emit({ event: "mx." + eventVerb + ".begin", module: "LmxProxy.dll", name, address: address.toString(), ecx: this.context.ecx ? this.context.ecx.toString() : "", serverHandle: this.serverHandle, itemHandle: this.itemHandle, statusOutPtr: this.statusOut.toString() }); }, onLeave(retval) { emit({ event: "mx." + eventVerb + ".end", module: "LmxProxy.dll", name, retval: retval.toString(), serverHandle: this.serverHandle, itemHandle: this.itemHandle, status: readMxStatusOut(this.statusOut) }); } }; }); } function hookSuspend() { // FUN_10013d9c, RVA 0x13d9c; matched on the // `CLMXProxyServer::Suspend - Server Handle ` string xref in // analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv:119. hookSuspendActivate(0x13d9c, "CLMXProxyServer.Suspend", "suspend"); } function hookActivate() { // FUN_10014028, RVA 0x14028; matched on the // `CLMXProxyServer::Activate - Server Handle ` string xref in // analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv:122. hookSuspendActivate(0x14028, "CLMXProxyServer.Activate", "activate"); } 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); // F46: Suspend / Activate wire-side instrumentation. No `Resume` / `Reactivate` // exports exist in LmxProxy.dll's symbol table — verified against // analysis/ghidra/exports/LmxProxy.dll.ghidra.md and the // ILMXProxyServer5 / ILMXProxyServer4 decompiled interfaces. hookSuspend(); hookActivate(); 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();