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,438 @@
|
||||
// Exports compact Ghidra facts for MXAccess/LMX/NMX reverse-engineering.
|
||||
// The output is intentionally metadata-oriented: functions, references,
|
||||
// imports, strings, and call relationships, not full proprietary decompiled
|
||||
// source listings.
|
||||
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Data;
|
||||
import ghidra.program.model.listing.DataIterator;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.FunctionIterator;
|
||||
import ghidra.program.model.listing.Instruction;
|
||||
import ghidra.program.model.listing.InstructionIterator;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.program.model.symbol.Reference;
|
||||
import ghidra.program.model.symbol.Symbol;
|
||||
import ghidra.program.model.symbol.SymbolIterator;
|
||||
import ghidra.program.model.symbol.SymbolTable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class MxNmxExport extends GhidraScript {
|
||||
private final List<String> interestingStringNeedles = Arrays.asList(
|
||||
"CLMXProxyServer::Write",
|
||||
"CLMXProxyServer::Advise",
|
||||
"CLMXProxyServer::AdviseSupervisory",
|
||||
"CLMXProxyServer::Register",
|
||||
"CLMXProxyServer::AddItem",
|
||||
"Fire_OnWriteComplete",
|
||||
"Fire_OnDataChange",
|
||||
"RemoteWrite",
|
||||
"WriteSecured",
|
||||
"WriteVerified",
|
||||
"PrebindReference",
|
||||
"SupervisoryRegisterPreboundReference",
|
||||
"UserRegisterPreboundReference",
|
||||
"TransferData",
|
||||
"DataReceived",
|
||||
"StatusReceived",
|
||||
"PutRequest",
|
||||
"GetResponse",
|
||||
"Nmx",
|
||||
"Lmx",
|
||||
"MX_E_",
|
||||
"MxSecurity",
|
||||
"MxSource",
|
||||
"MxCategory",
|
||||
"IDataClient",
|
||||
"PublishWriteComplete",
|
||||
"Write2",
|
||||
"RegisterItems",
|
||||
"socket",
|
||||
"WSASend",
|
||||
"WSARecv",
|
||||
"send",
|
||||
"recv",
|
||||
"Ndr",
|
||||
"RPC"
|
||||
);
|
||||
|
||||
private final List<String> interestingCallNeedles = Arrays.asList(
|
||||
"send",
|
||||
"recv",
|
||||
"WSASend",
|
||||
"WSARecv",
|
||||
"connect",
|
||||
"bind",
|
||||
"listen",
|
||||
"accept",
|
||||
"closesocket",
|
||||
"Ndr",
|
||||
"Rpc",
|
||||
"CoCreateInstance",
|
||||
"CoGetClassObject",
|
||||
"QueryInterface",
|
||||
"Variant",
|
||||
"SafeArray",
|
||||
"SysAllocString",
|
||||
"SysFreeString",
|
||||
"memcpy",
|
||||
"memmove",
|
||||
"memset"
|
||||
);
|
||||
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] args = getScriptArgs();
|
||||
File outDir = new File(args.length > 0 ? args[0] : "analysis/ghidra/exports");
|
||||
outDir.mkdirs();
|
||||
|
||||
String baseName = sanitize(currentProgram.getName());
|
||||
writeProgramMarkdown(new File(outDir, baseName + ".ghidra.md"));
|
||||
writeFunctionsTsv(new File(outDir, baseName + ".functions.tsv"));
|
||||
writeStringRefsTsv(new File(outDir, baseName + ".string-refs.tsv"));
|
||||
writeCallRefsTsv(new File(outDir, baseName + ".call-refs.tsv"));
|
||||
println("Wrote MX/NMX Ghidra export for " + currentProgram.getName() + " to " + outDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
private void writeProgramMarkdown(File outFile) throws Exception {
|
||||
PrintWriter out = new PrintWriter(new FileWriter(outFile));
|
||||
out.println("# " + currentProgram.getName());
|
||||
out.println();
|
||||
out.println("## Program");
|
||||
out.println();
|
||||
out.println("- Language: `" + currentProgram.getLanguageID() + "`");
|
||||
out.println("- Compiler spec: `" + currentProgram.getCompilerSpec().getCompilerSpecID() + "`");
|
||||
out.println("- Image base: `" + currentProgram.getImageBase() + "`");
|
||||
out.println("- Executable format: `" + currentProgram.getExecutableFormat() + "`");
|
||||
out.println();
|
||||
writeMemoryBlocks(out);
|
||||
writeExternalImports(out);
|
||||
writeExports(out);
|
||||
writeInterestingStringSummary(out);
|
||||
writeInterestingCallerSummary(out);
|
||||
out.close();
|
||||
}
|
||||
|
||||
private void writeMemoryBlocks(PrintWriter out) {
|
||||
out.println("## Memory Blocks");
|
||||
out.println();
|
||||
out.println("| Name | Start | End | Size | R | W | X |");
|
||||
out.println("| --- | ---: | ---: | ---: | :---: | :---: | :---: |");
|
||||
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
||||
out.println("| `" + escape(block.getName()) + "` | `" + block.getStart() + "` | `" + block.getEnd() + "` | " +
|
||||
block.getSize() + " | " + yn(block.isRead()) + " | " + yn(block.isWrite()) + " | " + yn(block.isExecute()) + " |");
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
|
||||
private void writeExternalImports(PrintWriter out) {
|
||||
out.println("## External Imports");
|
||||
out.println();
|
||||
List<String> imports = new ArrayList<String>();
|
||||
SymbolIterator it = currentProgram.getSymbolTable().getExternalSymbols();
|
||||
while (it.hasNext()) {
|
||||
imports.add(it.next().getName(true));
|
||||
}
|
||||
Collections.sort(imports);
|
||||
for (String name : imports) {
|
||||
out.println("- `" + escape(name) + "`");
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
|
||||
private void writeExports(PrintWriter out) {
|
||||
out.println("## Exports and Globals");
|
||||
out.println();
|
||||
out.println("| Name | Address | Function |");
|
||||
out.println("| --- | ---: | --- |");
|
||||
SymbolIterator symbols = currentProgram.getSymbolTable().getSymbolIterator(true);
|
||||
while (symbols.hasNext()) {
|
||||
Symbol symbol = symbols.next();
|
||||
if (symbol.isExternal() || !symbol.isGlobal()) {
|
||||
continue;
|
||||
}
|
||||
String name = symbol.getName();
|
||||
if (name.startsWith("FUN_") || name.startsWith("DAT_") || name.startsWith("LAB_")) {
|
||||
continue;
|
||||
}
|
||||
Function function = getFunctionContaining(symbol.getAddress());
|
||||
out.println("| `" + escape(name) + "` | `" + symbol.getAddress() + "` | `" +
|
||||
(function == null ? "" : escape(function.getName())) + "` |");
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
|
||||
private void writeInterestingStringSummary(PrintWriter out) {
|
||||
out.println("## Interesting Strings and Referencing Functions");
|
||||
out.println();
|
||||
out.println("| Address | String | Referencing Functions |");
|
||||
out.println("| ---: | --- | --- |");
|
||||
for (StringRecord record : collectInterestingStrings()) {
|
||||
out.println("| `" + record.address + "` | `" + escape(record.value) + "` | `" + escape(join(record.functions, "`, `")) + "` |");
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
|
||||
private void writeInterestingCallerSummary(PrintWriter out) {
|
||||
out.println("## Interesting API Callers");
|
||||
out.println();
|
||||
out.println("| Caller | Entry | Call Targets |");
|
||||
out.println("| --- | ---: | --- |");
|
||||
List<FunctionCalls> calls = collectInterestingFunctionCalls();
|
||||
Collections.sort(calls, new Comparator<FunctionCalls>() {
|
||||
public int compare(FunctionCalls a, FunctionCalls b) {
|
||||
return a.function.getEntryPoint().compareTo(b.function.getEntryPoint());
|
||||
}
|
||||
});
|
||||
for (FunctionCalls item : calls) {
|
||||
out.println("| `" + escape(item.function.getName()) + "` | `" + item.function.getEntryPoint() + "` | `" +
|
||||
escape(join(item.targets, "`, `")) + "` |");
|
||||
}
|
||||
out.println();
|
||||
}
|
||||
|
||||
private void writeFunctionsTsv(File outFile) throws Exception {
|
||||
PrintWriter out = new PrintWriter(new FileWriter(outFile));
|
||||
out.println("entry\tname\tsignature\tbody_size\tcall_count\tinteresting_calls");
|
||||
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
|
||||
while (functions.hasNext()) {
|
||||
Function function = functions.next();
|
||||
Set<String> targets = directCallTargets(function);
|
||||
Set<String> interesting = filterInterestingCalls(targets);
|
||||
out.println(function.getEntryPoint() + "\t" + tsv(function.getName()) + "\t" +
|
||||
tsv(function.getSignature().getPrototypeString()) + "\t" + function.getBody().getNumAddresses() + "\t" +
|
||||
targets.size() + "\t" + tsv(join(new ArrayList<String>(interesting), ";")));
|
||||
}
|
||||
out.close();
|
||||
}
|
||||
|
||||
private void writeStringRefsTsv(File outFile) throws Exception {
|
||||
PrintWriter out = new PrintWriter(new FileWriter(outFile));
|
||||
out.println("string_address\tstring_value\tref_from\tref_function");
|
||||
for (StringRecord record : collectInterestingStrings()) {
|
||||
for (String ref : record.references) {
|
||||
out.println(record.address + "\t" + tsv(record.value) + "\t" + tsv(ref) + "\t" + tsv(record.refFunctionByAddress.get(ref)));
|
||||
}
|
||||
}
|
||||
out.close();
|
||||
}
|
||||
|
||||
private void writeCallRefsTsv(File outFile) throws Exception {
|
||||
PrintWriter out = new PrintWriter(new FileWriter(outFile));
|
||||
out.println("caller_entry\tcaller_name\tcall_address\ttarget");
|
||||
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
|
||||
while (functions.hasNext()) {
|
||||
Function function = functions.next();
|
||||
InstructionIterator instructions = currentProgram.getListing().getInstructions(function.getBody(), true);
|
||||
while (instructions.hasNext()) {
|
||||
Instruction instruction = instructions.next();
|
||||
for (Reference ref : instruction.getReferencesFrom()) {
|
||||
if (!ref.getReferenceType().isCall()) {
|
||||
continue;
|
||||
}
|
||||
String target = callTargetName(ref.getToAddress());
|
||||
if (!isInterestingCall(target)) {
|
||||
continue;
|
||||
}
|
||||
out.println(function.getEntryPoint() + "\t" + tsv(function.getName()) + "\t" +
|
||||
instruction.getAddress() + "\t" + tsv(target));
|
||||
}
|
||||
}
|
||||
}
|
||||
out.close();
|
||||
}
|
||||
|
||||
private List<StringRecord> collectInterestingStrings() {
|
||||
List<StringRecord> records = new ArrayList<StringRecord>();
|
||||
Set<String> seenAt = new HashSet<String>();
|
||||
DataIterator data = currentProgram.getListing().getDefinedData(true);
|
||||
while (data.hasNext()) {
|
||||
if (monitor.isCancelled()) {
|
||||
break;
|
||||
}
|
||||
Data item = data.next();
|
||||
Object valueObject = null;
|
||||
try {
|
||||
valueObject = item.getValue();
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
if (valueObject == null) {
|
||||
continue;
|
||||
}
|
||||
String value = valueObject.toString();
|
||||
if (value.length() < 4 || !isInterestingString(value)) {
|
||||
continue;
|
||||
}
|
||||
String address = item.getAddress().toString();
|
||||
if (seenAt.contains(address)) {
|
||||
continue;
|
||||
}
|
||||
seenAt.add(address);
|
||||
StringRecord record = new StringRecord(address, value);
|
||||
for (Reference ref : getReferencesTo(item.getAddress())) {
|
||||
Address from = ref.getFromAddress();
|
||||
Function function = getFunctionContaining(from);
|
||||
String functionName = function == null ? "" : function.getName() + "@" + function.getEntryPoint();
|
||||
record.references.add(from.toString());
|
||||
record.refFunctionByAddress.put(from.toString(), functionName);
|
||||
if (functionName.length() > 0) {
|
||||
record.functions.add(functionName);
|
||||
}
|
||||
}
|
||||
records.add(record);
|
||||
}
|
||||
Collections.sort(records, new Comparator<StringRecord>() {
|
||||
public int compare(StringRecord a, StringRecord b) {
|
||||
return a.address.compareTo(b.address);
|
||||
}
|
||||
});
|
||||
return records;
|
||||
}
|
||||
|
||||
private List<FunctionCalls> collectInterestingFunctionCalls() {
|
||||
List<FunctionCalls> out = new ArrayList<FunctionCalls>();
|
||||
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
|
||||
while (functions.hasNext()) {
|
||||
Function function = functions.next();
|
||||
Set<String> targets = filterInterestingCalls(directCallTargets(function));
|
||||
if (!targets.isEmpty()) {
|
||||
out.add(new FunctionCalls(function, new ArrayList<String>(targets)));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Set<String> directCallTargets(Function function) {
|
||||
Set<String> targets = new LinkedHashSet<String>();
|
||||
InstructionIterator instructions = currentProgram.getListing().getInstructions(function.getBody(), true);
|
||||
while (instructions.hasNext()) {
|
||||
Instruction instruction = instructions.next();
|
||||
for (Reference ref : instruction.getReferencesFrom()) {
|
||||
if (!ref.getReferenceType().isCall()) {
|
||||
continue;
|
||||
}
|
||||
targets.add(callTargetName(ref.getToAddress()));
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
private String callTargetName(Address address) {
|
||||
Function function = getFunctionAt(address);
|
||||
if (function != null) {
|
||||
return function.getName();
|
||||
}
|
||||
Symbol symbol = getSymbolAt(address);
|
||||
if (symbol != null) {
|
||||
return symbol.getName(true);
|
||||
}
|
||||
SymbolTable symbols = currentProgram.getSymbolTable();
|
||||
Symbol primary = symbols.getPrimarySymbol(address);
|
||||
if (primary != null) {
|
||||
return primary.getName(true);
|
||||
}
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
private Set<String> filterInterestingCalls(Set<String> targets) {
|
||||
Set<String> out = new LinkedHashSet<String>();
|
||||
for (String target : targets) {
|
||||
if (isInterestingCall(target)) {
|
||||
out.add(target);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private boolean isInterestingString(String value) {
|
||||
String lower = value.toLowerCase();
|
||||
for (String needle : interestingStringNeedles) {
|
||||
if (lower.contains(needle.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isInterestingCall(String value) {
|
||||
String lower = value.toLowerCase();
|
||||
for (String needle : interestingCallNeedles) {
|
||||
if (lower.contains(needle.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String sanitize(String value) {
|
||||
return value.replaceAll("[^A-Za-z0-9_.-]", "_");
|
||||
}
|
||||
|
||||
private String yn(boolean value) {
|
||||
return value ? "Y" : "";
|
||||
}
|
||||
|
||||
private String join(List<String> items, String separator) {
|
||||
Collections.sort(items);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (i > 0) {
|
||||
builder.append(separator);
|
||||
}
|
||||
builder.append(items.get(i));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String escape(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.replace("\\", "\\\\").replace("`", "\\`").replace("|", "\\|").replace("\r", " ").replace("\n", " ");
|
||||
}
|
||||
|
||||
private String tsv(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.replace("\t", " ").replace("\r", " ").replace("\n", " ");
|
||||
}
|
||||
|
||||
private static class StringRecord {
|
||||
String address;
|
||||
String value;
|
||||
List<String> references = new ArrayList<String>();
|
||||
Map<String, String> refFunctionByAddress = new HashMap<String, String>();
|
||||
List<String> functions = new ArrayList<String>();
|
||||
|
||||
StringRecord(String address, String value) {
|
||||
this.address = address;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FunctionCalls {
|
||||
Function function;
|
||||
List<String> targets;
|
||||
|
||||
FunctionCalls(Function function, List<String> targets) {
|
||||
this.function = function;
|
||||
this.targets = targets;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user