# Ghidra headless analysis This note captures the headless Ghidra pass used to identify native MX/LMX/NMX function boundaries and build Frida hook targets. ## Tooling Ghidra was reused from: ```text C:\Users\dohertj2\Desktop\focas\tools\ghidra_12.0.4_PUBLIC ``` Java was available but not on `PATH`, so headless commands set: ```text JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.10.7-hotspot ``` The AVEVA binaries were staged under a path without spaces: ```text analysis\ghidra\input ``` The headless post-script is: ```text analysis\ghidra\scripts\MxNmxExport.java ``` It exports metadata, not full decompiled source: ```text analysis\ghidra\exports\*.ghidra.md analysis\ghidra\exports\*.functions.tsv analysis\ghidra\exports\*.string-refs.tsv analysis\ghidra\exports\*.call-refs.tsv ``` ## Static targets High-value functions from `LmxProxy.dll`: | Function | RVA | Evidence | | --- | ---: | --- | | `CLMXProxyServer::Write` variant A | `0x12c0c` | references `CLMXProxyServer::Write - Server Handle` | | `CLMXProxyServer::Write` variant B | `0x13280` | references `CLMXProxyServer::Write - Server Handle` | | `CLMXProxyServer::WriteSecured` variant A | `0x12f24` | references secured-write strings | | `CLMXProxyServer::WriteSecured` variant B | `0x135fe` | references secured-write strings | | `CLMXProxyServer::AdviseSupervisory` | `0x142b4` | references advise-supervisory strings | High-value functions from `NmxAdptr.dll`: | Function | RVA | Evidence | | --- | ---: | --- | | `CNmxAdapter::TransferData` | `0x10996` | references `CNmxAdapter::TransferData` strings | | `ProcessDataReceived` | `0x112da` | references invalid NMX request/response strings | | `CNmxAdapter::PutRequest` | `0x15169` | references `CNmxAdapter::PutRequest` strings | | `CNmxAdapter::PutRequestEx` | `0x159c3` | references `CNmxAdapter::PutRequestEx` strings | `LmxProxy.dll` has no direct Winsock callsites in the exported call refs. It is COM, `VARIANT`, `BSTR`, and `SAFEARRAY` heavy. `NmxAdptr.dll` and `NmxSvc.exe` contain the relevant NMX transport/body functions. `NmxSvcps.dll` is confirmed as a MIDL proxy/stub DLL. Its exports call: ```text NdrDllGetClassObject NdrDllCanUnloadNow NdrDllRegisterProxy NdrDllUnregisterProxy NdrCStdStubBuffer_Release ``` ## Runtime hook result Frida hook script: ```text analysis\frida\mx-nmx-trace.js analysis\scripts\run_frida_mx_trace.ps1 analysis\scripts\extract_frida_trace.py ``` Successful traces: ```text captures\022-frida-write-test-int-sequence-106-108 captures\023-frida-write-test-int-sequence-109-111 captures\024-frida-write-test-bool-sequence captures\025-frida-write-test-float-sequence captures\026-frida-write-test-double-sequence captures\027-frida-write-test-string-sequence captures\028-frida-write-test-datetime-sequence captures\029-frida-write-test-int-array captures\030-frida-write-test-bool-array captures\031-frida-write-test-float-array captures\032-frida-write-test-double-array captures\033-frida-write-test-string-array captures\035-frida-write-test-datetime-array-full captures\036-frida-write-secured-test-int captures\037-frida-write-secured2-test-int captures\038-frida-write-secured-protectedvalue captures\039-frida-write-secured-verified-protectedvalue1 captures\040-frida-write-normal-secured-protectedvalue captures\041-frida-write-normal-verified-protectedvalue1 captures\042-frida-write2-test-int-timestamp captures\043-frida-loopback-write-test-int-115 captures\044-frida-loopback-write-test-int-123456789 ``` Trace `023` proves the scalar write value is visible before the localhost transport. Traces `024` through `028` extend that result across bool, float, double, string, and datetime writes. At `CLMXProxyServer::Write` variant A: | Field | Observed value | | --- | --- | | `args[1]` | session handle `1` | | `args[2]` | item handle `1` | | `args[3]` | `0x3`, consistent with `VT_I4` | | `args[5]` | requested int value: `0x6d`, `0x6e`, `0x6f` | | `args[7]` | user/security id `1` | At `CNmxAdapter::PutRequest`: | Field | Observed value | | --- | --- | | `args[6]` | body size `0x28` / `40` | | `args[7]` | body pointer | | body offset `18` | requested int value as little-endian `int32` | The repeated 40-byte write body shape is: ```text 37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 ff ff 00 00 00 00 00 00 00 00 c9 14 b1 08 ``` At `CNmxAdapter::TransferData`, the same 40-byte body is wrapped in an 86-byte buffer. The scalar value appears at offset `64`, which is `46 + 18`. At `CNmxAdapter::ProcessDataReceived`, the callback/update body for the write value is 88 bytes and carries the scalar value at offset `84`. Generated parser output: ```text captures\023-frida-write-test-int-sequence-109-111\frida-events.tsv ``` Value-hit summary from that TSV: | Function | Body size | Hit | | --- | ---: | --- | | `CNmxAdapter::PutRequest` | `40` | `109@18`, `110@18`, `111@18` | | `CNmxAdapter::TransferData` | `86` | `109@64`, `110@64`, `111@64` | | `CNmxAdapter::ProcessDataReceived` | `88` | `109@84`, `110@84`, `111@84` | ## Write-body matrix The machine-readable matrix is: ```text analysis\frida\write-body-matrix.tsv ``` Observed write body encodings: | Type | COM carrier | `PutRequest` | `TransferData` | `ProcessDataReceived` | Encoding notes | | --- | --- | --- | --- | --- | --- | | `int` | `VT_I4` / `0x3`, `args[5]` | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `int32` | | `bool` | `VT_BOOL` / `0xb`, `args[5]` | size `37`, value offset `18` | size `83`, value offset `64` | size `85`, value offset `84` | true is `ff ff ff 00` in the write body and `ff` in the data-change body; false is `00 ff ff 00` and `00` | | `float` | `VT_R4` / `0x4`, `args[5]` | size `40`, value offset `18` | size `86`, value offset `64` | size `88`, value offset `84` | little-endian `float32` | | `double` | `VT_R8` / `0x5`, `args[5]`/`args[6]` | size `44`, value offset `18` | size `90`, value offset `64` | size `92`, value offset `84` | little-endian `float64` | | `string` | `VT_BSTR` / `0x8` | size `58` or `60`, value offset `26` | size `104` or `106`, value offset `72` | size `106` or `108`, value offset `92` | UTF-16LE string payload; body size follows string length | | `datetime` | `VT_DATE` / `0x7`, `args[5]`/`args[6]` | size `86`, value offset `26` | size `132`, value offset `72` | size `98`, value offset `88` | outbound write uses a UTF-16LE display string like `4/25/2026 2:30:00 AM`; callback/update uses FILETIME | The repeated numeric write bodies show a stable 18-byte prefix, scalar slot, and trailing status/counter fields. Variable-width types move the value to offset `26`, leaving an 8-byte descriptor before the UTF-16LE data. ## Array write-body matrix The machine-readable array matrix is: ```text analysis\frida\write-array-body-matrix.tsv ``` Observed array write body encodings: | Type | COM carrier | `PutRequest` | `TransferData` | `ProcessDataReceived` | Encoding notes | | --- | --- | --- | --- | --- | --- | | `int[]` | `SAFEARRAY VT_I4` / `0x2003` | size `86`, first value offset `28` | size `132`, first value offset `74` | size `134`, first value offset `94` | descriptor kind `0x42`, count `10`, width `4`, packed little-endian `int32` | | `bool[]` | `SAFEARRAY VT_BOOL` / `0x200b` | size `66`, first value offset `28` | size `112`, first value offset `74` | size `114`, first value offset `94` | descriptor kind `0x41`, count `10`, width `2`; capture `098` confirmed the x86 COM automation path can project a requested non-repeating .NET `bool[]` into a paired/shifted VARIANT_BOOL wire payload | | `float[]` | `SAFEARRAY VT_R4` / `0x2004` | size `86`, first value offset `28` | size `132`, first value offset `74` | size `134`, first value offset `94` | descriptor kind `0x43`, count `10`, width `4`, packed little-endian `float32` | | `double[]` | `SAFEARRAY VT_R8` / `0x2005` | size `126`, first value offset `28` | size `172`, first value offset `74` | size `174`, first value offset `94` | descriptor kind `0x44`, count `10`, width `8`, packed little-endian `float64` | | `string[]` | `SAFEARRAY VT_BSTR` / `0x2008` | size `256`, first string bytes at offset `41` | size `302`, first string bytes at offset `87` | size `304`, first string bytes at offset `107` | descriptor kind `0x45`; each element is a length-prefixed scalar string-style UTF-16LE record | | `datetime[]` | `SAFEARRAY VT_DATE` / `0x2007` | size `596`, first string bytes at offset `41` | size `642`, first string bytes at offset `87` | size `214`, first FILETIME at offset `94` | outbound values are per-element UTF-16LE display strings; callback/update uses packed FILETIME records | Array bodies use an 11-byte descriptor beginning at body offset `17`: ```text kind_byte 00 00 00 00 element_count:uint16 element_width_or_code:uint32 ``` Packed numeric array values begin at body offset `28`. String and datetime arrays also begin their element records at offset `28`, with the first actual UTF-16LE character at offset `41`. ## Write mode matrix The machine-readable write-mode matrix is: ```text analysis\frida\write-mode-matrix.tsv ``` Findings: | Scenario | Result | | --- | --- | | `WriteSecured` against `TestChildObject.TestInt` | rejected before value-bearing `PutRequest` with `0x80004021` | | `WriteSecured2` against `TestChildObject.TestInt` | rejected before value-bearing `PutRequest` with `E_INVALIDARG` | | `WriteSecured` against `TestMachine_001.ProtectedValue` (`SecuredWrite`) | rejected before value-bearing `PutRequest` with `0x80004021` | | `WriteSecured` against `TestMachine_001.ProtectedValue1` (`VerifiedWrite`) | rejected before value-bearing `PutRequest` with `0x80004021` | | normal `Write` against `ProtectedValue` with fourth argument `2` | succeeds; same bool body shape as scalar bool writes | | normal `Write` against `ProtectedValue1` with fourth argument `3` | succeeds; same bool body shape as scalar bool writes | | `Write2` against `TestChildObject.TestInt` | succeeds; `PutRequest` size remains `40`, value stays at offset `18`, FILETIME appears at offset `24` | Implication: for MXAccess public automation, the supported secured/verified route is the regular `Write` method with the fourth argument set to the Galaxy security classification. The public `WriteSecured*` methods are present in the type library but did not produce a supported value-bearing request in these captures. Later authenticated captures and headless decompile refined this: - `analysis\ghidra\exports\LmxProxy.dll.write-secured-decompile.md` decompiles `FUN_10012f24` (`WriteSecured`) and `FUN_100135fe` (`WriteSecured2`). - `WriteSecured` performs an extra item-record byte check at offset `0x0f` and returns `0x80004021` when that byte is nonzero. The secured and verified bool tags on this node hit that branch even after `AuthenticateUser` succeeds. - `WriteSecured2` skips that item-record flag check. It resolves current and verifier user handles to 16-byte authenticator tokens, copies both the value and timestamp variants, and calls the downstream vtable slot that emitted the observed NMX `0x38` body in captures `113`-`116`. - The shared user-token lookup branch returns `0x80070057` when the user handle is not mapped, explaining the earlier unauthenticated `WriteSecured2` failure against `TestChildObject.TestInt`. ## Transport correlation Combined Frida plus loopback capture: ```text captures\043-frida-loopback-write-test-int-115 captures\044-frida-loopback-write-test-int-123456789 analysis\scripts\run_frida_loopback_capture.ps1 analysis\scripts\map_frida_to_tcp.py captures\043-frida-loopback-write-test-int-115\frida-to-tcp-map.tsv captures\044-frida-loopback-write-test-int-123456789\frida-to-tcp-map.tsv ``` For the same write of `TestChildObject.TestInt = 115`, the mapper extracted the exact Frida `PutRequest`, `TransferData`, and callback bodies and searched the reassembled TCP streams. Result: | Needle | TCP result | | --- | --- | | raw `int32` value `115` | present in several streams, including `::1:49704`, but surrounding bytes show DCE/RPC call IDs/metadata and unrelated product records | | exact 40-byte `PutRequest` body | not found | | exact 86-byte `TransferData` body | not found | | exact 88-byte callback body | not found | Capture `044` repeats the test with `123456789`, which avoids DCE/RPC call-ID ambiguity. That raw scalar is absent from the full pcap payload scan, the `::1:49704` DCE/RPC stubs, and the mixed `127.0.0.1:57415 <-> 57433` stream. This means the native adapter bodies are not copied verbatim onto TCP, and the write value is not exposed as a plain little-endian scalar in the transport for the distinctive-value capture. The next transport task is to decode the DCE/RPC/NDR layer and the mixed local stream messages structurally. ## Managed codec artifact The first .NET 10 managed implementation artifact is: ```text src\MxNativeCodec\MxNativeCodec.csproj src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj ``` This is intentionally template-based. It preserves unknown tag/session/header fields from an observed `PutRequest` body, then rewrites the typed value slot and write index. The tests round-trip the real Frida bytes for bool, int, float, double, string, datetime, timestamped `Write2` int, and the observed int/bool/float/double/string array write bodies. Verification command: ```text dotnet run --project src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj -c Release ``` The harness also now accepts typed array writes: ```text --type=int[] --value="1;2;3" --type=string[] --value="A;B;C" ``` For multiple whole-array writes in one session, `--values=` can use `|` between array values so element commas are not ambiguous. ## Implication The prior pcap-only decode showed local stream framing but did not isolate the application scalar. Headless Ghidra plus Frida closes that gap: the native NMX adapter receives compact bodies where scalar and string/date values are plainly encoded before they enter localhost transport. The next implementation task is to turn this matrix into encoder/decoder tests, then broaden the same approach to arrays, quality/status responses, tag binding messages, and secured-write variants.