ci.dll — Code Integrity feature-staging graduation
KB5077181
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | ci_unpatched.dll |
| Patched binary | ci_patched.dll |
| Overall similarity | 0.9466 |
| Matched functions | 2498 |
| Changed functions | 99 |
| Identical functions | 2399 |
| Unmatched (unpatched / patched) | 0 / 0 |
Verdict: The patched binary is a newer servicing build of Windows Code Integrity. The bulk of the diff is WIL feature-staging churn plus a SymCrypt library rebuild:
- The
Feature_Servicing_CodeIntegrityWinTcbPplTocToustaging gate is fully removed (8 call sites in the unpatched build, 0 in the patched build). Behavior that was runtime-gated by that flag becomes unconditional. The unpatched build still contains the graduated code path; it is selected at run time by the feature flag. This is the graduation of a staged feature, not the delivery of a newly-added check. - Two new staging feature sets are introduced in the patched build:
Feature_CodeIntegrity_TrustedLaunchPolicy(0 → 51 call sites) andFeature_CodeIntegrity_EccSupport(0 → 5 call sites, ECC P-521 support). These add new gated code paths while retaining the old behavior when the flag is off. - Remaining changes in the statically-linked SymCrypt code (RSA field computation) are a library rebuild.
No attacker-controlled, demonstrably-reachable security primitive is delivered by this diff. The change to the boundary/masking logic operates on an internal Code Integrity policy generation counter, not on an attacker-supplied pointer, and the mask only affects values whose bits 58–63 are set — values that do not occur for a legitimate generation counter. The findings below are retained and characterized at their actual impact.
2. Change Summary
Finding 1 — Conditional pointer masking made unconditional in CipShouldRevalidateForPolicyChange
| Attribute | Value |
|---|---|
| Severity | Informational (defense-in-depth / feature-staging) |
| Class | Feature-staging graduation of an internal consistency mask |
| Function | CipShouldRevalidateForPolicyChange @ 0x1800670B4 (unpatched) / @ 0x18006A874 (patched) |
| Primitive | None demonstrable |
What the function does: As its name states, it decides whether a cached image validation must be re-run because the Code Integrity policy changed. It compares a value against the global policy generation counter g_CiPolicyGenerationCounter (the report’s data_180043330). The dword g_CiPolicyState (data_180043e00) gates whether the check runs at all (its sign bit must be set). This is a policy-staleness comparison, not a memory bounds check.
Difference: In the unpatched build, on the path taken when the input value is below MmSystemRangeStart, the mask and rbx, 0x3FFFFFFFFFFFFFF (clear bits 58–63) is applied only when Feature_Servicing_CodeIntegrityWinTcbPplTocTou__private_IsEnabledDeviceUsageNoInline @ 0x180019514 returns non-zero. In the patched build the feature call is gone and the mask is applied unconditionally. Both builds otherwise compute the same comparison against g_CiPolicyGenerationCounter.
Why this is not an exploitable bypass: The value that is masked and compared is an internal policy-generation value, not an attacker-supplied image pointer. The mask clears bits 58–63; a legitimate generation counter never has those bits set, so masked and unmasked comparisons agree for all real inputs. The unpatched build already contains the masked path (selected by the feature flag), so this is a staging graduation, not a delivered fix. The earlier “boundary check bypass allowing unsigned code execution” framing is not supported by the binaries and is removed.
Finding 2 — Feature-gated pointer checks made unconditional in CiRevalidateImage
| Attribute | Value |
|---|---|
| Severity | Informational (feature-staging) |
| Class | Feature-staging graduation |
| Function | CiRevalidateImage @ 0x180064DF0 (unpatched) / @ 0x180068570 (patched) |
| Primitive | None demonstrable |
Difference: CiRevalidateImage calls the same Feature_Servicing_CodeIntegrityWinTcbPplTocTou flag twice in the unpatched build (at 0x180064EC3 and 0x180064F65). At 0x180064EC3 the flag gates a block of pointer-validity checks on the argument at [rbp+arg_20]: non-null, below MmSystemRangeStart, and a server-silo test on bit 0x3B (PsIsCurrentThreadInServerSilo). When the flag returns 0 the block is skipped (jump to the Protected-Process-Light region); when it returns non-zero the checks run. In the patched build (0x18006863B onward) there is no feature call — the pointer-validity checks always run, i.e. the patched build behaves as the unpatched build did with the feature enabled.
Behavior: There is no “PPL returns ALLOW without verification” bypass. Both builds reach the same Protected-Process-Light handling (PsGetCurrentProcess → PsIsProtectedProcessLight, cmp ebx, 7) and then fall through to the Feature_TrustedLaunch path. The only real change is removal of the staging gate around the pointer-validity checks; the direction is that the patched build unconditionally runs those checks. The unpatched build already contains that path behind the flag. No supporting assembly for a PPL ALLOW-bypass exists, and that claim is removed.
Finding 3 — Feature-gated policy-flag computation made unconditional in CiGetActionsForImage
| Attribute | Value |
|---|---|
| Severity | Informational (feature-staging) |
| Class | Feature-staging graduation |
| Function | CiGetActionsForImage @ 0x1800D5610 (unpatched) / @ 0x1800D9230 (patched) |
Difference: In the unpatched build the function opens with call Feature_Servicing_CodeIntegrityWinTcbPplTocTou…; test eax, eax; jz loc_1800D56E0 (at 0x1800D563D), so the whole arg3 & 0x40 policy-flag block is skipped when the flag is off. A second gated branch sits at 0x1800D576C. The patched build removes both feature calls and runs the flag computation directly. Same graduation pattern as Findings 1 and 2, same flag. This affects which policy-action bits are set, but the unpatched build already contains the graduated path behind the flag.
Finding 4 — New Feature_CodeIntegrity_TrustedLaunchPolicy staging added to CiHvciIsVbsPolicyFailure
| Attribute | Value |
|---|---|
| Severity | Informational (feature-staging) |
| Class | Newly-introduced staged gate (old behavior retained when flag off) |
| Function | CiHvciIsVbsPolicyFailure @ 0x18006C478 (unpatched) / @ 0x1800707F8 (patched) |
What the function does: It walks the policy entries reachable from the argument structure ([rcx+958h], [rcx+930h], [rcx+928h]) and reports a VBS policy failure when an entry’s policy ID matches SiPolicyIDVbs and the associated global qword (qword_180030498 unpatched / qword_180031660 patched). It does not scan for the constants 0x4B5E588BA072029F / 0x87F67DD6AA05F9B7; those literals belong to other functions in the module and were misattributed here. That description is removed.
Difference: The patched build adds, before the match loop (at 0x18007081A), call Feature_CodeIntegrity_TrustedLaunchPolicy…; test eax, eax; jz loc_180070835. When that new flag is enabled it additionally requires bit 0x14 set and bit 0x15 clear in [rbx+938h] (bt eax, 14h / bt eax, 15h), otherwise it returns 0 (no failure reported). When the flag is off it falls straight through to the same loop as the unpatched build. This is a newly-introduced staged gate with the old path retained — a staged rollout, not a delivered security check.
Finding 5 — SymCrypt RSA field computation: added key-pair self-test (SymCryptRsakeyCalculatePrivateFields)
| Attribute | Value |
|---|---|
| Severity | Informational (SymCrypt library rebuild) |
| Class | Robustness self-test in statically-linked crypto |
| Function | SymCryptRsakeyCalculatePrivateFields @ 0x180002D0C |
Root cause: SymCryptRsakeyCalculatePrivateFields computes RSA private-key CRT fields from the prime factors. The patched build adds a flags parameter (arg7); when bit 0x200 is set it multiplies the two primes with SymCryptFdefIntMulMixedSize @ 0x1800256DC and compares the product against the stored modulus with SymCryptFdefIntIsEqual @ 0x180025418, i.e. an RSA key-pair consistency self-test (p·q == n). This is a robustness/self-test hardening inside the statically-linked SymCrypt library, not a certificate-verification or policy change. The trigger (arg7 & 0x200) is a caller-supplied compile-time flag, not attacker-controlled. Consistent with a SymCrypt library rebuild.
3. Pseudocode Diff
Finding 1: CipShouldRevalidateForPolicyChange — masking
// ===================== UNPATCHED @ 0x1800670B4 =====================
int64_t CipShouldRevalidateForPolicyChange(void* arg1)
{
int64_t rbx = (int64_t)arg1;
if (g_CiPolicyState >= 0) // sign bit not set -> no check
return 0;
if (g_CiPolicyGenerationCounter == 0x3FFFFFFFFFFFFFF)
return 1;
if (rbx < *MmSystemRangeStart) // value below kernel range
{
if (Feature_Servicing_CodeIntegrityWinTcbPplTocTou()) // *** STAGING GATE ***
rbx &= 0x3FFFFFFFFFFFFFF; // mask applied only when feature on
return rbx < g_CiPolicyGenerationCounter;
}
if (*(uint32_t*)arg1 & 0x8) // kernel-pointer path
return 1;
return *(uint64_t*)(arg1 + 8) < g_CiPolicyGenerationCounter;
}
// ===================== PATCHED @ 0x18006A874 =====================
int64_t CipShouldRevalidateForPolicyChange(void* arg1)
{
if (g_CiPolicyState >= 0)
return 0;
int64_t counter = g_CiPolicyGenerationCounter;
if (counter != 0x3FFFFFFFFFFFFFF)
{
if ((int64_t)arg1 < *MmSystemRangeStart)
return ((int64_t)arg1 & 0x3FFFFFFFFFFFFFF) < counter; // mask always applied
if (!(*(uint32_t*)arg1 & 0x8))
return *(uint64_t*)(arg1 + 8) < counter;
}
return 1;
}
Finding 2: CiRevalidateImage — pointer-validity checks
// ===================== UNPATCHED @ 0x180064DF0 (first gate, 0x180064EC3) =====================
if (Feature_Servicing_CodeIntegrityWinTcbPplTocTou() // *** STAGING GATE ***
&& arg5 != 0 && arg5 < *MmSystemRangeStart)
{
// pointer-validity / server-silo (bit 0x3B) checks run here
}
// when the flag is off, these checks are skipped
// ===================== PATCHED @ 0x180068570 (0x18006863B) =====================
if (arg5 != 0 && arg5 < *MmSystemRangeStart) // *** GATE REMOVED ***
{
// same pointer-validity / server-silo checks always run
}
4. Assembly Analysis
Finding 1: CipShouldRevalidateForPolicyChange
; ===== UNPATCHED @ 0x1800670B4 =====
00000001800670B4 mov [rsp+arg_0], rbx
00000001800670B9 push rdi
00000001800670BA sub rsp, 20h
00000001800670BE mov eax, cs:g_CiPolicyState
00000001800670C4 mov rbx, rcx
00000001800670C7 test al, al
00000001800670C9 js short loc_1800670CF
00000001800670CB xor al, al ; g_CiPolicyState >= 0 -> return 0
00000001800670CD jmp short loc_180067114
00000001800670CF mov rdi, 3FFFFFFFFFFFFFFh ; mask (clears bits 58-63)
00000001800670D9 cmp cs:g_CiPolicyGenerationCounter, rdi
00000001800670E0 jz short loc_180067112
00000001800670E2 mov rax, cs:MmSystemRangeStart
00000001800670E9 cmp rbx, [rax]
00000001800670EC jb short loc_1800670FA ; below kernel range -> value path
00000001800670EE mov eax, [rcx] ; kernel-pointer path
00000001800670F0 test al, 8
00000001800670F2 jnz short loc_180067112
00000001800670F4 mov rbx, [rcx+8]
00000001800670F8 jmp short loc_180067106
00000001800670FA call Feature_Servicing_CodeIntegrityWinTcbPplTocTou__private_IsEnabledDeviceUsageNoInline
00000001800670FF test eax, eax
0000000180067101 jz short loc_180067106 ; feature off -> skip mask
0000000180067103 and rbx, rdi ; conditional mask
0000000180067106 cmp rbx, cs:g_CiPolicyGenerationCounter
000000018006710D setb al
0000000180067110 jmp short loc_180067114
0000000180067112 mov al, 1
0000000180067114 mov rbx, [rsp+28h+arg_0]
0000000180067119 add rsp, 20h
000000018006711D pop rdi
000000018006711E retn
; ===== PATCHED @ 0x18006A874 =====
000000018006A874 mov eax, cs:g_CiPolicyState
000000018006A87A test al, al
000000018006A87C js short loc_18006A882
000000018006A87E xor al, al ; return 0
000000018006A880 retn
000000018006A882 mov rdx, cs:g_CiPolicyGenerationCounter
000000018006A889 mov r8, 3FFFFFFFFFFFFFFh
000000018006A893 cmp rdx, r8
000000018006A896 jz short loc_18006A8BB
000000018006A898 mov rax, cs:MmSystemRangeStart
000000018006A89F cmp rcx, [rax]
000000018006A8A2 jb short loc_18006A8B0
000000018006A8A4 mov eax, [rcx]
000000018006A8A6 test al, 8
000000018006A8A8 jnz short loc_18006A8BB
000000018006A8AA mov rcx, [rcx+8]
000000018006A8AE jmp short loc_18006A8B3
000000018006A8B0 and rcx, r8 ; mask always applied (no feature call)
000000018006A8B3 cmp rcx, rdx
000000018006A8B6 setb al
000000018006A8B9 retn
000000018006A8BB mov al, 1
000000018006A8BD retn
What changed: The unpatched build’s user-value path calls the feature flag at 0x1800670FA and only executes and rbx, rdi (0x180067103) when it returns non-zero. The patched build removes the call, the test, and the conditional jump; the mask (and rcx, r8 at 0x18006A8B0) is unconditional. The comparison against g_CiPolicyGenerationCounter and every other branch is identical in both builds.
Finding 2: CiRevalidateImage (staging gate at the pointer-validity block)
; ===== UNPATCHED @ 0x180064EC3 =====
0000000180064EC3 call Feature_Servicing_CodeIntegrityWinTcbPplTocTou__private_IsEnabledDeviceUsageNoInline
0000000180064EC8 mov rdi, [rbp+3Fh+arg_20]
0000000180064ECC test eax, eax
0000000180064ECE jz short loc_180064F39 ; feature off -> skip pointer checks
0000000180064ED0 test rdi, rdi
0000000180064ED3 jz short loc_180064F39
0000000180064ED5 mov rax, cs:MmSystemRangeStart
0000000180064EDC cmp rdi, [rax]
0000000180064EDF jnb short loc_180064F39
0000000180064EE1 bt rdi, 3Bh
0000000180064EE6 jnb short loc_180064F2B
0000000180064EE8 call cs:__imp_PsIsCurrentThreadInServerSilo
; ===== PATCHED @ 0x18006863B =====
000000018006863B test r15b, 0Ah
000000018006863F jnz short loc_180068672
0000000180068641 cmp dl, 5
0000000180068644 jz short loc_180068672
0000000180068646 mov rsi, [rbp+3Fh+arg_20] ; no feature call
000000018006864A test rsi, rsi
000000018006864D jz short loc_1800686AE
000000018006864F mov rax, cs:MmSystemRangeStart
0000000180068656 cmp rsi, [rax]
0000000180068659 jnb short loc_1800686AE
000000018006865B bt rsi, 3Bh
0000000180068660 jnb short loc_1800686A5
0000000180068662 call cs:__imp_PsIsCurrentThreadInServerSilo
The patched build reaches the identical pointer-validity / server-silo checks without the feature call.
5. Reachability
CipShouldRevalidateForPolicyChange is called from CiRevalidateImage (at 0x180064EAE and 0x180064FD7 in the unpatched build; 0x180068631 and 0x180068799 in the patched build), which is part of the Code Integrity image-validation path. The comparison operand in the affected path is the internal policy generation value / the argument at [rbp+arg_20], populated by Code Integrity itself, not directly by an untrusted caller. There is no verified path by which an external attacker supplies a value with bits 58–63 set into the masked comparison, so no reachable primitive is demonstrated. The dispatch-table offset 0x98 and the specific load-time entry claimed previously could not be confirmed and are not asserted.
6. Exploit Primitive
None demonstrable. The masked/compared value is an internal Code Integrity policy generation value, not an attacker-controlled pointer; the mask affects only bit patterns (bits 58–63 set) that a legitimate generation counter never carries. The unpatched build already contains the graduated (masked, checks-run) code path, selected at run time by the Feature_Servicing_CodeIntegrityWinTcbPplTocTou flag. No unsigned-code-execution, privilege-escalation, or Protected-Process-Light bypass is supported by the two builds.
7. Observation Aids
The behavioral difference for Finding 1 can be observed at the following real instructions:
| Build | Address | Instruction | Note |
|---|---|---|---|
| Unpatched | 0x1800670FA |
call Feature_Servicing_CodeIntegrityWinTcbPplTocTou… |
Present only in the unpatched build |
| Unpatched | 0x180067101 |
jz 0x180067106 |
Skips the mask when the flag returns 0 |
| Unpatched | 0x180067103 |
and rbx, rdi |
Conditional mask |
| Patched | 0x18006A8B0 |
and rcx, r8 |
Unconditional mask (no feature call) |
| Both | cmp …, g_CiPolicyGenerationCounter |
— | Comparison identical in both builds |
Relevant globals: g_CiPolicyState (dword, sign bit gates the check) and g_CiPolicyGenerationCounter (qword, the compared limit). Observing the masked vs unmasked value at run time shows the two agree for all legitimate generation-counter values.
8. Changed Functions — Triage
Feature-staging / servicing changes
| Function | Similarity | Nature |
|---|---|---|
CipShouldRevalidateForPolicyChange @ 0x1800670B4 |
0.561 | Feature_Servicing_CodeIntegrityWinTcbPplTocTou gate removed; mask made unconditional. Value compared is the policy generation counter, not an attacker pointer. |
CiRevalidateImage @ 0x180064DF0 |
0.889 | Same flag removed from the pointer-validity block; checks now unconditional. Also adds new Feature_CodeIntegrity_TrustedLaunchPolicy path (CipIsTrustedLaunch, RtlQueryPackageClaims). |
CiGetActionsForImage @ 0x1800D5610 |
0.946 | Same flag removed from policy-flag computation (arg3 & 0x40, bit 0x1D). |
CiHvciIsVbsPolicyFailure @ 0x18006C478 |
0.891 | New Feature_CodeIntegrity_TrustedLaunchPolicy gate plus bit 0x14/0x15 checks added before the VBS-policy match loop (matches on SiPolicyIDVbs). |
SymCrypt / crypto library rebuild
| Function | Similarity | Nature |
|---|---|---|
SymCryptRsakeyCalculatePrivateFields @ 0x180002D0C |
0.701 | Added arg7 & 0x200 self-test: p·q == n via SymCryptFdefIntMulMixedSize/SymCryptFdefIntIsEqual. Library robustness, not policy. |
MinCryptVerifySignedHash2 @ 0x1800852AC |
0.904 | Added ECC P-521 (OID 0x2E03) dispatch; stack frame 0x188 → 0x538. |
HashpVerifyEcdsaSignature @ 0x180081624 |
0.845 | Added P-521 init gated by Feature_CodeIntegrity_EccSupport; provider flags 0x1000 vs 0x1100. |
Other behavioral / servicing changes
| Function | Similarity | Nature |
|---|---|---|
CipInitialize @ 0x180053BD0 |
0.908 | Dispatch table 0x108 → 0x110 (new +0x100 entry CiIsTrustedLaunchPolicyEnabled); added wil_InitializeFeatureStaging; Feature_CodeIntegrity_EccSupport gate before MinCrypK_DisableEcdsa. |
CiQueryInformation @ 0x1800D5040 |
0.933 | Added gating data_180045e70 & 0x400000 / data_180052010 & 0x2000 before the 0x100000 flag (debugger-present logic). |
SIPolicySearchFileRules @ 0x180095218 |
0.897 | Added arg13 (UNICODE_STRING**), arg14; bsearch compare callback relocated. |
I_FreeCatalogData @ 0x18009986C |
0.909 | Structure 0x118 → 0x128; conditional free of arg1[0x23] gated by Feature_TrustedLaunchSysAppsOSCatalog; pool tag 0x63734943. |
CipReportAndReprieveUMCIFailure @ 0x18005A44C |
0.310 | Large refactor (351 → 1325 lines): ETW tracking, push locks, perf counters. Feature addition, not a security fix. |
CipUpdateValidationContextWithFileInfo @ 0x18008E9D8 |
0.856 | Feature_CodeIntegrity_TrustedLaunchPolicy gate before *(arg1 + 0x3D8) = arg2; added 5th UNICODE_STRING; null-check hardening. |
Cosmetic / register allocation
The remaining changed functions show register reallocation, basic-block reordering, or instruction-scheduling differences with no semantic impact, consistent with a compiler/servicing rebuild.
9. Unmatched Functions
No functions were added or removed (unmatched_unpatched: 0, unmatched_patched: 0). All changes occur within existing functions, consistent with a servicing rebuild that graduates one staged feature and introduces two others.
10. Confidence & Caveats
Confidence: High (that this is feature-staging / servicing churn, not a delivered exploitable vulnerability)
Rationale:
- The Feature_Servicing_CodeIntegrityWinTcbPplTocTou flag drops from 8 call sites to 0 across CiValidateImageHeader, CiRevalidateImage (×2), CipShouldRevalidateForPolicyChange, CipGetFileCache, and CiGetActionsForImage (×2) — a whole-feature graduation.
- Two new staging sets appear only in the patched build: Feature_CodeIntegrity_TrustedLaunchPolicy (0 → 51) and Feature_CodeIntegrity_EccSupport (0 → 5). New gated paths with old behavior retained are staged rollouts, not delivered checks.
- The affected comparison in CipShouldRevalidateForPolicyChange is against an internal policy generation counter; the mask is a no-op for all legitimate values. No attacker-controlled input reaches it in a way that changes the result.
Caveats
- Whether a given device runs the graduated (masked, checks-on) path in the unpatched build depends on the runtime state of the
Feature_Servicing_CodeIntegrityWinTcbPplTocTouflag, which cannot be determined from the static image. - The exact default enablement of the newly-introduced
Feature_CodeIntegrity_TrustedLaunchPolicystaging is likewise a runtime property not visible in the binary.