1. Overview

Field Value
Unpatched binary ci_unpatched.dll
Patched binary ci_patched.dll
Overall similarity 0.9804
Matched functions 2518
Changed functions 38
Identical functions 2480
Unmatched (unpatched → patched) 0
Unmatched (patched → unpatched) 0

Verdict: This patch delivers no code-integrity signature- or policy-validation vulnerability fix. Every CI-side change is the retirement of a WIL servicing feature flag whose staged behavior is graduated to unconditional in code. Three CodeIntegrity Feature_Servicing_* flags are retired module-wide (all references drop to zero in the patched build): Feature_Servicing_CodeIntegrityWinTcbPplTocTou, Feature_Servicing_CodeIntegrityMissing3077Events, and Feature_Servicing_CodeIntegrityNonConformantNotBlockedByResources. The retirements do not all move in the same security direction — retiring WinTcbPplTocTou makes a Protected Process Light (PPL) re-validation branch in CiRevalidateImage (sub_180065AB0) run unconditionally (a hardening direction), while retiring NonConformantNotBlockedByResources in CipApplySIPolicyUMCI (sub_1800DA3E4) makes a permissive early-return path always-on (the opposite direction). That mixed set confirms feature-staging lifecycle churn rather than a targeted security fix. The unpatched default state of these flags is resolved at runtime through WIL fallback config and cannot be established from the binary, so no reachable "window" can be demonstrated for any of them. Two further in-place instruction edits sit inside statically-linked SymCrypt library code — SymCryptFdefDecideModulusType (sub_18000E700) and SymCryptEckeySetValue (sub_18001FD38); they belong to the crypto-math / FIPS-self-test layer, not the Authenticode/PE signature-verification path, and carry no code-integrity impact. The remaining changes are telemetry and address-shift noise.


2. Vulnerability Summary

Finding A — SymCrypt Modulus-Type Selection: CPU-Feature Mask Added

  • Severity: Informational (no code-integrity impact)
  • Class: Behavioral change in bundled crypto library.
  • Affected function: ci!SymCryptFdefDecideModulusType (sub_18000E700) (selects a modular-arithmetic implementation type).

Root cause / what changed. SymCryptFdefDecideModulusType iterates the SymCryptModulusTypeSelections table (0x10-byte entries: +0 modulus-type descriptor/result, +4 CPU-feature requirement flags, +8 a size bound, +0xc mask). For each entry the unpatched loop only tests the entry's flags against the CPU-feature bitmap g_SymCryptCpuFeaturesNotPresent (test cs:g_SymCryptCpuFeaturesNotPresent, eax): an entry is rejected if it requires a CPU feature that is not present. The patch inserts, ahead of that test, an unconditional test eax, 0xC000 / jnz that skips any entry whose flags include bits 14-15. This masks off table entries tied to specific feature bits so they are never selected regardless of the CPU-feature bitmap.

Why it is not a code-integrity issue. This function is part of the SymCrypt library that is statically linked into ci.dll. It chooses which internal modular-arithmetic routine to use (used by RSA/DH/ECC math), driven by CPU capabilities. It is not on the Authenticode/PE hash-algorithm-acceptance path, does not read any signature field, and does not gate which hash algorithms are trusted. The 0xC000 bits are CPU-feature requirement flags, not "weak/deprecated/test hash-algorithm" markers. The change is a SymCrypt library update carried along in the same ci.dll rebuild.

Call chain (crypto-internal): 1. SymCryptFdefIntToModulus (sub_18000B7CC) — sets up a modulus object. 2. SymCryptFdefDecideModulusType (sub_18000E700) — selects the modulus type from SymCryptModulusTypeSelections.

Finding B — PPL Revalidation Feature Flag Retired (Graduated to Unconditional)

  • Severity: Informational (no demonstrable code-integrity impact)
  • Class: Servicing feature-flag retirement / WIL feature-staging graduation.
  • Affected function: ci!CiRevalidateImage (sub_180065AB0) (decides whether an image must be re-validated).

