[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.
|
// Frida hooks generated from headless Ghidra RVAs.
|
||||||
// Usage: frida -f <MxTraceHarness.exe> -l analysis/frida/mx-nmx-trace.js -- <harness args>
|
// 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 maxDump = 4096;
|
||||||
const installed = {};
|
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() {
|
function hookAuthenticateUser() {
|
||||||
hook("LmxProxy.dll", 0x1399f, "CLMXProxyServer.AuthenticateUser", function (address, module) {
|
hook("LmxProxy.dll", 0x1399f, "CLMXProxyServer.AuthenticateUser", function (address, module) {
|
||||||
return {
|
return {
|
||||||
@@ -452,6 +575,12 @@ function installKnownHooks() {
|
|||||||
hookPlainArgs("LmxProxy.dll", 0x1121d, "CLMXProxyServer.AddBufferedItem", 5);
|
hookPlainArgs("LmxProxy.dll", 0x1121d, "CLMXProxyServer.AddBufferedItem", 5);
|
||||||
hookPlainArgs("LmxProxy.dll", 0x0fc80, "CLMXProxyServer.SetBufferedUpdateInterval", 3);
|
hookPlainArgs("LmxProxy.dll", 0x0fc80, "CLMXProxyServer.SetBufferedUpdateInterval", 3);
|
||||||
hookPlainArgs("LmxProxy.dll", 0x142b4, "CLMXProxyServer.AdviseSupervisory", 5);
|
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", 0x163c0, "CProxy_ILMXProxyServerEvents2.Fire_OnBufferedDataChange", 8);
|
||||||
hookPlainArgs("LmxProxy.dll", 0x16b50, "CUserConnectionCallback.OnSetAttributeResult", 4);
|
hookPlainArgs("LmxProxy.dll", 0x16b50, "CUserConnectionCallback.OnSetAttributeResult", 4);
|
||||||
hookPlainArgs("LmxProxy.dll", 0x16d4b, "CUserConnectionCallback.OperationComplete", 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.
|
**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
|
**Severity: P2** (downgraded from P1 — client-side acceptance criteria are
|
||||||
now documented; LMX-proxy wire emission remains unconfirmed)
|
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`)
|
`captures/077-frida-suspend-advised-scanstate/` (per `docs/M6-buffered-evidence.md`)
|
||||||
documents:
|
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
|
(e.g. an `ILMXProxyServer5` opnum) or also handles them client-side. Capture
|
||||||
077's Frida script did not hook
|
077's Frida script did not hook
|
||||||
`LmxProxy.dll!CLMXProxyServer.Suspend`/`.Activate`, so the wire-side
|
`LmxProxy.dll!CLMXProxyServer.Suspend`/`.Activate`, so the wire-side
|
||||||
behaviour is invisible. Filed as **F45** in `design/followups.md` to
|
behaviour is invisible.
|
||||||
re-instrument and capture.
|
|
||||||
|
**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
|
**Current best answer:** expose `Session::suspend(item)` and
|
||||||
`Session::activate(item)` returning `Result<MxStatus, Error>`. The success
|
`Session::activate(item)` returning `Result<MxStatus, Error>`. The success
|
||||||
criteria match the .NET reference's client-side gating: the item must have
|
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.
|
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
|
**Settles when:** F45 produces a Frida capture instrumenting
|
||||||
`LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate` and either confirms a
|
`LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate` and either confirms a
|
||||||
|
|||||||
Reference in New Issue
Block a user