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 of 2 is replaced by a lookup into the pre-existing SymCryptEcpointFormatNumberofElements table 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 is 2, 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 as 2 * 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

  • SymCryptEcpointFormatNumberofElements is present in both binaries and already used by other SymCrypt routines in both builds (for example around 0x18002590F/0x180025A0E unpatched and 0x180025B9C/0x18002694F patched). The patch adds a single new use of the table inside SymCryptEckeySetValue at 0x180041C31 (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 constant 2 in 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 by SymCryptEcpointFormatNumberofElements[index] * field_element_size using the point-format index the callers already pass. For all reachable EC imports in the unpatched build the index is 2 and the table entry is 2, 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 : 1 in both builds and builds a two-coordinate public-key buffer of 2 * field_element_size. Patched build additionally passes a constant 2 in 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 for ECDSA_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), and SymCryptRsakeySetValueInternal (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.