What changed. CiRevalidateImage returns 1 to mean "this image must be (re)validated." In the unpatched code the block that inspects the image-address argument a5 and the following PPL decision are both wrapped in the Feature_Servicing_CodeIntegrityWinTcbPplTocTou (sub_180019584) feature flag:

  • The a5 range block runs only when Feature_...WinTcbPplTocTou() != 0 && a5 != 0 && a5 < *MmSystemRangeStart.
  • The PPL branch is PsIsProtectedProcessLight(...) && (a4 != 7 || (Feature_...WinTcbPplTocTou() && v9)).

The patch removes both Feature_...WinTcbPplTocTou() calls, so the a5 block runs whenever a5 != 0 && a5 < *MmSystemRangeStart and the PPL branch becomes PsIsProtectedProcessLight(...) && (a4 != 7 || v10). The PPL re-validation branch now runs regardless of the feature state.

Why it is not a delivered security fix. This is one of three CodeIntegrity Feature_Servicing_* flags retired module-wide in this patch (see the verdict). Feature_Servicing_CodeIntegrityWinTcbPplTocTou is a WIL velocity/servicing feature flag; the sub_180019584 helper resolves its state through wil_details_IsEnabledFallback reading external feature configuration, so the flag's default in the unpatched build cannot be established from the binary. Servicing feature flags are retired in code after they have reached full rollout, at which point the guarded behavior was already active in the field via feature config. Retiring the gate makes the branch unconditional in code, but there is no way to show from the binaries that any real system ran with this branch suppressed, so no reachable time-of-check/time-of-use window is demonstrable. The direction here (branch graduated on) is a hardening; the sibling retirement in CipApplySIPolicyUMCI graduates a permissive path on instead, confirming that this is feature-staging lifecycle, not a targeted TOCTOU fix.

Entry point / call chain: 1. Image validation / re-validation of a PPL image. 2. ci!CiRevalidateImage (sub_180065AB0) — the function whose feature gate the patch removes (registered via the CI callout table at +0x98).

Finding C — SymCrypt EC Key Import: FIPS Self-Test State Bit Added

  • Severity: Informational (no code-integrity impact)
  • Class: Behavioral change in bundled crypto library (FIPS self-test bookkeeping).
  • Affected function: ci!SymCryptEckeySetValue (sub_18001FD38) (imports an elliptic-curve key value).

Root cause / what changed. When bit 0xc of the key-import flags word (r15d) is set, both builds run the on-demand ECDSA FIPS self-test: if g_SymCryptFipsSelftestsPerformed & 4 is clear they call SymCryptEcDsaSelftest (sub_180021d1c) and set g_SymCryptFipsSelftestsPerformed |= 4. The patch adds a single or dword ptr [r12], 1 in that branch, where r12 points to the key object's flags word (initialized earlier with mov [r12], r15d). Effect: bit 0 of the key's flag word is set once the ECDSA self-test path has run.

Why it is not a code-integrity issue. This is SymCrypt FIPS 140 self-test state tracking on an imported EC key. g_SymCryptFipsSelftestsPerformed is the SymCrypt self-test completion bitmap, SymCryptEcDsaSelftest is the ECDSA known-answer self-test, and the added bit records self-test state on the key. None of this is a PE/Authenticode "verification-passed" bit or on the image-signature path.

Call chain (crypto-internal): 1. EC key import for an ECDSA/ECDH operation. 2. ci!SymCryptEckeySetValue (sub_18001FD38) — sets the key value and, when flagged, drives the FIPS self-test.


3. Pseudocode Diff

Finding A — SymCryptFdefDecideModulusType (sub_18000E700) (modulus-type selection)

// ==================== UNPATCHED ====================
// SymCryptModulusTypeSelections entry (0x10 bytes):
//   +0   modulus-type descriptor / result
//   +4   CPU-feature requirement flags
//   +8   size bound
//   +0xc mask

for (entry = &SymCryptModulusTypeSelections; ; entry += 0x10) {
    eax = entry->flags;                       // CPU-feature requirement flags
    if (g_SymCryptCpuFeaturesNotPresent & eax)  // required feature missing
        continue;
    r8 = entry->size_bound;
    if (!r8 || (arg2 <= block_count && rdi <= r8)) {
        if (!(entry->mask & ~rbx_1))
            return entry->descriptor;          // selected modulus type
    }
}

