ci.dll — servicing feature-flag retirement and SymCrypt library rebuild
KB5089549
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
a5range block runs only whenFeature_...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
- Import an EC key via
SymCryptEckeySetValuewith bit0xcof the flags word set (drives the on-demand ECDSA FIPS self-test). - After the call, inspect the key flags word (
[r12]). Bit 0 is clear on the unpatched build and set on the patched build. - 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
WinTcbPplTocTouservicing 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 isSymCryptModulusTypeSelectionsand the word tested isg_SymCryptCpuFeaturesNotPresent(CPU-feature bitmap), not a signature policy. The0xC000bits are CPU-feature flags.sub_18001FD38=SymCryptEckeySetValue; the bit-0xcbranch drivesSymCryptEcDsaSelftestand records completion ing_SymCryptFipsSelftestsPerformed; the newor [r12],1is 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 value1fromCiRevalidateImagemeans "must revalidate." TheWinTcbPplTocTou,Missing3077Events, andNonConformantNotBlockedByResourcesservicing 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.