Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,480 @@
|
||||
// Frida hooks generated from headless Ghidra RVAs.
|
||||
// Usage: frida -f <MxTraceHarness.exe> -l analysis/frida/mx-nmx-trace.js -- <harness args>
|
||||
|
||||
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 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);
|
||||
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();
|
||||
@@ -0,0 +1,379 @@
|
||||
// Frida hooks for the 32-bit MIDL COM proxy path used by NmxSvcps.dll.
|
||||
// Usage:
|
||||
// frida -f src/MxTraceHarness/bin/Release/net481/MxTraceHarness.exe \
|
||||
// -l analysis/frida/nmx-com-proxy-trace.js -- --scenario=register --duration=3
|
||||
|
||||
const maxDump = 8192;
|
||||
const installed = {};
|
||||
let lastBstrMarshalBuffer = ptr("0");
|
||||
let lastBstrMarshalLength = 0;
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function emit(event) {
|
||||
event.time = now();
|
||||
console.log(JSON.stringify(event));
|
||||
}
|
||||
|
||||
function toHex(arrayBuffer) {
|
||||
if (arrayBuffer === null || arrayBuffer === "") return "";
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const out = [];
|
||||
for (let i = 0; i < bytes.length; i++) out.push(bytes[i].toString(16).padStart(2, "0"));
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
function dump(ptrValue, length) {
|
||||
try {
|
||||
if (ptrValue.isNull() || length <= 0) return "";
|
||||
return toHex(ptrValue.readByteArray(Math.min(length, maxDump)));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readU32(ptrValue, offset) {
|
||||
try {
|
||||
return ptrValue.add(offset).readU32();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readPtr(ptrValue, offset) {
|
||||
try {
|
||||
return ptrValue.add(offset).readPointer();
|
||||
} catch (e) {
|
||||
return ptr("0");
|
||||
}
|
||||
}
|
||||
|
||||
function moduleNameFor(address) {
|
||||
try {
|
||||
const module = Process.findModuleByAddress(address);
|
||||
return module === null ? "" : module.name;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function readUtf16(ptrValue, maxChars) {
|
||||
try {
|
||||
if (ptrValue.isNull()) return "";
|
||||
return ptrValue.readUtf16String(maxChars);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function describePossibleBstr(ptrValue) {
|
||||
try {
|
||||
if (ptrValue.isNull()) return null;
|
||||
const byteLength = ptrValue.sub(4).readU32();
|
||||
if (byteLength > 4096 || (byteLength % 2) !== 0) return null;
|
||||
return {
|
||||
ptr: ptrValue.toString(),
|
||||
byteLength,
|
||||
charLength: byteLength / 2,
|
||||
text: readUtf16(ptrValue, byteLength / 2)
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function dumpNdrVarArgs(args) {
|
||||
const values = [];
|
||||
for (let i = 2; i < 12; i++) {
|
||||
let value = ptr("0");
|
||||
try {
|
||||
value = args[i];
|
||||
} catch (e) {
|
||||
value = ptr("0");
|
||||
}
|
||||
|
||||
const item = {
|
||||
index: i,
|
||||
value: value.toString(),
|
||||
module: moduleNameFor(value),
|
||||
asU32: value.toUInt32()
|
||||
};
|
||||
|
||||
const bstr = describePossibleBstr(value);
|
||||
if (bstr !== null) {
|
||||
item.bstr = bstr;
|
||||
}
|
||||
|
||||
values.push(item);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function dumpStackWords(context, count) {
|
||||
const values = [];
|
||||
let esp = ptr("0");
|
||||
try {
|
||||
esp = context.esp;
|
||||
} catch (e) {
|
||||
return values;
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const address = esp.add(i * Process.pointerSize);
|
||||
let value = ptr("0");
|
||||
try {
|
||||
value = address.readPointer();
|
||||
} catch (e) {
|
||||
value = ptr("0");
|
||||
}
|
||||
|
||||
const item = {
|
||||
index: i,
|
||||
address: address.toString(),
|
||||
value: value.toString(),
|
||||
module: moduleNameFor(value),
|
||||
asU32: value.toUInt32()
|
||||
};
|
||||
|
||||
const bstr = describePossibleBstr(value);
|
||||
if (bstr !== null) {
|
||||
item.bstr = bstr;
|
||||
}
|
||||
|
||||
values.push(item);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function hookExport(moduleName, exportName, callbacks) {
|
||||
const key = moduleName + "!" + exportName;
|
||||
if (installed[key]) return;
|
||||
|
||||
let address = null;
|
||||
try {
|
||||
const module = Process.findModuleByName(moduleName);
|
||||
if (module !== null && typeof module.findExportByName === "function") {
|
||||
address = module.findExportByName(exportName);
|
||||
}
|
||||
if (address === null && typeof Module.findExportByName === "function") {
|
||||
address = Module.findExportByName(moduleName, exportName);
|
||||
}
|
||||
} catch (e) {
|
||||
address = null;
|
||||
}
|
||||
|
||||
if (address === null) {
|
||||
emit({ event: "hook.missing", module: moduleName, name: exportName });
|
||||
return;
|
||||
}
|
||||
|
||||
Interceptor.attach(address, callbacks(address));
|
||||
installed[key] = true;
|
||||
emit({ event: "hook.installed", module: moduleName, name: exportName, address: address.toString() });
|
||||
}
|
||||
|
||||
function dumpRpcMessage(pRpcMsg) {
|
||||
// 32-bit RPC_MESSAGE layout:
|
||||
// Handle, DataRepresentation, Buffer, BufferLength, ProcNum, TransferSyntax,
|
||||
// RpcInterfaceInformation, ReservedForRuntime, ManagerEpv, ImportContext, RpcFlags.
|
||||
const buffer = readPtr(pRpcMsg, 8);
|
||||
const bufferLength = readU32(pRpcMsg, 12);
|
||||
const procNum = readU32(pRpcMsg, 16);
|
||||
const rpcInterfaceInfo = readPtr(pRpcMsg, 24);
|
||||
|
||||
return {
|
||||
rpcMessage: pRpcMsg.toString(),
|
||||
buffer: buffer.toString(),
|
||||
bufferLength,
|
||||
procNum,
|
||||
rpcInterfaceInfo: rpcInterfaceInfo.toString(),
|
||||
rpcInterfaceModule: moduleNameFor(rpcInterfaceInfo),
|
||||
hex: bufferLength === null ? null : dump(buffer, bufferLength)
|
||||
};
|
||||
}
|
||||
|
||||
function dumpStubMessage(pStubMsg) {
|
||||
// Common 32-bit MIDL_STUB_MESSAGE prefix. This is intentionally broad; the
|
||||
// important fields for this investigation are the RPC_MESSAGE pointer and
|
||||
// the active buffer range used by the generated NmxSvcps proxy.
|
||||
const rpcMsg = readPtr(pStubMsg, 0);
|
||||
const buffer = readPtr(pStubMsg, 4);
|
||||
const bufferStart = readPtr(pStubMsg, 8);
|
||||
const bufferEnd = readPtr(pStubMsg, 12);
|
||||
const bufferLength = readU32(pStubMsg, 20);
|
||||
let activeLength = 0;
|
||||
try {
|
||||
activeLength = bufferEnd.sub(bufferStart).toInt32();
|
||||
} catch (e) {
|
||||
activeLength = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
stubMessage: pStubMsg.toString(),
|
||||
rpcMessage: rpcMsg.toString(),
|
||||
buffer: buffer.toString(),
|
||||
bufferStart: bufferStart.toString(),
|
||||
bufferEnd: bufferEnd.toString(),
|
||||
bufferLength,
|
||||
activeLength,
|
||||
bufferHex: activeLength > 0 ? dump(bufferStart, activeLength) : "",
|
||||
rpc: rpcMsg.isNull() ? null : dumpRpcMessage(rpcMsg)
|
||||
};
|
||||
}
|
||||
|
||||
function installHooks() {
|
||||
hookExport("oleaut32.dll", "BSTR_UserMarshal", function () {
|
||||
return {
|
||||
onEnter(args) {
|
||||
this.buffer = args[1];
|
||||
this.bstrSlot = args[2];
|
||||
let bstr = ptr("0");
|
||||
try {
|
||||
bstr = this.bstrSlot.readPointer();
|
||||
} catch (e) {
|
||||
bstr = ptr("0");
|
||||
}
|
||||
emit({
|
||||
event: "bstr.usermarshal.enter",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
flags: args[0].toString(),
|
||||
buffer: this.buffer.toString(),
|
||||
bstrSlot: this.bstrSlot.toString(),
|
||||
bstr: describePossibleBstr(bstr)
|
||||
});
|
||||
},
|
||||
onLeave(retval) {
|
||||
let length = 0;
|
||||
try {
|
||||
length = retval.sub(this.buffer).toInt32();
|
||||
} catch (e) {
|
||||
length = 0;
|
||||
}
|
||||
lastBstrMarshalBuffer = this.buffer;
|
||||
lastBstrMarshalLength = length;
|
||||
emit({
|
||||
event: "bstr.usermarshal.leave",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
buffer: this.buffer ? this.buffer.toString() : "",
|
||||
retval: retval.toString(),
|
||||
marshaledLength: length,
|
||||
marshaledHex: length > 0 ? dump(this.buffer, length) : ""
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
hookExport("rpcrt4.dll", "NdrInterfacePointerMarshall", function () {
|
||||
return {
|
||||
onEnter(args) {
|
||||
this.stubMsg = args[0];
|
||||
emit({
|
||||
event: "ndr.interfaceptr.marshal.enter",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
interfacePointer: args[1].toString(),
|
||||
format: args[2].toString(),
|
||||
formatPrefix: dump(args[2], 32),
|
||||
stub: dumpStubMessage(this.stubMsg)
|
||||
});
|
||||
},
|
||||
onLeave(retval) {
|
||||
emit({
|
||||
event: "ndr.interfaceptr.marshal.leave",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
retval: retval.toString(),
|
||||
stub: this.stubMsg ? dumpStubMessage(this.stubMsg) : null
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
hookExport("rpcrt4.dll", "NdrClientCall2", function () {
|
||||
return {
|
||||
onEnter(args) {
|
||||
this.format = args[1];
|
||||
this.varargs = dumpNdrVarArgs(args);
|
||||
emit({
|
||||
event: "ndr.client.enter",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
stubDesc: args[0].toString(),
|
||||
procFormat: this.format.toString(),
|
||||
procFormatPrefix: dump(this.format, 64),
|
||||
varargs: this.varargs,
|
||||
stack: dumpStackWords(this.context, 24)
|
||||
});
|
||||
},
|
||||
onLeave(retval) {
|
||||
let surroundingStubHex = "";
|
||||
if (lastBstrMarshalBuffer && !lastBstrMarshalBuffer.isNull()) {
|
||||
try {
|
||||
surroundingStubHex = dump(lastBstrMarshalBuffer.sub(36), 192);
|
||||
} catch (e) {
|
||||
surroundingStubHex = null;
|
||||
}
|
||||
}
|
||||
emit({
|
||||
event: "ndr.client.leave",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
procFormat: this.format ? this.format.toString() : "",
|
||||
retval: retval.toString(),
|
||||
lastBstrMarshalLength,
|
||||
surroundingStubHex
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
hookExport("rpcrt4.dll", "NdrProxySendReceive", function () {
|
||||
return {
|
||||
onEnter(args) {
|
||||
emit({
|
||||
event: "ndr.proxy.sendreceive.enter",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
thisPtr: args[0].toString(),
|
||||
stub: dumpStubMessage(args[1])
|
||||
});
|
||||
},
|
||||
onLeave(retval) {
|
||||
emit({
|
||||
event: "ndr.proxy.sendreceive.leave",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
retval: retval.toString()
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
hookExport("rpcrt4.dll", "I_RpcSendReceive", function () {
|
||||
return {
|
||||
onEnter(args) {
|
||||
emit({
|
||||
event: "rpc.sendreceive.enter",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
rpc: dumpRpcMessage(args[0])
|
||||
});
|
||||
},
|
||||
onLeave(retval) {
|
||||
emit({
|
||||
event: "rpc.sendreceive.leave",
|
||||
callerModule: moduleNameFor(this.returnAddress),
|
||||
retval: retval.toString()
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
emit({ event: "script.loaded", process: Process.id, arch: Process.arch, pointerSize: Process.pointerSize });
|
||||
installHooks();
|
||||
const retryTimer = setInterval(function () {
|
||||
installHooks();
|
||||
if (installed["oleaut32.dll!BSTR_UserMarshal"]
|
||||
&& installed["rpcrt4.dll!NdrInterfacePointerMarshall"]
|
||||
&& installed["rpcrt4.dll!NdrClientCall2"]) {
|
||||
clearInterval(retryTimer);
|
||||
}
|
||||
}, 100);
|
||||
@@ -0,0 +1,239 @@
|
||||
// Service-side Frida hooks for NmxSvc.exe.
|
||||
// Usage: frida -p <NmxSvc pid> -l analysis/frida/nmxsvc-trace.js
|
||||
|
||||
const maxDump = 4096;
|
||||
const installed = {};
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function emit(event) {
|
||||
event.time = now();
|
||||
console.log(JSON.stringify(event));
|
||||
}
|
||||
|
||||
function toHex(arrayBuffer) {
|
||||
if (arrayBuffer === null || arrayBuffer === "") return "";
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const out = [];
|
||||
for (let i = 0; i < bytes.length; i++) out.push(bytes[i].toString(16).padStart(2, "0"));
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
function hexDumpSafe(ptrValue, length) {
|
||||
try {
|
||||
if (ptrValue.isNull() || length <= 0) return "";
|
||||
const capped = Math.min(length, maxDump);
|
||||
return ptrValue.readByteArray(capped);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function argValue(args, index) {
|
||||
try {
|
||||
return args[index].toString();
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
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 readStackWord(context, index) {
|
||||
try {
|
||||
return context.esp.add(index * Process.pointerSize).readPointer();
|
||||
} catch (e) {
|
||||
return ptr("0");
|
||||
}
|
||||
}
|
||||
|
||||
function readUtf16(ptrValue) {
|
||||
try {
|
||||
if (ptrValue.isNull()) return "";
|
||||
const text = ptrValue.readUtf16String(96);
|
||||
if (text === null) return "";
|
||||
return text.replace(/\u0000.*$/g, "");
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function looksLikeSize(value) {
|
||||
return value !== null && value > 0 && value <= 65536;
|
||||
}
|
||||
|
||||
function addCandidate(candidates, source, sizeIndex, ptrIndex, size, dataPtr) {
|
||||
try {
|
||||
if (!looksLikeSize(size) || dataPtr.isNull()) return;
|
||||
const hex = toHex(hexDumpSafe(dataPtr, size));
|
||||
if (hex === "") return;
|
||||
candidates.push({
|
||||
source,
|
||||
sizeIndex,
|
||||
ptrIndex,
|
||||
size,
|
||||
ptr: dataPtr.toString(),
|
||||
hex
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function candidateBuffers(args, context) {
|
||||
const candidates = [];
|
||||
|
||||
for (let sizeIndex = 0; sizeIndex <= 12; sizeIndex++) {
|
||||
const size = uintArg(args, sizeIndex);
|
||||
const directPtr = ptrArg(args, sizeIndex + 1);
|
||||
addCandidate(candidates, "args.direct", sizeIndex, sizeIndex + 1, size, directPtr);
|
||||
|
||||
try {
|
||||
if (!directPtr.isNull()) {
|
||||
addCandidate(candidates, "args.byref", sizeIndex, sizeIndex + 1, size, directPtr.readPointer());
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
for (let sizeIndex = 0; sizeIndex <= 16; sizeIndex++) {
|
||||
const sizeWord = readStackWord(context, sizeIndex);
|
||||
let size = null;
|
||||
try {
|
||||
size = sizeWord.toUInt32();
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
const directPtr = readStackWord(context, sizeIndex + 1);
|
||||
addCandidate(candidates, "stack.direct", sizeIndex, sizeIndex + 1, size, directPtr);
|
||||
|
||||
try {
|
||||
if (!directPtr.isNull()) {
|
||||
addCandidate(candidates, "stack.byref", sizeIndex, sizeIndex + 1, size, directPtr.readPointer());
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function stackWords(context, count) {
|
||||
const words = [];
|
||||
for (let i = 0; i < count; i++) words.push(readStackWord(context, i).toString());
|
||||
return words;
|
||||
}
|
||||
|
||||
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 hookNmxServiceFunction(rva, name, argCount) {
|
||||
hook("NmxSvc.exe", rva, name, function (address, module) {
|
||||
return {
|
||||
onEnter(args) {
|
||||
const argList = [];
|
||||
for (let i = 0; i < argCount; i++) argList.push(argValue(args, i));
|
||||
|
||||
emit({
|
||||
event: "nmxsvc.enter",
|
||||
module: "NmxSvc.exe",
|
||||
name,
|
||||
address: address.toString(),
|
||||
ecx: this.context.ecx ? this.context.ecx.toString() : "",
|
||||
esp: this.context.esp ? this.context.esp.toString() : "",
|
||||
args: argList,
|
||||
stack: stackWords(this.context, 18),
|
||||
candidates: candidateBuffers(args, this.context)
|
||||
});
|
||||
},
|
||||
onLeave(retval) {
|
||||
emit({ event: "nmxsvc.leave", module: "NmxSvc.exe", name, retval: retval.toString() });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function hookWinsock(name) {
|
||||
let address = null;
|
||||
try {
|
||||
if (typeof Module.findExportByName === "function") {
|
||||
address = Module.findExportByName("ws2_32.dll", name);
|
||||
} else {
|
||||
const ws2 = Process.findModuleByName("ws2_32.dll");
|
||||
if (ws2 !== null && typeof ws2.findExportByName === "function") {
|
||||
address = ws2.findExportByName(name);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
address = null;
|
||||
}
|
||||
if (address === null) return;
|
||||
const key = "ws2_32.dll!" + name;
|
||||
if (installed[key]) return;
|
||||
Interceptor.attach(address, {
|
||||
onEnter(args) {
|
||||
const len = uintArg(args, 2);
|
||||
emit({
|
||||
event: "winsock.enter",
|
||||
module: "ws2_32.dll",
|
||||
name,
|
||||
socket: argValue(args, 0),
|
||||
buf: argValue(args, 1),
|
||||
len,
|
||||
flags: argValue(args, 3),
|
||||
hex: looksLikeSize(len) ? toHex(hexDumpSafe(ptrArg(args, 1), len)) : ""
|
||||
});
|
||||
},
|
||||
onLeave(retval) {
|
||||
emit({ event: "winsock.leave", module: "ws2_32.dll", name, retval: retval.toString() });
|
||||
}
|
||||
});
|
||||
installed[key] = true;
|
||||
emit({ event: "hook.installed", module: "ws2_32.dll", name, address: address.toString() });
|
||||
}
|
||||
|
||||
function installKnownHooks() {
|
||||
hookNmxServiceFunction(0x05be1, "CFMCCallback.DataReceived", 8);
|
||||
hookNmxServiceFunction(0x1807f, "CNmxControler.ProcessDataReceivedForEngine", 10);
|
||||
hookNmxServiceFunction(0x1d910, "CNmxControler.DataReceived", 10);
|
||||
hookNmxServiceFunction(0x1dcb5, "CNmxControler.TransferData", 10);
|
||||
hookNmxServiceFunction(0x1eea5, "CNmxControler.LocalCallbackDataReceived", 10);
|
||||
hookNmxServiceFunction(0x21b20, "CNmxService.TransferData", 10);
|
||||
|
||||
hookWinsock("send");
|
||||
hookWinsock("recv");
|
||||
hookWinsock("sendto");
|
||||
hookWinsock("recvfrom");
|
||||
}
|
||||
|
||||
emit({
|
||||
event: "script.loaded",
|
||||
process: Process.id,
|
||||
arch: Process.arch,
|
||||
pointerSize: Process.pointerSize
|
||||
});
|
||||
|
||||
installKnownHooks();
|
||||
@@ -0,0 +1,8 @@
|
||||
capture tag type values com_variant_type putrequest_size putrequest_first_value_offset transferdata_size transferdata_first_value_offset processdatareceived_size processdatareceived_first_value_offset processdatareceived_encoding array_kind_byte element_count element_width_or_code array_descriptor_hex notes
|
||||
029-frida-write-test-int-array TestChildObject.TestIntArray[] int[] 201;202;203;204;205;206;207;208;209;210 SAFEARRAY VT_I4/0x2003 86 28 132 74 134 94 int32 little-endian 0x42 10 4 42 00 00 00 00 0a 00 04 00 00 00 Packed numeric array values begin immediately after the 11-byte array descriptor.
|
||||
030-frida-write-test-bool-array TestChildObject.TestBoolArray[] bool[] true;false;true;false;true;false;true;false;true;false SAFEARRAY VT_BOOL/0x200b 66 28 112 74 114 94 VARIANT_BOOL-style 16-bit values 0x41 10 2 41 00 00 00 00 0a 00 02 00 00 00 Requested alternating values returned as True,True,False,False,True,True,False,False,True,True; capture 098 confirms this is an x86 COM automation projection issue rather than a one-off capture.
|
||||
098-frida-write-bool-array-pattern-10 TestChildObject.TestBoolArray[] bool[] true;false;false;true;true;false;true;false;false;true SAFEARRAY VT_BOOL/0x200b 66 28 112 74 114 94 VARIANT_BOOL-style 16-bit values 0x41 10 2 41 00 00 00 00 0a 00 02 00 00 00 Requested non-repeating values emitted as True,True,False,False,False,False,True,True,True,True through x86 MXAccess COM automation; managed native encoder keeps direct per-element bool encoding by default.
|
||||
031-frida-write-test-float-array TestChildObject.TestFloatArray[] float[] 1.25;2.5;3.75;4.25;5.5;6.75;7.25;8.5;9.75;10.25 SAFEARRAY VT_R4/0x2004 86 28 132 74 134 94 float32 little-endian 0x43 10 4 43 00 00 00 00 0a 00 04 00 00 00 Packed numeric array values begin immediately after the 11-byte array descriptor.
|
||||
032-frida-write-test-double-array TestChildObject.TestDoubleArray[] double[] 1.125;2.25;3.5;4.625;5.75;6.875;7.0;8.125;9.25;10.375 SAFEARRAY VT_R8/0x2005 126 28 172 74 174 94 float64 little-endian 0x44 10 8 44 00 00 00 00 0a 00 08 00 00 00 Packed numeric array values begin immediately after the 11-byte array descriptor.
|
||||
033-frida-write-test-string-array TestChildObject.TestStringArray[] string[] A01;B02;C03;D04;E05;F06;G07;H08;I09;J10 SAFEARRAY VT_BSTR/0x2008 256 41 302 87 304 107 UTF-16LE per-element variable record 0x45 10 4 45 00 00 00 00 0a 00 04 00 00 00 Each element is record_length + scalar string-style variable payload.
|
||||
035-frida-write-test-datetime-array-full TestChildObject.TestDateTimeArray[] datetime[] 2026-04-25T03:00:00 through 2026-04-25T03:09:00 SAFEARRAY VT_DATE/0x2007 596 41 642 87 214 94 outbound UTF-16LE per-element display string; callback FILETIME sequence 0x45 10 4 45 00 00 00 00 0a 00 04 00 00 00 Original capture 034 was truncated by the old 256-byte Frida dump cap; capture 035 uses the 4096-byte cap and has the full body.
|
||||
|
@@ -0,0 +1,7 @@
|
||||
capture tag type values com_variant_type com_value_carrier putrequest_size putrequest_value_offset putrequest_encoding transferdata_size transferdata_value_offset processdatareceived_size processdatareceived_value_offset processdatareceived_encoding notes
|
||||
023-frida-write-test-int-sequence-109-111 TestChildObject.TestInt int 109,110,111 VT_I4/0x3 args[5] int32 40 18 int32 little-endian 86 64 88 84 int32 little-endian TransferData offset is 46-byte wrapper plus inner offset 18.
|
||||
024-frida-write-test-bool-sequence TestChildObject.TestBool bool true,false,true VT_BOOL/0xb args[5] VARIANT_BOOL, true=0xffff false=0x0000 37 18 4-byte slot: true ff ff ff 00, false 00 ff ff 00 83 64 85 84 data-change body final byte: true ff, false 00 A separate 51-byte status-like ProcessDataReceived body appears after each write; the bool data-change value is in the 85-byte body.
|
||||
025-frida-write-test-float-sequence TestChildObject.TestFloat float 1.25,2.5,3.75 VT_R4/0x4 args[5] float32 bits 40 18 float32 little-endian 86 64 88 84 float32 little-endian Same 40-byte write body shape as int with scalar slot retyped.
|
||||
026-frida-write-test-double-sequence TestChildObject.TestDouble double 1.125,2.25,4.5 VT_R8/0x5 args[5]/args[6] float64 bits 44 18 float64 little-endian 90 64 92 84 float64 little-endian Body grows by 4 bytes relative to int/float.
|
||||
027-frida-write-test-string-sequence TestChildObject.TestString string AlphaMX,BetaMX,GammaMX VT_BSTR/0x8 BSTR pointer 58 or 60 26 UTF-16LE string bytes 104 or 106 72 106 or 108 92 UTF-16LE string bytes Size depends on string length; value starts 8 bytes later than numeric slot.
|
||||
028-frida-write-test-datetime-sequence TestChildObject.TestDateTime datetime 2026-04-25T02:30:00,2026-04-25T02:31:00,2026-04-25T02:32:00 VT_DATE/0x7 args[5]/args[6] OLE Automation DATE double 86 26 UTF-16LE display string, e.g. 4/25/2026 2:30:00 AM 132 72 98 88 FILETIME little-endian Outbound datetime is formatted as a string, while callback/update uses FILETIME.
|
||||
|
@@ -0,0 +1,6 @@
|
||||
capture tag write_type value value_body status_body on_write_complete notes
|
||||
089-frida-write-testint-wrong-type TestChildObject.TestInt string not_an_int 0x37 string wire kind 0x05 completion-only 0x41 no String-to-integer conversion failure is not surfaced by MXAccess OnWriteComplete.
|
||||
090-frida-write-invalid-reference NoSuchObject_999.NoSuchAttr int 145 none after failed registration none no Write returns S_OK after invalid registration result, but no value-bearing write body is emitted.
|
||||
091-frida-write-testint-double-type TestChildObject.TestInt double 1.25 0x37 double wire kind 0x04 completion-only 0x00 no Double-to-integer path appears accepted/coerced at NMX completion level.
|
||||
092-frida-write-testbool-string-type TestChildObject.TestBool string not_bool 0x37 string wire kind 0x05 completion-only 0x41 no String-to-boolean conversion failure matches string-to-integer failure completion.
|
||||
093-frida-write-testdatetime-string-type TestChildObject.TestDateTime string not_a_date 0x37 string wire kind 0x05 completion-only 0x41 no String-to-time conversion failure matches string-to-integer failure completion.
|
||||
|
@@ -0,0 +1,8 @@
|
||||
capture tag scenario method_result com_hook com_args_summary putrequest_size putrequest_value_offset putrequest_time_offset transferdata_size transferdata_value_offset transferdata_time_offset callback_size callback_value_offset callback_time_offset notes
|
||||
036-frida-write-secured-test-int TestChildObject.TestInt write-secured 0x80004021 unsupported CLMXProxyServer.WriteSecured.variantA session=1 item=1 currentUser=1 verifier=0 vt=0x3 value=112 none none none none none none none none none Operate tag rejects WriteSecured before value-bearing PutRequest.
|
||||
037-frida-write-secured2-test-int TestChildObject.TestInt write-secured2 E_INVALIDARG CLMXProxyServer.WriteSecured.variantB session=1 item=1 currentUser=1 verifier=0 vt=0x3 value=113 vt_date timestamp none none none none none none none none none Operate tag rejects WriteSecured2 before value-bearing PutRequest.
|
||||
038-frida-write-secured-protectedvalue TestMachine_001.ProtectedValue write-secured 0x80004021 unsupported CLMXProxyServer.WriteSecured.variantA session=1 item=1 currentUser=1 verifier=0 vt=0xb value=true none none none none none none none none none SecuredWrite tag still rejects public WriteSecured before value-bearing PutRequest.
|
||||
039-frida-write-secured-verified-protectedvalue1 TestMachine_001.ProtectedValue1 write-secured 0x80004021 unsupported CLMXProxyServer.WriteSecured.variantA session=1 item=1 currentUser=1 verifier=1 vt=0xb value=true none none none none none none none none none VerifiedWrite tag still rejects public WriteSecured before value-bearing PutRequest.
|
||||
040-frida-write-normal-secured-protectedvalue TestMachine_001.ProtectedValue write success CLMXProxyServer.Write.variantA session=1 item=1 vt=0xb value=true user/security=2 37 18 none 83 64 none 85 84 75 Supported public route for SecuredWrite is normal Write with fourth argument 2.
|
||||
041-frida-write-normal-verified-protectedvalue1 TestMachine_001.ProtectedValue1 write success CLMXProxyServer.Write.variantA session=1 item=1 vt=0xb value=true user/security=3 37 18 none 83 64 none 85 84 75 Supported public route for VerifiedWrite is normal Write with fourth argument 3.
|
||||
042-frida-write2-test-int-timestamp TestChildObject.TestInt write2 success CLMXProxyServer.Write.variantB session=1 item=1 vt=0x3 value=114 vt_date timestamp user/security=1 40 18 24 86 64 70 88 84 75 Write2 embeds FILETIME at PutRequest offset 24 and callback offset 75.
|
||||
|
Reference in New Issue
Block a user