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>
439 lines
16 KiB
Java
439 lines
16 KiB
Java
// 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;
|
|
}
|
|
}
|
|
}
|