808fea18a0
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>
610 lines
20 KiB
JavaScript
610 lines
20 KiB
JavaScript
// 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 = {};
|
||
|
||
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();
|