// ==================== PATCHED ====================
for (entry = &SymCryptModulusTypeSelections; ; entry += 0x10) {
    eax = entry->flags;
    if (eax & 0xC000)                          // *** NEW: skip entries needing bits 14-15 ***
        continue;
    if (g_SymCryptCpuFeaturesNotPresent & eax)
        continue;
    r8 = entry->size_bound;
    if (!r8 || (arg2 <= block_count && rdi <= r8)) {
        if (!(entry->mask & ~rbx_1))
            return entry->descriptor;
    }
}

Finding B — CiRevalidateImage (sub_180065AB0) (revalidation gate)

// ==================== UNPATCHED ====================
if (Feature_..._WinTcbPplTocTou_IsEnabled() != 0 && a5 != 0 && a5 < *MmSystemRangeStart) {
    if (a5 & 0x800000000000000)
        { if (!PsIsCurrentThreadInServerSilo()) return 1; }
    else
        v9 = (a5 & 0x400000000000000) == 0;
}
if ((a1 & 0x10) == 0) {
    if (PsIsProtectedProcessLight(PsGetCurrentProcess())
        && (a4 != 7 || (Feature_..._WinTcbPplTocTou_IsEnabled() != 0 && v9)))
        return 1;                              // must revalidate
}

// ==================== PATCHED ====================
if (a5 != 0 && a5 < *MmSystemRangeStart) {     // feature gate removed → block always runs
    if (a5 & 0x800000000000000)
        { if (!PsIsCurrentThreadInServerSilo()) return 1; }
    else
        v10 = (a5 & 0x400000000000000) == 0;
}
if ((a1 & 0x10) == 0) {
    if (PsIsProtectedProcessLight(PsGetCurrentProcess())
        && (a4 != 7 || v10))                   // gate removed → branch graduated to unconditional
        return 1;
}

Finding C — SymCryptEckeySetValue (sub_18001FD38) (FIPS self-test bit)

// ==================== UNPATCHED ====================
*r12 = r15d;                                   // key flags
if (r15d & (1 << 0xc)) {                        // bit 0xc set
    if ((g_SymCryptFipsSelftestsPerformed & 4) == 0) {
        SymCryptEcDsaSelftest();
        g_SymCryptFipsSelftestsPerformed |= 4;
    }
    // *** no *r12 |= 1 ***
}

// ==================== PATCHED ====================
*r12 = r15d;
if (r15d & (1 << 0xc)) {
    if ((g_SymCryptFipsSelftestsPerformed & 4) == 0) {
        SymCryptEcDsaSelftest();
        g_SymCryptFipsSelftestsPerformed |= 4;
    }
    *r12 |= 1;                                  // *** NEW: record self-test state on key ***
}

4. Assembly Analysis

Finding A — SymCryptFdefDecideModulusType (sub_18000E700)

; ===== UNPATCHED loop at 0x18000e7f5 =====
18000e7f5:  lea  rcx, SymCryptModulusTypeSelections
18000e7fc:  not  ebx
18000e7fe:  mov  r9d, 0x1ff
; --- LOOP START ---
18000e804:  mov  eax, [rcx+0x4]              ; entry->flags (CPU-feature requirement)
18000e807:  test cs:g_SymCryptCpuFeaturesNotPresent, eax
18000e80d:  jnz  0x18000e848                 ; skip if required feature not present
18000e80f:  mov  r8d, [rcx+0x8]             ; entry->size_bound
            ...                              ; size/mask acceptance checks
18000e848:  add  rcx, 0x10                   ; ++entry
18000e84c:  jmp  0x18000e804
; ===== PATCHED loop at 0x18000e855 =====
18000e855:  lea  rdx, SymCryptModulusTypeSelections
18000e85e:  mov  r9d, 0x1ff
; --- LOOP START ---
18000e864:  mov  eax, [rdx+0x4]             ; entry->flags
18000e867:  test eax, 0xC000                ; *** NEW: bits 14-15 mask ***
18000e86c:  jnz  0x18000e8af                ; *** NEW: unconditional skip ***
18000e86e:  test cs:g_SymCryptCpuFeaturesNotPresent, eax
18000e874:  jnz  0x18000e8af
18000e876:  mov  r8d, [rdx+0x8]
            ...

