docs(grpc-events): trace the ExchangeKey token crypto — KDF=SHA256(secret); token construction localized
Frida-hooked Windows CNG (scripts/frida/aahclientmanaged-cng-exchangekey.js) during a real native ExchangeKey to recover the token derivation: - The ECDH + KDF are standard CNG driven by managed System.Security.Cryptography .ECDiffieHellmanCng: NCryptSecretAgreement (P-256) -> NCryptDeriveKey(KDF=HASH, SHA256, 32 bytes). So the derived key = SHA256(ECDH shared secret). - "ECK1" is the standard CNG BCRYPT_ECCPUBLIC_BLOB magic (P-256), confirming our BuildExchangeKeyClientHello wire format. - The 26-byte token (constant 0x8e marker) is a custom construction over the derived key: a 528-candidate offline cracker (HMAC/SHA/AES-GCM/CBC/CTR over the derived key x request slices x creds) found no match, and it matches none of the traced hash digests. It is built in aahClientManaged's C++/CLI <Module> code between the DeriveKeyMaterial call and the openParameters assembly. Next: ILSpy cannot decompile the mixed-mode assembly (crashes, exit 70); use dnlib (IL-level) to dump the <Module> method referencing DeriveKeyMaterial and read the post-derive token construction. 2 of 3 layers cleared (key exchange + client key); the 3rd (token) is localized, pending dnlib extraction. Orchestrator stays on v6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
@@ -245,12 +245,38 @@ Path B (ExchangeKey ECDH): 132/171 AuthenticationFailed "EstablishConnection
|
||||
```
|
||||
|
||||
So the ECDH cleared the client-key check; the remaining blocker is **authentication**: the 26-byte
|
||||
v8 credential token must be a *valid* value derived from the ECDH shared secret (not zeros). This is
|
||||
the token KDF/cipher — the part that is not yet reverse-engineered and that would require analyzing
|
||||
AVEVA's native ExchangeKey/credential crypto to recover the derivation (the .NET-shipped result stays
|
||||
pure managed either way). The "Path B-lite" hypothesis (zeroed token rides through after key
|
||||
agreement) is therefore disproven at the auth layer — 2 of 3 layers are cleared, the 3rd is the
|
||||
credential-token derivation. ExchangeKey + the v8 serializer are committed and ready; the orchestrator
|
||||
stays on v6 (set `eventConnection: true` to re-arm once the token KDF lands). The token-loop routing
|
||||
guardrail (`HistorianGrpcHandshakeRoutingTests`) was scoped to the closure so the legitimate
|
||||
ExchangeKey call is allowed while still pinning that the Negotiate token loop never routes there.
|
||||
v8 credential token must be a *valid* value derived from the ECDH shared secret (not zeros).
|
||||
|
||||
### Token crypto traced 2026-06-23 (Frida → Windows CNG) — KDF found, token construction still open
|
||||
|
||||
Hooked Windows CNG (`bcrypt.dll`/`ncrypt.dll`) while the native harness ran a real ExchangeKey
|
||||
(`scripts/frida/aahclientmanaged-cng-exchangekey.js` + `artifacts/.../cng-trace.py`). Findings:
|
||||
|
||||
- **The ECDH + KDF are standard CNG, driven by managed `System.Security.Cryptography.ECDiffieHellmanCng`**
|
||||
(backtrace top frame = `System.Core.ni.dll`; the caller is aahClientManaged's C++/CLI `<Module>`):
|
||||
`NCryptSecretAgreement` (P-256) → `NCryptDeriveKey(KDF=HASH, HASH_ALGORITHM=SHA256, 32 bytes)`. So the
|
||||
derived key = **SHA256(ECDH shared secret)** — exactly `ECDiffieHellmanCng{ KeyDerivationFunction=Hash,
|
||||
HashAlgorithm=SHA256 }.DeriveKeyMaterial(...)`. Our managed `DeriveExchangeKeySecret` should switch to
|
||||
this (SHA256 of the raw agreement) to match.
|
||||
- **`"ECK1"` is NOT AVEVA-custom** — it is the standard Windows CNG `BCRYPT_ECCPUBLIC_BLOB` magic for
|
||||
P-256 (`NCryptExportKey`/`ImportKey` emit exactly `ECK1 + len(32) + X(32) + Y(32)`), confirming our
|
||||
`BuildExchangeKeyClientHello` wire format is correct.
|
||||
- **The 26-byte token is a custom construction that is not yet reproduced.** Correlated one run's
|
||||
derived key (`SHA256(secret)`) with that run's token (from the IL openParameters capture): a
|
||||
528-candidate offline cracker (HMAC/SHA/AES-GCM/CBC/CTR over the derived key × request slices ×
|
||||
creds) found **no match**, and the token matches **none** of the traced hash digests. The token
|
||||
starts with a constant `0x8e` marker in both captured runs (so it is structured, not raw cipher
|
||||
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 `<Module>` both crash,
|
||||
exit 70). Use **dnlib** (IL-level, won't choke on the native parts) to dump the `<Module>` method that
|
||||
references `ECDiffieHellmanCng.DeriveKeyMaterial` and read the post-derive token construction, then
|
||||
implement it managed-side and re-test (non-destructive).
|
||||
|
||||
**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
|
||||
orchestrator stays on v6 (set `eventConnection: true` to re-arm once the token construction lands). The
|
||||
token-loop routing guardrail (`HistorianGrpcHandshakeRoutingTests`) was scoped to the closure so the
|
||||
legitimate ExchangeKey call is allowed while still pinning that the Negotiate token loop never routes
|
||||
there.
|
||||
|
||||
Reference in New Issue
Block a user