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.
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// Frida hook for the native ExchangeKey credential-token crypto (Windows CNG / bcrypt.dll).
|
||||
// Traces the ECDH secret agreement, the KDF (with its parameter list), symmetric-key import, and
|
||||
// encrypt/hash so the 26-byte v8 credential-token derivation can be reconstructed in managed code.
|
||||
// Reverse-engineering aid only — observes the native client; nothing is shipped from here.
|
||||
'use strict';
|
||||
|
||||
function resolve(modName, fnName) {
|
||||
let m = null;
|
||||
try { m = Process.getModuleByName(modName); } catch (e) {
|
||||
try { m = Module.load(modName); } catch (e2) { return null; }
|
||||
}
|
||||
try { return m.findExportByName(fnName); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
function dump(label, ptr, len) {
|
||||
if (ptr.isNull() || len <= 0) { console.log(label + ' <empty>'); return; }
|
||||
const n = Math.min(len, 256);
|
||||
console.log(label + ' (' + len + ' bytes)\n' + hexdump(ptr, { length: n, header: false, ansi: false }));
|
||||
}
|
||||
|
||||
function hook(modName, fnName, onEnter, onLeave) {
|
||||
const addr = resolve(modName, fnName);
|
||||
if (!addr) { console.log('[skip] ' + modName + '!' + fnName + ' not found'); return; }
|
||||
Interceptor.attach(addr, { onEnter: onEnter, onLeave: onLeave });
|
||||
console.log('[hooked] ' + modName + '!' + fnName);
|
||||
}
|
||||
|
||||
// BCryptOpenAlgorithmProvider(phAlgorithm, pszAlgId, pszImplementation, dwFlags) — names every algo used.
|
||||
hook('bcrypt.dll', 'BCryptOpenAlgorithmProvider', function (a) {
|
||||
console.log('[OpenAlgorithmProvider] algId=' + (a[1].isNull() ? '?' : a[1].readUtf16String()));
|
||||
});
|
||||
|
||||
// BCryptSecretAgreement(hPrivKey, hPubKey, *phAgreedSecret, flags)
|
||||
hook('bcrypt.dll', 'BCryptSecretAgreement', function (a) {
|
||||
console.log('[SecretAgreement] hPriv=' + a[0] + ' hPub=' + a[1]);
|
||||
});
|
||||
|
||||
// Decode a BCryptBufferDesc parameter list (used by BCryptDeriveKey) into (type -> bytes).
|
||||
function dumpParamList(pParamList) {
|
||||
if (pParamList.isNull()) { console.log(' paramList <null>'); return; }
|
||||
const cBuffers = pParamList.add(4).readU32(); // ULONG ulVersion; ULONG cBuffers;
|
||||
const pBuffers = pParamList.add(8).readPointer(); // BCryptBuffer* pBuffers;
|
||||
const names = { 0: 'HASH_ALGORITHM', 1: 'SECRET_PREPEND', 2: 'SECRET_APPEND', 3: 'HMAC_KEY',
|
||||
4: 'TLS_PRF_LABEL', 5: 'TLS_PRF_SEED', 6: 'SECRET_HANDLE', 8: 'SP80056A_CONCAT',
|
||||
0xD: 'LABEL', 0xE: 'CONTEXT', 0xF: 'SALT', 0x10: 'ITERATION_COUNT' };
|
||||
console.log(' paramList cBuffers=' + cBuffers);
|
||||
for (let i = 0; i < cBuffers; i++) {
|
||||
const b = pBuffers.add(i * 16); // { ULONG cbBuffer; ULONG BufferType; PVOID pvBuffer; }
|
||||
const cb = b.readU32();
|
||||
const type = b.add(4).readU32();
|
||||
const pv = b.add(8).readPointer();
|
||||
const tn = names[type] || ('0x' + type.toString(16));
|
||||
if (type === 0 || type === 4 || type === 0xD) { // string-ish (hash alg name / label)
|
||||
console.log(' [' + tn + '] ' + (pv.isNull() ? '?' : pv.readUtf16String()));
|
||||
} else {
|
||||
dump(' [' + tn + ']', pv, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BCryptDeriveKey(hSecret, pwszKDF, *pParamList, pbDerivedKey, cbDerivedKey, *pcbResult, flags)
|
||||
hook('bcrypt.dll', 'BCryptDeriveKey', function (a) {
|
||||
this.kdf = a[1].isNull() ? '?' : a[1].readUtf16String();
|
||||
this.outKey = a[3]; this.pcb = a[5];
|
||||
console.log('[DeriveKey] KDF=' + this.kdf + ' cbDerivedKey=' + a[4].toInt32());
|
||||
dumpParamList(a[2]);
|
||||
}, function () {
|
||||
const n = this.pcb.isNull() ? 0 : this.pcb.readU32();
|
||||
dump('[DeriveKey] derived', this.outKey, n);
|
||||
});
|
||||
|
||||
hook('bcrypt.dll', 'BCryptDeriveKeyPBKDF2', function (a) {
|
||||
console.log('[PBKDF2] cbPassword=' + a[2].toInt32() + ' cbSalt=' + a[4].toInt32() + ' iter=' + a[5]);
|
||||
dump(' password', a[1], a[2].toInt32());
|
||||
dump(' salt', a[3], a[4].toInt32());
|
||||
});
|
||||
|
||||
// BCryptGenerateSymmetricKey(hAlg, *phKey, pbKeyObject, cbKeyObject, pbSecret, cbSecret, flags) — the actual key bytes.
|
||||
hook('bcrypt.dll', 'BCryptGenerateSymmetricKey', function (a) {
|
||||
dump('[GenerateSymmetricKey] keyBytes', a[4], a[5].toInt32());
|
||||
});
|
||||
|
||||
// BCryptEncrypt(hKey, pbIn, cbIn, *pPad, pbIV, cbIV, pbOut, cbOut, *pcbResult, flags)
|
||||
hook('bcrypt.dll', 'BCryptEncrypt', function (a) {
|
||||
this.out = a[6]; this.pcb = a[8];
|
||||
dump('[Encrypt] plaintext', a[1], a[2].toInt32());
|
||||
dump('[Encrypt] IV', a[4], a[5].toInt32());
|
||||
}, function () {
|
||||
const n = this.pcb.isNull() ? 0 : this.pcb.readU32();
|
||||
dump('[Encrypt] ciphertext', this.out, n);
|
||||
});
|
||||
|
||||
// Hash path (in case the token is a keyed hash rather than a cipher).
|
||||
hook('bcrypt.dll', 'BCryptHashData', function (a) {
|
||||
dump('[HashData] input', a[1], a[2].toInt32());
|
||||
});
|
||||
hook('bcrypt.dll', 'BCryptFinishHash', function (a) {
|
||||
this.out = a[1]; this.cb = a[2].toInt32();
|
||||
}, function () {
|
||||
dump('[FinishHash] digest', this.out, this.cb);
|
||||
});
|
||||
|
||||
// ---- NCrypt (CNG key-storage layer) — the likely home of the ECDH ExchangeKey + token crypto ----
|
||||
|
||||
// NCryptSecretAgreement(hPrivKey, hPubKey, *phAgreedSecret, dwFlags)
|
||||
hook('ncrypt.dll', 'NCryptSecretAgreement', function (a) {
|
||||
console.log('[NCryptSecretAgreement] hPriv=' + a[0] + ' hPub=' + a[1]);
|
||||
console.log(' backtrace (addr -> module+offset):');
|
||||
Thread.backtrace(this.context, Backtracer.ACCURATE).slice(0, 14).forEach(function (addr) {
|
||||
const m = Process.findModuleByAddress(addr);
|
||||
if (m) {
|
||||
console.log(' ' + addr + ' ' + m.name + '+0x' + addr.sub(m.base).toString(16));
|
||||
} else {
|
||||
console.log(' ' + addr + ' <JIT/unknown>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// NCryptDeriveKey(hSharedSecret, pwszKDF, *pParameterList, pbDerivedKey, cbDerivedKey, *pcbResult, dwFlags)
|
||||
hook('ncrypt.dll', 'NCryptDeriveKey', function (a) {
|
||||
this.kdf = a[1].isNull() ? '?' : a[1].readUtf16String();
|
||||
this.outKey = a[3]; this.pcb = a[5];
|
||||
console.log('[NCryptDeriveKey] KDF=' + this.kdf + ' cbDerivedKey=' + a[4].toInt32());
|
||||
dumpParamList(a[2]);
|
||||
}, function () {
|
||||
const n = this.pcb.isNull() ? 0 : this.pcb.readU32();
|
||||
dump('[NCryptDeriveKey] derived', this.outKey, n);
|
||||
});
|
||||
|
||||
// NCryptEncrypt(hKey, pbInput, cbInput, *pPaddingInfo, pbOutput, cbOutput, *pcbResult, dwFlags)
|
||||
hook('ncrypt.dll', 'NCryptEncrypt', function (a) {
|
||||
this.out = a[4]; this.pcb = a[6];
|
||||
dump('[NCryptEncrypt] plaintext', a[1], a[2].toInt32());
|
||||
}, function () {
|
||||
const n = this.pcb.isNull() ? 0 : this.pcb.readU32();
|
||||
dump('[NCryptEncrypt] ciphertext', this.out, n);
|
||||
});
|
||||
|
||||
// NCryptImportKey(hProvider, hImportKey, pszBlobType, *pParameterList, *phKey, pbData, cbData, dwFlags)
|
||||
hook('ncrypt.dll', 'NCryptImportKey', function (a) {
|
||||
console.log('[NCryptImportKey] blobType=' + (a[2].isNull() ? '?' : a[2].readUtf16String()));
|
||||
dump(' blob', a[5], a[6].toInt32());
|
||||
});
|
||||
|
||||
// NCryptExportKey(hKey, hExportKey, pszBlobType, *pParameterList, pbOutput, cbOutput, *pcbResult, dwFlags)
|
||||
hook('ncrypt.dll', 'NCryptExportKey', function (a) {
|
||||
this.blobType = a[2].isNull() ? '?' : a[2].readUtf16String();
|
||||
this.out = a[4]; this.pcb = a[6];
|
||||
}, function () {
|
||||
const n = this.pcb.isNull() ? 0 : this.pcb.readU32();
|
||||
console.log('[NCryptExportKey] blobType=' + this.blobType);
|
||||
dump(' blob', this.out, n);
|
||||
});
|
||||
|
||||
hook('ncrypt.dll', 'NCryptOpenStorageProvider', function (a) {
|
||||
console.log('[NCryptOpenStorageProvider] ' + (a[1].isNull() ? '?' : a[1].readUtf16String()));
|
||||
});
|
||||
|
||||
// BCrypt EC key operations (in case the ECDH is bcrypt but uses import/export rather than DeriveKey).
|
||||
hook('bcrypt.dll', 'BCryptImportKeyPair', function (a) {
|
||||
console.log('[BCryptImportKeyPair] blobType=' + (a[2].isNull() ? '?' : a[2].readUtf16String()) + ' cb=' + a[5].toInt32());
|
||||
dump(' blob', a[4], a[5].toInt32());
|
||||
});
|
||||
hook('bcrypt.dll', 'BCryptExportKey', function (a) {
|
||||
this.blobType = a[2].isNull() ? '?' : a[2].readUtf16String();
|
||||
this.out = a[3]; this.pcb = a[5];
|
||||
}, function () {
|
||||
const n = this.pcb.isNull() ? 0 : this.pcb.readU32();
|
||||
console.log('[BCryptExportKey] blobType=' + this.blobType);
|
||||
dump(' blob', this.out, n);
|
||||
});
|
||||
|
||||
console.log('=== CNG ExchangeKey crypto hooks installed ===');
|
||||
Reference in New Issue
Block a user