The added test eax, 0xC000 ; jnz pair is the entire change in this function. It filters SymCryptModulusTypeSelections entries by CPU-feature bits 14-15; it is a SymCrypt library edit, not a signature-verification fix.

Finding B — CiRevalidateImage (sub_180065AB0) unpatched vs @ 0x180068570 patched

The two Feature_Servicing_CodeIntegrityWinTcbPplTocTou__private_IsEnabledDeviceUsageNoInline calls present in the unpatched function are absent in the patched function:

; ===== UNPATCHED (sub_180065AB0) =====
0000000180065B87  call    Feature_Servicing_CodeIntegrityWinTcbPplTocTou__private_IsEnabledDeviceUsageNoInline
0000000180065B99  mov     rax, cs:MmSystemRangeStart          ; a5-range block, gated by the call above
...
0000000180065C05  call    cs:__imp_PsGetCurrentProcess
0000000180065C14  call    cs:__imp_PsIsProtectedProcessLight
0000000180065C29  call    Feature_Servicing_CodeIntegrityWinTcbPplTocTou__private_IsEnabledDeviceUsageNoInline

; ===== PATCHED (0x180068570) — no such call in the function =====
000000018006864F  mov     rax, cs:MmSystemRangeStart          ; a5-range block runs unconditionally
...
00000001800686B6  call    cs:__imp_PsGetCurrentProcess
00000001800686C5  call    cs:__imp_PsIsProtectedProcessLight  ; PPL branch no longer feature-gated

The patched function contains zero calls to the WinTcbPplTocTou feature helper (the helper itself is removed from the patched build along with all its other call sites). This is the feature-flag retirement described above, not a change to the surrounding validation logic.

Finding C — SymCryptEckeySetValue (sub_18001FD38)

Single added instruction in the bit-0xc branch, after the ECDSA self-test block:

; unpatched: ... lock or cs:g_SymCryptFipsSelftestsPerformed, 4
;            bt  r15d, 0Dh   (no *r12 write between the two selftest blocks)
; patched:
1800204d2:  lock or cs:g_SymCryptFipsSelftestsPerformed, 4
1800204da:  or   dword ptr [r12], 1          ; *** NEW ***
1800204df:  bt   r15d, 0Dh

5. Trigger Conditions

Finding A — SymCrypt modulus-type selection

Not an attacker-triggerable security condition. The change alters which internal modular-arithmetic implementation SymCrypt picks based on CPU-feature flag bits 14-15. To observe the behavioral difference: 1. Break on SymCryptFdefDecideModulusType. 2. At the loop, dump SymCryptModulusTypeSelections entries and inspect +4 (flags). Entries with bits 14-15 set are skipped by the patched build's new test eax, 0xC000 before the g_SymCryptCpuFeaturesNotPresent test. 3. The selected descriptor returned in eax may differ between builds on hardware where such an entry would otherwise match; there is no code-integrity effect.

Finding B — PPL revalidation feature-flag retirement

Not an attacker-triggerable security condition. The observable difference is which code path the feature flag selects: 1. Reach CiRevalidateImage for a Protected Process Light image with a4 == 7 and a5 non-null, below *MmSystemRangeStart. 2. The unpatched build calls Feature_Servicing_CodeIntegrityWinTcbPplTocTou (at 0x180065B87 and 0x180065C29); when that helper returns 0 the a5 block is skipped and the PPL condition collapses to a4 != 7. The patched build has no such call and always runs the a5 block, so the PPL branch is a4 != 7 || v10. 3. Whether the unpatched build ever ran with the helper returning 0 depends on the WIL feature configuration, which is external to the binary; it cannot be established here, so no reachable difference on a real system is demonstrable.

Finding C — SymCrypt EC key FIPS self-test bit

  1. Import an EC key via SymCryptEckeySetValue with bit 0xc of the flags word set (drives the on-demand ECDSA FIPS self-test).
  2. After the call, inspect the key flags word ([r12]). Bit 0 is clear on the unpatched build and set on the patched build.
  3. Confirmation: purely internal FIPS self-test bookkeeping; no image-verification effect.

6. Exploit Primitive & Development Notes

Finding A

  • Primitive: None. This is a SymCrypt library change to CPU-feature-driven modulus-type selection; it does not select or accept signature hash algorithms and provides no code-integrity bypass.

