cng.sys — SymCrypt point-format element-count generalization
KB5089549
1. Overview
- Unpatched Binary:
cng_unpatched.sys - Patched Binary:
cng_patched.sys - Overall Similarity Score: 0.9685 (96.85%)
- Diff Statistics: 1609 matched functions, 72 changed functions, 1537 identical functions, 0 unmatched functions in either direction.
- Verdict: The most notable code change is inside the statically-linked SymCrypt routine
SymCryptEckeySetValue, where a hardcoded public-key element-count multiplier of2is replaced by a lookup into the pre-existingSymCryptEcpointFormatNumberofElementstable using the point-format argument the callers already pass. For every elliptic-curve key import reachable in the unpatched build the looked-up element count is2, so the check is behaviorally identical. The generalization exists to support the additional point formats introduced by this build's post-quantum / composite-algorithm SymCrypt uplift. No reachable, attacker-controlled out-of-bounds condition is demonstrable in the unpatched build. This is library generalization, not a security fix.
2. Change Summary
Change Class: Library generalization (hardcoded constant replaced by table lookup)
- Severity: None (informational)
- Affected Function:
SymCryptEckeySetValue@0x180041B98(Unpatched) /SymCryptEckeySetValue@0x180041B50(Patched)
What changed:
When validating the size of imported elliptic-curve public-key material, the unpatched SymCryptEckeySetValue doubles the field-element size (mov eax,[rsi+14h]; add eax,eax) and compares it against the caller-supplied public-key length in r9. The patched build instead reads the point-format index that the callers already pass on the stack (arg5), indexes the SymCryptEcpointFormatNumberofElements table with it, and multiplies the field-element size by the resulting element count (imul eax,[rsi+14h]).
SymCryptEcpointFormatNumberofElements is a pre-existing SymCrypt read-only table. It is present in both binaries and is already referenced by other SymCrypt routines (for example the SymCryptEcpointSetValue family) in both builds. The patch only adds one additional use of it, inside SymCryptEckeySetValue.
Why this is not a reachable security issue:
1. The point-format index is not attacker-controlled blob data. The callers compute it as (*(curve_descriptor + 4) != 3) ? 2 : 1 (cmp dword [r8+4], 3; setnz al; inc eax). The field at +4 is the SymCrypt curve type; value 3 is the Montgomery curve type (X-only point format). This is a static property of the selected curve/algorithm, not a value taken from the imported key blob.
2. The size being validated (r9, the 4th argument cbPublicKey) is not the raw attacker-supplied blob length. The reachable callers build a two-coordinate public-key buffer and pass its length, computed as 2 * field_element_size (stored in var_18 in ImportEccKeyBlobWithoutParameters). The equality check therefore compares two caller-derived quantities.
3. The buffer that later receives key material is allocated from an internal key-object field (mov edi,[rsi+3Ch]; call SymCryptCallbackAlloc), not from the validated cbPublicKey. The size validation is an accept/reject gate returning STATUS_INVALID_PARAMETER (0x800E) on mismatch; it does not itself drive a copy length from attacker input.
4. For every elliptic-curve import reachable through the three callers in the unpatched build, the curve is short-Weierstrass, so the computed index is 2 and SymCryptEcpointFormatNumberofElements[2] == 2, identical to the hardcoded doubling. There is no reachable input in the unpatched build for which the hardcoded 2 yields a different (let alone exploitable) result.
Entry Point and Call Chain (reachable, but benign):
1. BCryptImportKeyPair — exported CNG API accessible from user mode.
2. Provider key-import dispatch.
3. MSCryptEccImportKeyPair (sub_18007b6e8) — ECC key import wrapper.
4. ImportEccKeyBlobWithoutParameters @ 0x18004166C (unpatched) / 0x18004161C (patched) — key blob parser.
5. SymCryptEckeySetValue — the size validation described above.
3. Pseudocode Diff
The point-format index argument is passed by the callers in both versions; the patch makes the function use it to select the element count from the table instead of doubling unconditionally.
// UNPATCHED (SymCryptEckeySetValue @ 0x180041b98)
// rsi = internal EC key/curve object, r9 = caller-supplied cbPublicKey (= 2 * field_element_size)
if (cbPublicKey != 0) {
int expected = *(int*)(rsi + 0x14) * 2; // hardcoded doubling
if (cbPublicKey != expected)
return STATUS_INVALID_PARAMETER; // 0x800E
}
// PATCHED (SymCryptEckeySetValue @ 0x180041b50)
// arg5 = point-format index (2 for short-Weierstrass, the only reachable value here)
if (cbPublicKey != 0) {
int element_count = SymCryptEcpointFormatNumberofElements[arg5];
int expected = *(int*)(rsi + 0x14) * element_count;
if (cbPublicKey != expected)
return STATUS_INVALID_PARAMETER; // 0x800E
}
4. Assembly Analysis
UNPATCHED (SymCryptEckeySetValue @ 0x180041B98), public-key size check:
0000000180041C68 test r9, r9 ; r9 = cbPublicKey (caller-derived = 2 * field_element_size)
0000000180041C6B jz short loc_180041C77
0000000180041C6D mov eax, [rsi+14h] ; field_element_size
0000000180041C70 add eax, eax ; * 2 (hardcoded element count)
0000000180041C72 cmp r9, rax
0000000180041C75 jnz short loc_180041C44 ; -> STATUS_INVALID_PARAMETER (0x800E)
0000000180041C77 mov edi, [rsi+3Ch] ; internal key-object alloc size (NOT cbPublicKey)
0000000180041C80 call SymCryptCallbackAlloc
PATCHED (SymCryptEckeySetValue @ 0x180041B50), same check via table lookup:
0000000180041BE4 lea rcx, cs:180000000h ; image base, loaded earlier
0000000180041C24 test r9, r9
0000000180041C27 jz short loc_180041C41
0000000180041C29 movsxd rax, [rsp+0C8h+arg_28] ; arg5 = point-format index
0000000180041C31 mov eax, ds:rva SymCryptEcpointFormatNumberofElements[rcx+rax*4]
0000000180041C38 imul eax, [rsi+14h] ; field_element_size * element_count
0000000180041C3C cmp r9, rax
0000000180041C3F jnz short loc_180041C00 ; -> STATUS_INVALID_PARAMETER (0x800E)
0000000180041C41 mov edi, [rsi+3Ch] ; internal key-object alloc size
0000000180041C4A call SymCryptCallbackAlloc
Caller ImportEccKeyBlobWithoutParameters, argument setup before the call (unpatched @ 0x18004199C, patched @ 0x180041954). Both builds compute and pass the point-format index; the patched build additionally stores a constant 2 in one added stack argument slot:
; UNPATCHED @ 0x18004198B
0000000180041975 mov r9, [rbp+var_18] ; cbPublicKey = 2 * field_element_size
000000018004198B cmp dword ptr [r8+4], 3 ; curve type (3 = Montgomery / X-only)
0000000180041993 setnz al
0000000180041996 inc eax
0000000180041998 mov [rsp+70h+var_50], eax ; arg5 = (type != 3) ? 2 : 1
000000018004199C call SymCryptEckeySetValue
; PATCHED @ 0x18004193B
0000000180041925 mov r9, [rbp+var_18] ; cbPublicKey = 2 * field_element_size
000000018004193B mov dword ptr [rsp+70h+var_48], 2 ; added constant argument
0000000180041943 cmp dword ptr [r8+4], 3
000000018004194B setnz al
000000018004194E inc eax
0000000180041950 mov [rsp+70h+var_50], eax ; arg5 = (type != 3) ? 2 : 1
0000000180041954 call SymCryptEckeySetValue
5. Trigger Conditions
There is no security-relevant trigger. To reach SymCryptEckeySetValue, a caller invokes BCryptImportKeyPair with an ECC key blob and the provider dispatches to ImportEccKeyBlobWithoutParameters (or ImportEccKeyBlobWithParameters / SetEccPrivateKeyVal). For the short-Weierstrass curves reachable through these paths the computed point-format index is 2, so the patched table lookup returns the same value as the unpatched hardcoded doubling. The size check is an equality gate between two caller-derived quantities and returns STATUS_INVALID_PARAMETER on mismatch; it does not gate an attacker-controlled copy length.
6. Reachability and Impact Assessment
- The point-format index is a static property of the selected curve (Montgomery vs. short-Weierstrass), not attacker-controlled blob content.
- The validated public-key length (
r9) is derived by the caller as2 * field_element_size, not taken raw from the blob. - The internal buffer allocation uses
[rsi+3Ch](an internal key-object field), independent of the validated length. - For every reachable elliptic-curve import in the unpatched build the looked-up element count is
2, identical to the hardcoded doubling; no reachable input produces a divergent result. - No out-of-bounds read or write, and no privilege-escalation primitive, is demonstrable from this change in the unpatched build.
7. Verification Notes
SymCryptEcpointFormatNumberofElementsis present in both binaries and already used by other SymCrypt routines in both builds (for example around0x18002590F/0x180025A0Eunpatched and0x180025B9C/0x18002694Fpatched). The patch adds a single new use of the table insideSymCryptEckeySetValueat0x180041C31(patched only).- The three callers in both builds compute the index identically as
(curve_type != 3) ? 2 : 1. The only build difference in the callers is the patched builds' addition of a constant2in a new stack argument slot, consistent with a SymCrypt library signature revision rather than a targeted bounds fix. - This build carries a broad SymCrypt uplift: composite-encoding key routines (for example
SymCryptEckeySetValueCompositeEncodingPk/...Sk) are absent from the unpatched binary and present in the patched binary, and the patched binary contains additional ML-DSA / PQ-DSA functions. The generalized point-format element-count lookup supports the additional point formats these new algorithms use.
8. Changed Functions — Full Triage
SymCryptEckeySetValue@0x180041b98/0x180041b50(Not security-relevant): Hardcoded public-key element-count doubling (add eax, eax) replaced bySymCryptEcpointFormatNumberofElements[index] * field_element_sizeusing the point-format index the callers already pass. For all reachable EC imports in the unpatched build the index is2and the table entry is2, so behavior is unchanged. Generalization for new point formats; no reachable security impact.ImportEccKeyBlobWithoutParameters@0x18004166c/0x18004161c(Not security-relevant): Primary caller. Computes the point-format index(curve_type != 3) ? 2 : 1in both builds and builds a two-coordinate public-key buffer of2 * field_element_size. Patched build additionally passes a constant2in an added stack argument slot.ImportEccKeyBlobWithParameters@0x180079c38/0x18007a558(Not security-relevant): Second caller. Same index computation; pre-existing integer-overflow checks on blob-size arithmetic are present in both builds and unchanged. Patched build passes the same added constant argument.SetEccPrivateKeyVal@0x18007bab0/0x18007c3a8(Not security-relevant): Third caller. Same index computation and same added constant argument in the patched build.BCryptOpenAlgorithmProvider(Behavioral): Post-quantum algorithm-family registration (ML-DSA / Composite-ML-DSA) added in this build.MSCryptMlDsaExportKeyPair (sub_18007c9f0)/GetSignatureInterface (sub_1800797c0)(Behavioral): Low-similarity match; post-quantum signature/algorithm interface support with string comparisons forECDSA_P256,ML-DSA, and related names.MSCryptMlDsaGetProperty (sub_18007cd90)/MSCryptPqDsaFinalizeKeyPair (sub_18007d4f0)(Behavioral): Low-similarity match; handling for new blob tags (YKSM,LASM) and algorithm types (0x3000c,0x3000d).VsmOpenAlgorithmProvider (sub_1800482ec)/VsmOpenAlgorithmProvider (sub_18004856c)(Behavioral): Provider initialization structure expanded by 16 bytes (0x3b8->0x3c8) to accommodate 2 new algorithm types.- Recompilation Changes:
SymCryptDlkeySetValue (sub_180092860) / (sub_18009588c),SymCryptEcurveInitialize (sub_180024dfc) / (sub_180022d74), andSymCryptRsakeySetValueInternal (sub_18008e82c) / (sub_18008ff18)show register reallocation and call-target relocation from recompilation, with no security-relevant logic change.
9. Unmatched Functions
The diff reports no added or removed functions at the matched-function level. Within the changed functions, this build adds composite-encoding and additional post-quantum routines that are absent from the unpatched binary (for example SymCryptEckeySetValueCompositeEncodingPk / ...Sk).
10. Confidence & Caveats
- Confidence Level: High. The assembly diff shows the hardcoded doubling replaced by a lookup into a table that already exists and is already used in both builds; the callers compute a static per-curve index and, for the reachable short-Weierstrass imports, that index selects element count
2, identical to the previous behavior. - Basis for the downgrade: The point-format index is a static per-curve property, the validated length is caller-derived rather than raw attacker input, and the buffer allocation is driven by an internal key-object field. No reachable input in the unpatched build produces a divergent multiplier or an out-of-bounds condition. The change generalizes the routine for the additional point formats used by this build's post-quantum / composite-algorithm SymCrypt uplift.