From 808fea18a08d4e11341e1a4cfb31e919a8e884b3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 6 May 2026 05:42:24 -0400 Subject: [PATCH] [F46] analysis/frida: Suspend/Activate hooks + R5 next-step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the wire-side gap left by capture 077 in F44's R5 walk. The Frida script now hooks the production LmxProxy.dll dispatchers so a future live re-run on the AVEVA host can answer "does CLMXProxyServer issue a separate ORPC method for Suspend/Activate, or are they synthesised client-side?" Hooks added in `analysis/frida/mx-nmx-trace.js`: - `LmxProxy.dll!CLMXProxyServer.Suspend` @ RVA 0x13d9c (FUN_10013d9c) - `LmxProxy.dll!CLMXProxyServer.Activate` @ RVA 0x14028 (FUN_10014028) Both RVAs were extracted from `analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv` rows 119/122 (the `CLMXProxyServer::Suspend - Server Handle` / `Activate - Server Handle` log strings each xref one function — same pattern as the existing AdviseSupervisory hook at 0x142b4). The hooks emit `mx.suspend.begin/end` and `mx.activate.begin/end` events with serverHandle, itemHandle, and the `MxStatus*` out parameter decoded as 4 x int16 (Success / Category / DetectedBy / Detail per `src/MxNativeCodec/MxStatus.cs`). Naming matches the F46 spec's `mx..begin / end` grep convention rather than the generic `call.enter / leave` shape because we want to filter these out of large traces without false positives from other LmxProxy entrypoints. No `Resume` / `Reactivate` exports exist in `LmxProxy.dll` — verified against `analysis/ghidra/exports/LmxProxy.dll.ghidra.md` (no such string xrefs) and the decompiled `ILMXProxyServer5` / `ILMXProxyServer4` interfaces under `analysis/decompiled-mxaccess/ArchestrA/MxAccess/` (only Suspend and Activate are declared on the dispatch interface). The script's top-of-file comment now carries the live re-run procedure (rebuild MxTraceHarness x86, attach Frida with `--scenario=suspend-advised` then `--scenario=activate-advised`, save under `captures/NNN-frida-suspend-activate-instrumented/`, grep the new TSV for `mx.suspend.*` / `mx.activate.*` and correlate with `nmx.enter` events in the same time window). Live capture is intentionally deferred to the maintainer per the F46 spec — this dev box has no AVEVA install. `design/70-risks-and-open-questions.md` R5 status updated: - Title flag `(filed as F45)` -> `(filed as F46, hook landed pending live re-run)` (the docs/M6-buffered-evidence.md footnote referenced F45 from before F45 / F46 were de-conflicted by commit 2120dfa). - New "Next step - F46" paragraph documents the two hooked RVAs, the out-param decode shape, and the verified absence of Resume / Reactivate symbols. - "Current best answer" paragraph re-points the residual ORPC question at F46. Co-Authored-By: Claude Opus 4.7 (1M context) --- analysis/frida/mx-nmx-trace.js | 129 ++++++++++++++++++++++++++ design/70-risks-and-open-questions.md | 25 +++-- 2 files changed, 148 insertions(+), 6 deletions(-) diff --git a/analysis/frida/mx-nmx-trace.js b/analysis/frida/mx-nmx-trace.js index 428b823..26ebccf 100644 --- a/analysis/frida/mx-nmx-trace.js +++ b/analysis/frida/mx-nmx-trace.js @@ -1,5 +1,55 @@ // 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 = {}; @@ -173,6 +223,79 @@ function hookPlainArgs(moduleName, rva, name, argCount) { }); } +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 { @@ -452,6 +575,12 @@ function installKnownHooks() { 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); diff --git a/design/70-risks-and-open-questions.md b/design/70-risks-and-open-questions.md index d6dd9f6..9409c00 100644 --- a/design/70-risks-and-open-questions.md +++ b/design/70-risks-and-open-questions.md @@ -60,12 +60,13 @@ The `OnBufferedDataChange` **public event shape** the wwtools api-notes describe **Settles when:** indefinitely deferred — see Open evidence gaps table. Settle criteria depends on the same Ghidra mapping table as R3, which does not exist in `analysis/ghidra/` and has no owner. Reopen if a future capture or decompiled output produces evidence. -### R5 — Activate / Suspend behaviour **(partially observed — F44 documented client-side trigger; wire-side residual gap filed as F45)** +### R5 — Activate / Suspend behaviour **(partially observed — F44 documented client-side trigger; wire-side residual gap filed as F46, hook landed pending live re-run)** **Severity: P2** (downgraded from P1 — client-side acceptance criteria are now documented; LMX-proxy wire emission remains unconfirmed) -**Status (2026-05-06): PARTIALLY OBSERVED.** F44's evidence walk on +**Status (2026-05-06): PARTIALLY OBSERVED — Frida hooks ready, live capture pending.** +F44's evidence walk on `captures/077-frida-suspend-advised-scanstate/` (per `docs/M6-buffered-evidence.md`) documents: @@ -83,15 +84,27 @@ What capture 077 could **not** answer: whether the production (e.g. an `ILMXProxyServer5` opnum) or also handles them client-side. Capture 077's Frida script did not hook `LmxProxy.dll!CLMXProxyServer.Suspend`/`.Activate`, so the wire-side -behaviour is invisible. Filed as **F45** in `design/followups.md` to -re-instrument and capture. +behaviour is invisible. + +**Next step — F46.** `analysis/frida/mx-nmx-trace.js` now carries +`Interceptor.attach` blocks for `LmxProxy.dll!CLMXProxyServer.Suspend` +(RVA `0x13d9c`, `FUN_10013d9c`) and `.Activate` (RVA `0x14028`, +`FUN_10014028`), emitting `mx.suspend.begin/end` and +`mx.activate.begin/end` events with the `MxStatus*` out-parameter +decoded as 4 × int16. No `Resume` / `Reactivate` symbols exist in +`LmxProxy.dll` — verified against +`analysis/ghidra/exports/LmxProxy.dll.ghidra.md` and the decompiled +`ILMXProxyServer5` / `ILMXProxyServer4` interfaces. R5 stays open +until a live re-run on the AVEVA host produces +`captures/NNN-frida-suspend-activate-instrumented/` per the procedure +documented at the top of `analysis/frida/mx-nmx-trace.js`. **Current best answer:** expose `Session::suspend(item)` and `Session::activate(item)` returning `Result`. The success criteria match the .NET reference's client-side gating: the item must have -an active subscription. If F45's wire capture later proves the LMX proxy +an active subscription. If F46's wire capture later proves the LMX proxy issues a separate ORPC method, add the wire emission here in M6 follow-up. -Do not build callback-driven state transitions on top until F45 settles. +Do not build callback-driven state transitions on top until F46 settles. **Settles when:** F45 produces a Frida capture instrumenting `LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate` and either confirms a