Finding B

  • Primitive: None demonstrable. The change retires the WinTcbPplTocTou servicing feature flag and graduates the guarded PPL re-validation branch to unconditional. Because the flag's unpatched default is resolved from external WIL configuration and not from the binary, no reachable time-of-check/time-of-use window can be shown, and no code-integrity bypass is provable from these builds.

Finding C

  • Primitive: None. FIPS self-test state tracking on an EC key object; no verification-decision impact.

7. Debugger PoC Playbook

Assume WinDbg attached to the unpatched target, symbols loaded for ci.dll.

Finding A — ci!SymCryptFdefDecideModulusType (sub_18000E700)

Breakpoints:

bp ci!SymCryptFdefDecideModulusType           ; entry
bp ci!SymCryptFdefDecideModulusType+0xF5       ; 0x18000e7f5 — table loop start
bp ci!SymCryptFdefIntToModulus                 ; caller (crypto-internal)

What to inspect: - At the loop body 0x18000e804, rcx points at the current SymCryptModulusTypeSelections entry. - eax = [rcx+4] — CPU-feature requirement flags. Note any entry with bits 14-15 set. - [rcx+0] — modulus-type descriptor. - [rcx+8] — size bound; [rcx+0xc] — mask. - g_SymCryptCpuFeaturesNotPresent is the CPU-features-not-present bitmap tested at 0x18000e807.

Dumping the table:

dc ci!SymCryptModulusTypeSelections L400

Look for entries with bits 14-15 set at offset +4; the patched build skips those via the new test eax, 0xC000.

Finding B — ci!CiRevalidateImage (sub_180065AB0) PPL revalidation

Breakpoints:

bp ci!CiRevalidateImage
bp ci!PsIsProtectedProcessLight
bp ci!Feature_Servicing_CodeIntegrityWinTcbPplTocTou__private_IsEnabledDeviceUsageNoInline

What to inspect: - CiRevalidateImage entry: ecx=arg1 (flags, bit 0x10 checked), dl=arg2, r8b=arg3, r9d=arg4 (compared to 7); a5 (5th arg) compared to *MmSystemRangeStart. - At the WinTcbPplTocTou feature call: al == 0 means the gate is disabled — the unpatched build then skips the a5 block and the PPL condition collapses to a4 != 7. - At PsIsProtectedProcessLight: rcx = PsGetCurrentProcess(); al == 1 → PPL image.

Expected observation: on the unpatched build the two WinTcbPplTocTou helper calls select the a5/PPL path; on the patched build those calls are gone and the path runs unconditionally. This shows the feature-flag retirement; it does not establish a reachable security difference, since the unpatched flag default is external WIL configuration.

Finding C — ci!SymCryptEckeySetValue (sub_18001FD38) FIPS self-test bit

Breakpoints:

bp ci!SymCryptEckeySetValue
bp ci!SymCryptEcDsaSelftest                     ; on-demand ECDSA self-test

What to inspect: - r15d = key-import flags. Bit 0xc is the trigger for the ECDSA self-test path. - r12 = key flags pointer; dword [r12] bit 0 is set on the patched build after the self-test block, clear on the unpatched build. - g_SymCryptFipsSelftestsPerformed — SymCrypt self-test completion bitmap; bit 2 (& 4) records ECDSA self-test done.

Expected observation: after return, dd r12 L1 shows bit 0 clear (unpatched) vs set (patched).


8. Changed Functions — Full Triage

Security-relevant

Function Sim Change Note
(none) No changed function adds or removes a signature/policy-validation check. All CI-side deltas are servicing feature-flag retirements (below).

CodeIntegrity servicing feature-flag retirements (not a delivered fix)

Function Sim Change Note
CiRevalidateImage (sub_180065AB0) 0.95 feature-retire Removed the Feature_Servicing_CodeIntegrityWinTcbPplTocTou (sub_180019584) gate so the PPL re-validation branch runs unconditionally (graduated on). WIL feature-staging lifecycle; unpatched flag default not determinable from the binary.

Bundled SymCrypt library changes (not code-integrity path)

