fe2a6db786
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>
240 lines
6.3 KiB
JavaScript
240 lines
6.3 KiB
JavaScript
// 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();
|