[F46] analysis/frida: Suspend/Activate hooks + R5 next-step
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.<verb>.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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,55 @@
|
||||
// Frida hooks generated from headless Ghidra RVAs.
|
||||
// Usage: frida -f <MxTraceHarness.exe> -l analysis/frida/mx-nmx-trace.js -- <harness args>
|
||||
//
|
||||
// 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);
|
||||
|
||||
@@ -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<MxStatus, Error>`. 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
|
||||
|
||||
Reference in New Issue
Block a user