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_CodeIntegrityWinTcbPplTocTou staging 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) and Feature_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 (PsGetCurrentProcessPsIsProtectedProcessLight, 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 0x1880x538.
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 0x1080x110 (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 0x1180x128; 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_CodeIntegrityWinTcbPplTocTou flag, which cannot be determined from the static image.
  • The exact default enablement of the newly-introduced Feature_CodeIntegrity_TrustedLaunchPolicy staging is likewise a runtime property not visible in the binary.