Function Sim Change Note
SymCryptFdefDecideModulusType (sub_18000E700) 0.97 crypto-lib Added test eax, 0xC000 / jnz before the g_SymCryptCpuFeaturesNotPresent test in the SymCryptModulusTypeSelections loop; skips entries requiring CPU-feature bits 14-15. No CI impact.
SymCryptEckeySetValue (sub_18001FD38) 0.99 crypto-lib Added or dword ptr [r12], 1 in the bit-0xc ECDSA FIPS self-test branch; records self-test state on the key. No CI impact.

Behavioural (feature-flag retirements)

Function Sim Note
CipGetEmbeddedSignatureAndFindFirstMatch (sub_1800DB414) 0.90 Removed Feature_Servicing_CodeIntegrityMissing3077Events (sub_1800199b0) gate; servicing flag retirement.
CipApplySIPolicyUMCI (sub_1800DA3E4) 0.96 Removed Feature_Servicing_CodeIntegrityNonConformantNotBlockedByResources (sub_180019a04) from an early-return guard, so the feature's permissive bypass (skip the extra policy-apply loop when (a1+2360)&4 == 0) is now always-on. Feature-staging graduation in the loosening direction.
CipValidateImageHash (sub_18008B4F0) 0.97 Removed Feature_Servicing_CodeIntegrityMissing3077Events (sub_1800199b0) from the validation loop; address shifts otherwise.
CiGetActionsForImage (sub_1800D5CD0) 0.95 Removed Feature_Servicing_CodeIntegrityWinTcbPplTocTou (sub_180019584) gate, simplified flag computation.
wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState (sub_180019080) 0.99 Added | 0x40000 to an initialization flag word (feature-state cache).
CipGetFileCache (sub_180096CE0) 0.99 Removed Feature_Servicing_CodeIntegrityWinTcbPplTocTou (sub_180019584) gate near the server-silo check.

Cosmetic / spurious matches

SymCryptParallelHashProcess (sub_180006910) (address-shift only), CiInitializePolicyFromPolicies (sub_18001A91C) (address-shift only), wil_details_RegisterFeatureUsageProvider (sub_180054718) (added var_c=0, var_10=1 telemetry fields), CipProcessSIPolicyLogs (sub_1800DDCB4) (sim 0.24, spurious call-graph match), CipShouldRevalidateForPolicyChange (sub_180067DC4) (sim 0.56, spurious). These should not be used as anchors for further diff work.

The remaining 22 changed functions in the 38-function total are address-only or register-allocation-only drift: noise induced by the rebased layout and table relocations, with no behavioural delta.


9. Unmatched Functions

Removed Added
(none) (none)

Both binaries are function-set identical. No sanitizer or mitigation function was added; no check was removed wholesale. All deltas are in-place edits, consistent with a targeted servicing patch plus a bundled SymCrypt refresh rather than a refactor.


10. Confidence & Caveats

  • Overall confidence: High for the direction and mechanics of all three changes (each is confirmed by the assembly diff). None is a delivered code-integrity vulnerability fix: Finding B is a WIL servicing feature-flag retirement whose reachable impact cannot be established from the binaries; Findings A and C are SymCrypt library edits with no code-integrity effect.
  • Established facts:
  • sub_18000E700 = SymCryptFdefDecideModulusType; its table is SymCryptModulusTypeSelections and the word tested is g_SymCryptCpuFeaturesNotPresent (CPU-feature bitmap), not a signature policy. The 0xC000 bits are CPU-feature flags.
  • sub_18001FD38 = SymCryptEckeySetValue; the bit-0xc branch drives SymCryptEcDsaSelftest and records completion in g_SymCryptFipsSelftestsPerformed; the new or [r12],1 is FIPS self-test state on the key.
  • sub_180065AB0 = CiRevalidateImage; Feature_Servicing_CodeIntegrityWinTcbPplTocTou (sub_180019584) gates the PPL revalidation decision in the unpatched build and is removed in the patched build. Return value 1 from CiRevalidateImage means "must revalidate." The WinTcbPplTocTou, Missing3077Events, and NonConformantNotBlockedByResources servicing flags are all retired (references drop to zero in the patched build).
  • Caveat / what cannot be shown here:
  • The unpatched default state of the retired feature flags is resolved through WIL fallback configuration external to the binary, so no reachable difference on a real system can be demonstrated from these two builds. Severity is therefore Informational, not a rated vulnerability.