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,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();
|
||||
Reference in New Issue
Block a user