Files
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
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>
2026-05-05 06:21:00 -04:00

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();