diff --git a/docs/reverse-engineering/grpc-event-query-capture.md b/docs/reverse-engineering/grpc-event-query-capture.md index 837f07f..95f021e 100644 --- a/docs/reverse-engineering/grpc-event-query-capture.md +++ b/docs/reverse-engineering/grpc-event-query-capture.md @@ -269,10 +269,36 @@ Hooked Windows CNG (`bcrypt.dll`/`ncrypt.dll`) while the native harness ran a re output). It is built in managed code between the `DeriveKeyMaterial` call and the openParameters assembly. -**Next step:** ILSpy cannot decompile the mixed-mode assembly (full-assembly and `` both crash, -exit 70). Use **dnlib** (IL-level, won't choke on the native parts) to dump the `` method that -references `ECDiffieHellmanCng.DeriveKeyMaterial` and read the post-derive token construction, then -implement it managed-side and re-test (non-destructive). +**dnlib IL extraction 2026-06-23 — the token scheme is fully reverse-engineered.** ILSpy can't +decompile the mixed-mode assembly (crashes), but loading `dnlib` in PowerShell and scanning the IL +recovered the whole construction: + +- **`::CHistoryConnectionGrpc.GetClientKey`** is the ECDH driver: `new ECDiffieHellmanCng()` + → `KeyDerivationFunction = Hash`, `HashAlgorithm = SHA256`, `KeySize = 256` → + `GrpcHistoryClient.ExchangeKey(strHandle, ourPubKey.ToByteArray(), out serverPub, out err)` → + `CngKey.Import(serverPub, CngKeyBlobFormat.EccPublicBlob)` → **`DeriveKeyMaterial`** = the 32-byte + client key = **`SHA256(ECDH shared secret)`**. (So our managed side should derive the key the same + way — `ECDiffieHellman` raw agreement then SHA256, or equivalently `DeriveKeyFromHash(..., SHA256)`.) +- **The 26-byte token is built by `aahClientCommon.CClientBase.ConfigureOpenConnection`** (the lone + caller of `GetClientKey`) using the **`HistorianCrypto.NRC4_V2.aahCryptV2`** scheme — a custom + **MD5-keyed RC4 stream cipher with a version prefix**: + - `aahCryptV2.body`/`HashData` = **MD5** (verified: the IL loads MD5 round constants `0xd76aa478`… + and rotates 7/12/17/22). + - `aahCryptV2.prepare_key` = standard **RC4 KSA** seeding the 256-byte S-box from a **16-byte (MD5)** + key (`std.array`). + - `aahCryptV2.enc_buffer` = `MD5(...)` → key, then **`rc4encrypt`** the body; `enc` prepends a + scheme **prefix** (`NRC4_V2.PrefixV2` / `InnerPrefixV2`) — the constant `0x8e` token marker. + - `from_GUID` keys the cipher from a GUID string. + +So the token = `prefix + RC4(plaintext, key = MD5(keyMaterial))`, where the key material ties back to +the `SHA256(ECDH secret)` client key. **This is 100% reproducible in pure managed code** (RC4 + MD5 +are ~40 lines; nothing AVEVA ships). + +**Remaining to finish (next cycle):** read `ConfigureOpenConnection`'s exact wiring (which value is +MD5'd for the RC4 key, what plaintext is encrypted, the exact prefix bytes — a little more dnlib IL), +implement `aahCryptV2` (RC4+MD5+prefix) managed-side, set the v8 token = that, and live-test +(non-destructive). The offline correlation data (one run's derived key + token + openParameters) is +captured under `artifacts/.../` to validate the managed reproduction before going live. **2 of 3 layers cleared** (key exchange + client key); the 3rd (token construction) is localized to a specific managed method, pending dnlib extraction. ExchangeKey + the v8 serializer are committed; the