---
title: 'skci.dll Internals — Reverse Engineering the Secure Kernel Code Integrity Module on Windows 11 26H1'
url: 'https://kvc.pl/research/skci-code-integrity'
markdown: 'https://kvc.pl/research/skci-code-integrity.md'
date: '2026-08-29'
description: 'Deep static analysis of skci.dll (10.0.28000.2336) with public PDB on Windows 11 Pro 26H1. Documents the full trust chain from UEFI Secure Boot through bootmgfw.efi, winload.efi, and securekernel.exe into the Secure Kernel Code Integrity module. Covers seven non-obvious findings: non-zero success code from SkciValidateDynamicCodePages (0x12C), empty-but-authorized output from SkciQueryEnclavePackageID, two-instruction CipForceImageRevalidation, BugCheck invariant in SkciBuildAttestationReport, rigid two-class SkciQueryInformation protocol, EKU fallback path in SkciDetermineAuthorizedPageUse, and compact mode-2 update in SkciSetCodeIntegrityPolicy. Also covers ESP audit findings: two bootmgfw.efi binaries with identical version but different hash, and non-standard EFI assets. Includes authorized_page_use_flags bitmap, SKCI image context layout, attestation report format, and diagnostic gap analysis. No bypass, no exploit chain.'
---

# skci.dll Internals — Reverse Engineering the Secure Kernel Code Integrity Module on Windows 11 26H1

[ Download DisableVBS.7z](https://kvc.pl/research/skci-code-integrity/DisableVBS.7z)  🔐 ARCHIVE PASSWORD: `github.com` 

**Author:** Marek Wesołowski (WESMAR)
**Date:** 27 June 2026
**Target:** Windows 11 Pro 26H1, build 28000, fully patched
**Subject:** `skci.dll` version `10.0.28000.2336`, public PDB available

> **PL:** Analiza statyczna `skci.dll` — modułu Secure Kernel Code Integrity — z publicznym PDB na Windows 11 Pro 26H1. Dokumentuje łańcuch zaufania od firmware UEFI przez boot manager i OS loader do kontekstu securekernel, mapuje eksportowane API, identyfikuje siedem nieoczywistych zachowań, bitmapę uprawnień strony i format raportu atestacji. Na marginesie: inwentarz ESP i dwie kopie bootmgfw.efi z tą samą wersją ale różnym hashem. **W ramach praktycznego zademonstrowania wniosków z analizy, do artykułu dołączam autorskie, ultra-lekkie narzędzie DisableVBS (x64 oraz natywne ARM64), które pozwala w ułamku sekundy wyłączyć VBS/HVCI na czas jednego cyklu uruchomieniowego. Narzędzie działa przy włączonym SecureBoot i wszystkich zabezpieczeniach, pozostawiając po restarcie całkowicie czysty i nienaruszony stan plików na dysku. Kod źródłowy nie jest udostępniany, aby zapobiec jego modyfikacji i potencjalnym nadużyciom ofensywnym.**
> 
> **EN:** Static analysis of `skci.dll` — the Secure Kernel Code Integrity module — with public PDB on Windows 11 Pro 26H1. Documents the trust chain from UEFI firmware through boot manager and OS loader into the secure kernel context, maps the exported API, identifies seven non-obvious behaviours, the page-use permission bitmap, and the attestation report format. Side-finding: ESP inventory and two bootmgfw.efi copies with same version but different hash. **As a practical proof-of-concept of the analysis findings, this write-up includes DisableVBS, a standalone, CRT-free utility (supporting x64 and native ARM64) that disables VBS/HVCI for a single boot session in a fraction of a second. It operates seamlessly under active Secure Boot and default system integrity policies, ensuring a pristine, unmodified filesystem state immediately after the system boots. The source code is intentionally withheld to prevent modification and potential offensive abuse.**

---

## Abstract

This document records my static analysis of `skci.dll` on a live Windows 11 Pro 26H1 system, build 28000. The investigation began as a natural continuation of the ForceIO work: having established that `WdFilter.sys` filters file I/O in VTL0 and can be bypassed by routing through an overpowered signed driver, the next honest question was whether the kernel-level code-integrity machinery in VTL1 deserves the same quality of scrutiny. The answer, as always, is yes — and the methodology here differs from the VTL0 work because SKCI is more structurally interesting and carries public debug symbols.

I had a meaningful advantage that I want to acknowledge up front: **the public PDB for `skci.dll` was available on the Microsoft symbol server**. This is not common for VTL1 components. It means that most of the function names I cite here are their real names, not my analytical aliases. That changes the nature of the investigation from pure archaeology into something closer to reading annotated assembly — you still have to understand the semantics, but you skip the naming guesswork. I want to be honest about this because it affects how you should weigh the confidence in my findings.

The analysis produced seven findings that I consider non-obvious — things that surprised me or that I expect to surprise a reader who knows VTL0 Code Integrity well:

1. `SkciValidateDynamicCodePages` returns `0x12C` on success, not `STATUS_SUCCESS (0)`. Any caller checking `status == 0` gets a false negative.
2. `SkciQueryEnclavePackageID` passes the authorization check and then returns 40 bytes of zeros. The actual enclave identities are computed separately and live elsewhere in the context.
3. `SkciQueryInformation` accepts exactly two information classes (`0x67` and `0xF1`), with a rigid 8-byte handshake protocol for class `0x67`. Everything else is `STATUS_INVALID_INFO_CLASS`.
4. `SkciBuildAttestationReport` calls `KeBugCheckEx(SECURITY_SYSTEM)` when the attestation history count is zero. Zero history is an invariant violation, not an API error condition.
5. `CipForceImageRevalidation` is two instructions: `bts [g_CiPolicyState], 7; ret`. This pass confirms SKCI as the producer of the revalidation-pending signal; the exact consumer was not proven in static analysis.
6. `SkciDetermineAuthorizedPageUse` has an EKU fallback path for signing levels between `0x08` and `0x0C`: the presence of EKU `1.3.6.1.4.1.311.76.8.1` can grant the `WHQL_level` bit without a full level upgrade.
7. `SkciSetCodeIntegrityPolicy` mode 2 accepts exactly 5 bytes — a compact scenario state update under the CI policy lock.

The document also covers the ESP inventory, boot chain components, the image validation pipeline, the page-use flag bitmap, the attestation report format, and a diagnostic gap analysis. It is not a guide to bypassing HVCI. It is what I would want to read before writing defensive tooling or incident response procedures that interact with the CI attestation surface.

---

## 1. Motivation

The [ForceIO write-up](https://kvc.pl/research/forceio-minifilter-bypass/) ended at the `WdFilter.sys` minifilter layer: we can route around it by speaking directly to an overpowered but legitimately signed driver. That closes one chapter and opens another. The minifilter is a VTL0 construct. The deeper question — whether a driver or image is authorized to execute at all — belongs to VTL1, and specifically to `skci.dll`.

I had been thinking about this for a while. The problem with most published HVCI/VBS research is that it operates at the level of "what happens when you try" rather than "why the machinery decides what it does." There are good theoretical write-ups, but almost nothing that walks through actual disassembly of the current enforcement path with current symbols. Part of the reason is that VTL1 components are harder to reach: you cannot single-step through `securekernel.exe` with a user-mode debugger, and full kernel debugging of the VTL1 world requires specialized setup. Static analysis with symbols is the practical alternative for a researcher who does not want to instrument firmware.

So I started there. The questions I brought to the binary:

1. What is the exact sequence of decisions between an image being presented to SKCI and a page-use authorization being issued?
2. Are there observable gaps in the diagnostic surface — places where the code makes a security-relevant decision without surfacing sufficient information to incident response tooling?
3. What does the attestation report actually contain, and what are the correctness invariants the code depends on?
4. Is there anything in the EFI/boot chain that a forensic baseline should record, beyond what `sigcheck.exe -v` reports?

I'm writing this in the first person and in chronological order, including the parts where I misread something or chased a dead end, because that is the form of record I want to leave behind. The failures are not embarrassments — they are evidence of what the code is designed to make non-obvious.

---

## 2. Test Environment

| Component | Path | Version | SHA-256 (first 8 bytes) |
|---|---|---|---|
| `skci.dll` | `C:\Windows\System32\skci.dll` | `10.0.28000.2336` | `615D8C124D2C31CC` |
| `securekernel.exe` | `C:\Windows\System32\securekernel.exe` | `10.0.28000.2336` | — |
| `ntoskrnl.exe` | `C:\Windows\System32\ntoskrnl.exe` | `10.0.28000.2336` | — |
| `winload.efi` | `C:\Windows\System32\winload.efi` | `10.0.28000.2336` | — |
| Boot Manager (System32) | `C:\Windows\Boot\EFI\bootmgfw.efi` | `10.0.28000.342` | `456DE3C04EA6A39B` |
| Boot Manager (ESP/Microsoft) | `S:\EFI\Microsoft\Boot\bootmgfw.efi` | `10.0.28000.342` | `200D1E3A6A0DE342` |
| Boot Manager (ESP/fallback) | `S:\EFI\boot\bootx64.efi` | `10.0.28000.342` | `200D1E3A6A0DE342` |
| VBS SI policy | `C:\Windows\System32\CodeIntegrity\VbsSiPolicy.p7b` | — | — |
| Driver SI policy | `C:\Windows\System32\CodeIntegrity\driversipolicy.p7b` | — | — |

| Item | Value |
|---|---|
| OS | Windows 11 Pro 26H1, build 28000, fully patched |
| VBS | Enabled (`{current}.isolatedcontext = Yes`) |
| Hyper-V launch type | `Auto` |
| PDB path | `C:\Symbols` (public Microsoft symbol server) |
| PDB available | `skci.pdb`, `bootmgfw.pdb` — yes. `winload.pdb` — export symbols only |
| Primary tool | CDB (Windows SDK 10.0.28000.2114, AMD64) |
| Constraint | No VTL1 live dump. Static analysis only for skci.dll |

---

## 3. Scope and Honest Constraints

Before the findings: what I cannot see from this analysis.

`skci.dll` runs inside `securekernel.exe`, which is the VTL1 kernel. A normal kernel debugger attached to the NT kernel does not give you a window into VTL1 execution. To single-step through SKCI at runtime you would need either a virtual machine with a VTL1-aware debugger, or a hypervisor trace. I have neither in this session. Everything I report is derived from:

- Disassembly of `skci.dll` at rest, using `cdb uf` / `cdb x` with the public PDB loaded
- Cross-referencing function names from the import table of `securekernel.exe`
- Inferring runtime behavior from code paths, not from observed execution traces

The implications are:

1. Values in global variables (`g_CiOptions`, `g_CiDeveloperMode`, `g_CiScenarios`, etc.) are their link-time defaults in my analysis. Runtime state may differ.
2. "This function calls X" is established. "This function is called from Y" is partially established (via imports and cross-references), but the call graph is necessarily incomplete without a live trace.
3. Where I state a hypothesis about runtime behavior, I will label it as such.

Where the PDB gives me a real name, I use it. Where I had to derive a name analytically, I will mark it in parentheses as *(analytical)*.

---

## 4. The Trust Chain — From UEFI to `skci.dll`

`skci.dll` is not the first line of defense. It receives a world that has already gone through a sequence of trust-narrowing decisions. Understanding that sequence is necessary context for evaluating what SKCI can and cannot guarantee.

flowchart TD A[UEFI firmware] --> B[Secure Boot database\nPK / KEK / db / dbx] B --> C[bootmgfw.efi\n10.0.28000.342] C --> D[BmSecureBootInitializeMachinePolicy\nSecure Boot policy + revocation] D --> E[BlImgQueryCodeIntegrityBootOptions\nCI / HVCI boot options from BCD] E --> F[BlSecureBootPackageActivePolicyForKernel\npolicy package for kernel handoff] F --> G[winload.efi\n10.0.28000.2336] G --> H[BlImgRegisterCodeIntegrityCatalogs\ncatalog + revocation context] G --> I[BlVsmCheckSystemPolicy\nVSM policy gate] I --> J[Hypervisor / isolated context\nVTL1 activation] J --> K[securekernel.exe\n10.0.28000.2336] K --> L[SkciInitialize\nABI version 0x0A000012] L --> M[SkciCreateSecureImage\nimage context allocation] M --> N[SkciValidateImageData\npage data streaming] N --> O[SkciFinalizeSecureImageHash\nhash materialization] O --> P[SkciFinishImageValidation\npolicy + page-use convergence] 

The table below shows what each stage inputs and outputs for the next stage:

| Stage | Key input | Key output |
|---|---|---|
| Firmware | PK/KEK/db/dbx, hardware Secure Boot state | boot application accepted or rejected before execution |
| Boot Manager | Secure Boot policy, BCD options, revocation lists | packaged kernel policy blob, CI boot options |
| OS Loader | CI options, SI policies, catalogs, HVCI options | runtime policy and catalog state, VSM configuration |
| Secure Kernel | VBS / isolated context, packaged policies | VTL1 OS services, SKCI initialization |
| SKCI | PE image bytes, page hashes, signing state | image trust level, authorized page-use flags, attestation data |

Each step is a monotonic reduction: it can only restrict what the next step is allowed to trust, never expand it. A compromised earlier stage invalidates all later guarantees.

### 4.1 Boot Manager phase

`bootmgfw.efi` has public symbols. The Secure Boot initialization sequence is visible in named routines:

| Function | Role |
|---|---|
| `BmSecureBootInitializeMachinePolicy` | Top-level machine policy initialization. Queries Secure Boot state via `SbIsEnabled2`, checks test-root trust via `SbIsTestRootTrusted`, reads firmware revocation lists, sets accepted root-key masks |
| `BmpSecureBootInitializeCurrentPolicy` | Loads current Secure Boot policy; handles the state where a policy is required but absent |
| `BmSecureBootLoadSupplementalPoliciesFromAllPartitions` | Discovers supplemental policy material from storage |
| `BlSecureBootPackageActivePolicyForKernel` | Converts policy state into a kernel-consumable blob via `SbGetSizeOfKernelPolicyPackageForPolicy` → `SbGetKernelPolicyPackageForPolicy` → `BlpPdSaveData` |
| `BlImgQueryCodeIntegrityBootOptions` | Reads CI/HVCI-related BCD boolean options `0x16000048`, `0x16000049`, `0x1600007e` and active policy bits |
| `BlImgValidateFileHash` | Validates a policy hash blob against image bytes using `SymCryptEqual` |
| `BlImgLoadPEImageWithPolicyValidatedHash` | Validates policy-provided hash before using the image |

The key insight from the boot manager phase: **policy is packaged, not transmitted raw**. The handoff from `BlSecureBootPackageActivePolicyForKernel` to the kernel is a compact blob, not a live database. This boot policy package establishes the initial trust baseline; later policy changes go through explicit SKCI policy refresh paths (see `SkciSetCodeIntegrityPolicy`, §15).

### 4.2 OS Loader phase

`winload.efi` does not have a matching full PDB on this host, but export symbols are rich enough:

| Function | Role |
|---|---|
| `BlImgRegisterCodeIntegrityCatalogs` | Registers catalog directories and revocation-list context |
| `BlSIPolicyCheckPolicyOnDevice` | Checks policy presence and applicability for this device |
| `SIPolicyGetHvciOptions` | Extracts HVCI options from policy state |
| `SIPolicyHashActiveCodeExecutionPolicies` | Hashes the active execution policy set |
| `BlVsmCheckSystemPolicy` | VSM policy state gate before launch |
| `BlSetVirtualizationLaunched` | Marks virtualization launch state |

### 4.3 Secure Kernel import surface

`securekernel.exe` imports from `skci.dll` the full API surface documented in §6 — CDB shows 19 `_imp_Skci*` symbols in the import table, covering lifecycle, image validation, query, policy, catalog, cert, hotpatch, and attestation functions. The core image-validation loop drives four calls in sequence:

| Import | Role |
|---|---|
| `SkciCreateSecureImage` | Secure image context creation from PE header mapping |
| `SkciFinalizeSecureImageHash` | Final hash materialization |
| `SkciFinishImageValidation` | Policy, page hash, and attestation convergence |
| `SkciFreeImageContext` | Context teardown and memory release |

`skci.dll` imports VTL1 OS services from `securekernel.exe`:

| Service class | Examples |
|---|---|
| Pool / memory | `SkAllocatePool`, `SkFreePool`, `SkAllocateNormalModePool`, `SkmmFreeSecureAllocation` |
| Locking | `SkInitializePushLock`, `SkAcquirePushLockExclusive`, `SkAcquirePushLockShared`, `SkReleasePushLockExclusive`, `SkReleasePushLockShared` |
| Critical region | `SkeEnterCriticalRegion`, `SkeLeaveCriticalRegion` |
| Object manager | `SkobCreateObject`, `SkobCreateHandle`, `SkobDereferenceObject` |
| Secure Boot query | `SeQuerySecureBootPolicyValue`, `SeQuerySecureBootPlatformManifest` |
| Time / reference | `SkQuerySystemTime`, `ShvlGetReferenceTime` |
| Hotpatch | `NtManageHotPatch` |

The architecture statement this import table makes is clean: `securekernel.exe` provides VTL1 OS services; `skci.dll` implements CI/SI policy, page hashing, attestation, and page-use authorization. Neither can function without the other.

---

## 5. ESP Audit Finding — Version Is Not Identity

While inventorying the test system, I found something worth documenting separately from the SKCI analysis proper.

### 5.1 Two `bootmgfw.efi` with the same version, different hashes

| Path | Size (bytes) | Version | SHA-256 |
|---|---|---|---|
| `C:\Windows\Boot\EFI\bootmgfw.efi` | 3 055 616 | `10.0.28000.342` | `456DE3C04EA6A39B03964181E23A725E9A27A1097D79D02355CBA0A061BD96C1` |
| `S:\EFI\Microsoft\Boot\bootmgfw.efi` | 3 055 456 | `10.0.28000.342` | `200D1E3A6A0DE342A5091654C0E62A434E38D467ADD78057B60A1FDBFC8EF101` |
| `S:\EFI\boot\bootx64.efi` | 3 055 456 | `10.0.28000.342` | `200D1E3A6A0DE342A5091654C0E62A434E38D467ADD78057B60A1FDBFC8EF101` |

All three carry valid Authenticode signatures. Both SHA-256 values in the ESP match each other (`bootx64.efi` is a copy of the ESP-side boot manager). The System32 copy is 160 bytes larger and has a different hash. Same PE version string. Same signed certificate subject. Different binaries.

I did not investigate the root cause. The gap might be a deployment artifact from a Windows Update that staged a new binary to System32 but not to the ESP, or vice versa. What matters for this document is the defensive lesson: **a version string is not a content fingerprint**. Incident response tooling that records `bootmgfw.efi` version `10.0.28000.342` as verified has not recorded enough. The minimum useful baseline is: path, size, SHA-256, certificate subject, certificate thumbprint, signing time. The bootmgfw.efi case is a clean demonstration of why.

### 5.2 Non-standard EFI assets

The ESP inventory (`S:\`) contained the following files that are not part of a standard Windows 11 installation:

| File | Size (bytes) | SHA-256 | Classification |
|---|---|---|---|
| `S:\EFI\boot\HvciBypass.efi` | — | `FFC60B2B21199E9A63585F724A52D4206C9060082492FB6C6220C60ECB8794FF` | Non-standard; name indicates HVCI context/secret |
| `S:\EFI\boot\EfiGuardDxe.efi` | — | `FD0F43E3D8DB8BF118072328CF27FD5AEB7AEF8C356A4177B2F0E8CE650C49A6` | Known EFI bootkit framework, also in OpenCore distributions |
| `S:\EFI\OC\Drivers\EfiGuardDxe.efi` | — | `FD0F43E3D8DB8BF118072328CF27FD5AEB7AEF8C356A4177B2F0E8CE650C49A6` | Same hash — duplicate |
| `S:\EFI\boot\BlackLotus_2th_GEN.efi` | — | `1033F56887519F06D29A9B5688E890E4D4A9DF7A1DF8A6D290EAA3AF06687BB0` | Requires separate classification/secret |
| `S:\EFI\boot\UnderVolter.efi` | — | `5CED5112D8ED2998AD0CEA222661A1EFC14A977D9A285EFFEB9FBA23C97B4F87` | Firmware boot entry points to this path |
| `S:\EFI\boot\Loader.efi` | — | `5C991046E83A5E076F986A0BA3C6D133109E511CD34B5795C881D0D178005822` | Custom loader |

I am documenting these because the ESP is the first file system that the firmware reads after power-on. On this specific research machine their presence is known and intentional — this is a test system, not production. But the broader point stands: **EDR and incident response tools have no standardized, supported channel for alerting on unexpected EFI executables, correlating them with BCD entries, or recording their hashes as part of boot chain telemetry**. The ESP is essentially a blind spot in contemporary enterprise monitoring.

The recommendation is simple: Windows should ship a first-class, supported mechanism for:

1. Enumerating EFI executables on the ESP with hash and signing status
2. Correlating each file against active BCD entries and firmware boot entries
3. Alerting when a new EFI executable appears or an existing one changes hash
4. Distinguishing "signed by Microsoft" from "signed by Microsoft but not expected here"

None of those capabilities exist today in the standard Windows toolset. `bcdedit /enum all` shows boot entries. It does not show you what else lives on the ESP.

---

## 6. The SKCI Export Surface

`skci.dll` exports the following functions. This is the complete API surface that `securekernel.exe` (and potentially other components) consume:

| Export | Category | Purpose |
|---|---|---|
| `SkciInitialize` | Lifecycle | Global CI/SI initialization. Version magic check, crypto init, policy load, catalog setup |
| `SkciCreateSecureImage` | Image | Allocate and populate image validation context from PE header mapping |
| `SkciValidateImageData` | Image | Stream image / page data into the pending validation context |
| `SkciFinalizeSecureImageHash` | Image | Finalize accumulated hash material for the image |
| `SkciFinishImageValidation` | Image | Policy, page hash, authorized-use, and attestation convergence point |
| `SkciFreeImageContext` | Image | Context teardown and memory release |
| `SkciValidateDynamicCodePages` | Dynamic | Page-hash validation for dynamically generated code (non-image-backed) |
| `SkciDetermineAuthorizedPageUse` | Policy | Derive page-use permission flags from signing trust chain |
| `SkciQueryImageUniqueID` | Query | Read unique image ID from validated context (ctx+0x148) |
| `SkciQueryImageAuthorID` | Query | Read author ID from validated context (ctx+0x168) |
| `SkciQueryImageAttestationData` | Query | Read attestation certificate thumbprint and program name |
| `SkciQueryEnclavePackageID` | Query | Read enclave package identifier (guarded by enclave trust bit) |
| `SkciCalculateEnclaveIdentities` | Compute | Compute unique ID and author ID for enclave context |
| `SkciQueryInformation` | Query | Query CI option summary (class 0x67) or serialized policies (class 0xF1) |
| `SkciSetCodeIntegrityPolicy` | Policy | Policy ingestion: deserialize (mode 0), revocation (mode 1), compact update (mode 2) |
| `SkciCreateCodeCatalog` | Catalog | Catalog context creation and registration |
| `SkciDeleteCatalog` | Catalog | Catalog teardown |
| `SkciValidateCertChain` | Cert | Certificate chain validation |
| `SkciValidateAmeCertChain` | Cert | AME (Azure Managed Environment) certificate chain validation |
| `SkciMatchHotPatch` | Hotpatch | Hotpatch identity matching against policy |
| `SkciBuildAttestationReport` | Attestation | Build policy-level attestation report from history |
| `CipForceImageRevalidation` | Internal | Set `g_CiPolicyState` bit 7 — confirmed producer; exact consumer not proven in this static pass |

---

## 7. `SkciInitialize` — The ABI Contract

`SkciInitialize` is the entry point that `securekernel.exe` calls once, at VTL1 startup. The first thing it does is check a version magic:

```asm
skci!SkciInitialize:
    ; rcx = caller context block
    ; first field of that block = version DWORD
    cmp   dword ptr [rcx], 0A000012h
    jne   revision_mismatch

    ; ... initialization proceeds
    jmp   init_body

revision_mismatch:
    mov   eax, 0C0000059h    ; STATUS_REVISION_MISMATCH
    ret
```

If the caller does not present the exact magic `0x0A000012`, initialization fails immediately with `STATUS_REVISION_MISMATCH`. No fallback, no version negotiation, no legacy path. The contract is intentionally rigid — the version must match the binary exactly. On a mismatched build, SKCI initialization would fail; whether the system then continues booting in a degraded state, halts, or BSODs depends on `securekernel.exe`'s policy path for a failed `SkciInitialize` — that caller logic is outside the static scope of this binary.

The initialization sequence that follows:

flowchart TD A[SkciInitialize\ncaller passes 0x0A000012] --> B{version == 0x0A000012?} B -->|no| X[STATUS_REVISION_MISMATCH\n0xC0000059] B -->|yes| C[store g_CiOptions\nfrom boot context] C --> D[SkQuerySecureKernelInformation\nquery SK info needed for CI] D --> E[CiInitializePolicyFromPolicies\nload packaged boot policies] E --> F[g_CiDeveloperMode\nderive flight / test / weak-crypto flags] F --> G[HashpInitializeCrypto\nalgorithm table init] G --> H[CiInitializeParallelHashingContexts\nset up concurrent hash workers] H --> I[CiInitializeGlobalState\ninitialize push locks and state] I --> J[CiInitializeCatalogs\ncatalog registration] J --> K[CiUpdatePolicyHistory\nrecord initial policy in g_CiScenarios] 

Step `CiUpdatePolicyHistory` is the first time `g_CiScenarios+0x3B0` (the attestation history count) gets incremented. This matters for `SkciBuildAttestationReport` — see §12.

---

## 8. The Image Validation Pipeline

The main runtime loop that `securekernel.exe` drives through SKCI:

sequenceDiagram participant SK as securekernel.exe participant SKCI as skci.dll participant Crypto as CiHash / Page hash engine participant Policy as CI policy state SK->>SKCI: SkciCreateSecureImage(pe_header, size, flags) SKCI->>SKCI: CiValidateImageHeaderMapping SKCI->>SKCI: SkciCheckNtHeaderForCompliance SKCI->>SKCI: allocate 0x188-byte context SKCI->>Crypto: CiCalculateFirstPageHash SKCI-->>SK: image_context* SK->>SKCI: SkciValidateImageData(ctx, data_ptr, data_len) SKCI->>Crypto: accumulate page hashes SK->>SKCI: SkciFinalizeSecureImageHash(ctx) SKCI->>Crypto: finalize accumulated hash SK->>SKCI: SkciFinishImageValidation(ctx, signing_info, policy_info) SKCI->>Policy: CiValidateImagePages SKCI->>SKCI: SkciDetermineAuthorizedPageUse SKCI->>SKCI: SkciCalculateEnclaveIdentities SKCI->>SKCI: SkciCalculateImageAttestationData SKCI-->>SK: authorized_page_use_flags, status 

### 8.1 The SKCI image context layout

`SkciCreateSecureImage` allocates a context of exactly `0x188` bytes. The analytically-derived layout from studying field accesses across multiple functions:

| Offset | Name *(analytical)* | Size | Notes |
|---|---|---|---|
| `0x000` | `hash_algorithm` | 4 | Hash algorithm ID used by the verifier |
| `0x004` | `hash_length` | 4 | Hash length in bytes |
| `0x008` | `validated_page_hash_context` | 8 | Pointer to finalized page-hash context |
| `0x010` | `image_or_first_page_hash` | variable | Hash of first page, or finalized image hash |
| `0x0A0` | `image_context_lock` | 8 | Push lock for concurrent access |
| `0x0A8` | `page_hash_generation_context` | 8 | Temporary context for ongoing hash generation |
| `0x0E0` | `copied_image_buffer` | 8 | Pointer to optional image copy |
| `0x0E8` | `rebased_nt_headers` | 8 | NT headers in the image copy |
| `0x0F8` | `image_context_flags` | 4 | Validation state flags |
| `0x118` | **`authorized_page_use_flags`** | 4 | Output of `SkciDetermineAuthorizedPageUse`. Gatekeeper for all Query\* exports |
| `0x11C` | `attestation_thumbprint_algorithm` | 4 | Set to `0x8004` when cert attestation present |
| `0x120` | `attestation_certificate_thumbprint` | 20 | Cert thumbprint bytes from `SkciGetCertificateThumbprint` |
| `0x138` | `attestation_program_name` | 16 | `UNICODE_STRING` from OPUS attribute, if present |
| `0x148` | **`image_unique_id`** | 32 | Computed by `CiCalculateUniqueID` |
| `0x168` | **`image_author_id`** | 32 | Computed by `CiCalculateAuthorID` |

The field at `0x118` — `authorized_page_use_flags` — is the single most important output of the entire validation pipeline. It is checked by every `SkciQuery*` export before returning data:

```asm
; Pattern common to SkciQueryImageUniqueID, SkciQueryImageAuthorID, SkciQueryEnclavePackageID:
mov   eax, [rcx+118h]    ; load authorized_page_use_flags
test  al, 8              ; check bit 3 (enclave_trusted)
jne   authorized
mov   eax, 0C0000022h    ; STATUS_ACCESS_DENIED
ret
```

No authorization flag → no data. The gate is mandatory.

### 8.2 Page hash model

The page-hash record format uses a 36-byte (`0x24`) record:

```
record := uint32 FileOffset || uint8[32] SHA256PageHash
```

Validation reduces to:

```
for each requested_page:
    exists record R where R.FileOffset == page_file_offset
    and constant_time_compare(SHA256(page_bytes), R.SHA256PageHash) == equal
```

| Invariant | Meaning |
|---|---|
| Requested length must be `0x1000`-aligned | No half-pages in authorization decisions |
| Records are sorted by file offset | Binary search is used for lookup (`bsearch`) |
| Each record covers exactly one 4KB page | Granularity matches the page table |
| Overflow in record count computation → `0xC0000095` | Integer overflow guarded explicitly |
| Missing record → `STATUS_INVALID_IMAGE_HASH (0xC0000428)` | One known telemetry correlation point |
| Hash mismatch → `STATUS_INVALID_IMAGE_HASH (0xC0000428)` | Same status for two different conditions |

That last row is a minor diagnostic gap: a missing record and a mismatching hash both surface as `0xC0000428`. An observer who sees this status code cannot immediately determine whether the image is unsigned (no record) or tampered (wrong hash). Distinguishing the two requires deeper inspection of the context state.

---

## 9. `SkciDetermineAuthorizedPageUse` — The Trust Decision Map

This is the function that converts signing trust information into page-use permission flags. It is the convergence point of everything that preceded it: Secure Boot policy, signing level, certificate chain, EKU table, and CI global options.

### 9.1 Input selection

The signing information arrives as an array of entries at `[rdx+0x28]`, with a count at `[rdx+0x20]`. The function selects the *highest-index* valid entry:

```asm
eax = [rdx+0x20]           ; signing result count (0..4)
if eax != 0: eax = (eax-1) & 3
rsi = rdx+0x28 + eax*0xA0  ; select signing_info entry
```

Each entry is `0xA0` bytes. The selection of the last (highest-index) entry prefers the strongest available trust claim when multiple signing chains are present.

### 9.2 The page-use flag bitmap

The output is a 4-byte DWORD at `ctx+0x118`. Each bit encodes a specific trust grant:

| Bit | Hex | Name *(analytical)* | How it is set |
|---|---|---|---|
| 0 | `0x01` | `IUM_execute` | `CiIsSignatureTrustedForIum(rsi)` returns true, OR `input_flags & 5` non-zero, OR `g_CiOptions bit 21` AND `input bit 30` |
| 1 | `0x02` | `IUM_trusted` | `CiIsSignatureTrustedForIum(rsi)` confirmed (stronger IUM grant) |
| 2 | `0x04` | `WHQL_level` | `signing_level >= 0x0C` OR EKU `1.3.6.1.4.1.311.76.8.1` present at level `0x08..0x0C` |
| 3 | `0x08` | `enclave_trusted` | `CiIsSignatureTrustedForVsmEnclaves(rsi)` returns true |
| 4 | `0x10` | `whql_bio` | `CiIsSignatureWhqlBio(rsi)` returns true (WHQL Bio-specific) |
| 5..9 | `0xE0`..`0x1E0` | `signing_level<<5` | Raw signing level encoded in upper bits |

The base value for these bits is established before any condition check: `(signing_level & 0xF) << 5`. This means the raw signing level is always recorded in the flags, regardless of which conditional grants succeed.

flowchart TD A[SkciDetermineAuthorizedPageUse] --> B[select highest-index signing_info entry] B --> C[base = signing_level AND 0xF SHL 5] C --> D{CiIsSignatureTrustedForIum?} D -->|yes| E[set bit 0 and bit 1\nIUM_execute and IUM_trusted] E --> F{CiIsSignatureWhqlBio?} F -->|yes| G[set bit 4\nwhql_bio] D -->|no| H{input_flags AND 5 nonzero?} H -->|yes| I[set bit 0\nIUM_execute fallback] H -->|no| J{g_CiOptions bit 21\nAND input bit 30?} J -->|yes| I G --> K{CiIsSignatureTrustedForVsmEnclaves?} I --> K K -->|yes| L[set bit 3\nenclave_trusted] K -->|no| M{bit 0 set?} L --> M M -->|yes| N{signing_level ge 0x0C?} N -->|yes| O[set bit 2\nWHQL_level] N -->|no| P{level 0x08 to 0x0B\nAND WHQL-Ext EKU present?} P -->|yes| O P -->|no| Q[bit 2 remains 0] O --> R[write authorized_page_use_flags to ctx+0x118] Q --> R 

### 9.3 The EKU fallback path (Finding 6)

The most non-obvious branch in this function concerns signing levels between `0x08` and `0x0C`. For an image with `signing_level >= 0x0C` (full WHQL level), bit 2 (`WHQL_level`) is set unconditionally when bit 0 is set. For levels `0x08` through `0x0B`, there is a fallback: if the EKU `1.3.6.1.4.1.311.76.8.1` is present in the signing chain, bit 2 is still granted.

`1.3.6.1.4.1.311.76.8.1` is `szOID_WINDOWS_HARDWARE_DRIVER_EXTENDED_VERIFICATION` — the Windows Hardware Quality Labs extended verification OID. Its presence is checked via `MincryptIsEKUInPolicy`. This means a driver signed at level `0x08` (WHQL submission level) can obtain `WHQL_level` trust if the extended verification EKU was issued as part of its certificate. The signing level alone does not tell you whether `WHQL_level` is set; you also need to check the EKU.

---

## 10. Finding 1: `SkciValidateDynamicCodePages` — Success Is `0x12C`

This was the first thing that stopped me cold. The function is small enough to read in one pass:

```asm
skci!SkciValidateDynamicCodePages:
    sub   rsp, 48h
    mov   eax, 20h           ; hash_length = 0x20 (SHA-256)
    shr   r9, 5              ; page_hash_count = caller_byte_length >> 5
    ; [stack spills of arguments]
    mov   r8d, 800Eh         ; algorithm = 0x800E
    call  CiValidateFullImagePages
    test  eax, eax
    mov   ecx, 12Ch          ; preload 0x12C
    cmovns eax, ecx          ; if eax >= 0 (no sign bit): replace with 0x12C
    ret
```

`cmovns` is "conditional move if not signed" — move if the sign flag is clear. Since `test eax, eax` sets the sign flag when `eax < 0` (negative, i.e. error status), the `cmovns` fires precisely when `CiValidateFullImagePages` returned a non-negative status, which in NTSTATUS terms means success or an informational code.

**The success return value is `0x12C`, not `STATUS_SUCCESS (0x00000000)`.**

`NT_SUCCESS(0x12C)` is `TRUE` — bit 31 is clear, so it passes the canonical success check. But `status == 0` is false. Any caller that checks `status == 0` as its success condition gets a false negative on a successfully validated dynamic code page set.

The algorithm code `0x800E` is distinct from the image-backed page hash algorithm `0x800C`. They are not interchangeable — the algorithm index routes to different hash-algorithm table entries inside `CiValidateFullImagePages`. Dynamic code pages and image-backed pages go through different validation paths even when both use SHA-256.

The `r9 >> 5` conversion deserves a note: the input `r9` is a byte count of the hash buffer, not a page count. Each hash record is `0x20` bytes (SHA-256 length). So `byte_count >> 5` = `byte_count / 32` = record count. The page count must match exactly or the validation fails before hashing begins.

### What this means for callers

Caller correlation confirms `securekernel.exe` handles this correctly. In `securekernel!SkmiValidateDynamicCodePages`:

```asm
call    qword ptr [securekernel!_imp_SkciValidateDynamicCodePages]
test    eax, eax
js      error_path
```

The caller uses NT sign-bit testing, not equality-to-zero. `0x12C` is non-negative, so `js` does not branch — the call is treated as success. This is a valid internal ABI contract. **The risk is not a Microsoft caller bug; the risk is for analysts or reimplementations that assume success means `status == 0` only.** A correctly-written external consumer must check `NT_SUCCESS(status)` or compare against `0x12C` explicitly. The public documentation for this function does not exist (it is not documented by Microsoft at the time of writing). Without the symbol information that the public PDB provides, a researcher reading the binary would see `cmovns ecx, 12Ch` and might spend time wondering whether `0x12C` is an error variant rather than the primary success path.

---

## 11. Finding 2: `SkciQueryEnclavePackageID` — Authorized but Empty

```asm
skci!SkciQueryEnclavePackageID:
    mov   eax, [rcx+118h]       ; load authorized_page_use_flags from context
    test  al, 8                 ; bit 3 = enclave_trusted
    jne   authorized

    mov   eax, 0C0000022h       ; STATUS_ACCESS_DENIED
    ret

authorized:
    mov   byte ptr [rdx], 0     ; zero first output byte
    xorps xmm0, xmm0
    movups [r8], xmm0           ; zero bytes 0..15 of output buffer
    xor   eax, eax
    movups [r8+10h], xmm0       ; zero bytes 16..31
    mov   [r8+20h], rax         ; zero bytes 32..39   (total: 40 bytes)
    ; eax is already 0 = STATUS_SUCCESS
    ret
```

The authorization check is real: if `ctx->authorized_page_use_flags & 0x08` is zero, the function returns `STATUS_ACCESS_DENIED`. The enclave trust bit must have been set by `SkciDetermineAuthorizedPageUse` — which requires `CiIsSignatureTrustedForVsmEnclaves` to have returned true — or the gate closes.

When the gate opens: 40 bytes of zeros, `STATUS_SUCCESS`.

**No package ID is computed. No call to `CiCalculateUniqueID` or `CiCalculateAuthorID` here.**

The actual enclave identities — unique ID and author ID — are computed by `SkciCalculateEnclaveIdentities` and stored at:

- `ctx+0x148`: `image_unique_id` (32 bytes, via `CiCalculateUniqueID`)
- `ctx+0x168`: `image_author_id` (32 bytes, via `CiCalculateAuthorID`)

These are read back by `SkciQueryImageUniqueID` and `SkciQueryImageAuthorID`, not by `SkciQueryEnclavePackageID`. The three query functions are separate exports that address different parts of the context.

### Negative finding or stub?

Two interpretations:

1. **Stub not yet implemented.** The function has the gate logic of a complete API but returns a zeroed buffer where a package identifier should be. This is consistent with an in-progress feature where the authorization model was built before the identity computation was wired up.
2. **Intentional.** The package ID in the enclave sense may be a compound derived from policy state rather than a per-image computation. In that interpretation the caller gets `STATUS_SUCCESS` and zeros as a "not applicable in this context" signal, and the real enclave identification goes through `SkciQueryImageUniqueID` / `SkciQueryImageAuthorID`.

I cannot resolve this from static analysis alone. What I can state as fact: in build `10.0.28000.2336`, the 40-byte output buffer is always zeroed when this function succeeds. A caller who reads from it gets nothing.

---

## 12. Finding 3: `SkciQueryInformation` — Two Classes, Rigid Protocol

```asm
skci!SkciQueryInformation:
    ; [prologue and argument save]
    SkeEnterCriticalRegion
    SkAcquirePushLockShared(g_CipPolicyLock)

    cmp   esi, 67h              ; class 0x67?
    je    class_67
    cmp   esi, 0F1h             ; class 0xF1?
    je    class_f1

    mov   ebx, 0C0000003h       ; STATUS_INVALID_INFO_CLASS
    jmp   exit
```

Only two branches. Everything else — any class value except `0x67` and `0xF1` — falls through to `STATUS_INVALID_INFO_CLASS (0xC0000003)`. There is no `default` that logs the unknown class or routes to a stub. The lock is acquired, the class is checked, the lock is released, and the caller gets an error. The surface is intentionally minimal.

### Class `0x67` — CI options summary

The protocol for class `0x67` is unusually rigid:

```
Preconditions (ALL must hold):
    input_buffer_length == 8        (exact, not >=)
    [input_buffer + 0x00] == 8      (caller must pre-fill magic DWORD)
    output_buffer_length == 8       (exact)
```

If any of these fail: `STATUS_INFO_LENGTH_MISMATCH (0xC0000004)`.

When all pass, SKCI builds an 8-byte response:

| Output bytes | Source | Meaning |
|---|---|---|
| `[output+0x00]` | Echoed from input `[input+0x00]` = 8 | Magic roundtrip (protocol confirmation) |
| `[output+0x04]` bit 0 | `g_CiOptions & 2` | CI enforcement active |
| `[output+0x04]` bit 1 | `g_CiOptions & 2` AND `g_CiOptions & 8` | Both CI flags set |
| `[output+0x04]` bit 9 | `g_CiDeveloperMode & 0x40` | Developer/flight mode |
| `[output+0x04]` bit 14 | `CipWhqlEnforcementEnabled()` | WHQL enforcement active |
| `[output+0x04]` bit 15 | `CipWhqlEnforcementEnabled()` inner result | WHQL enforcement sub-condition |

`g_CiOptions` has many more bits internally. The API exposes only 5 derived flags. The pre-fill requirement (`[input+0x00] == 8`) acts as a simple protocol versioning token — a caller that does not know the protocol sends a wrong magic and gets `STATUS_INFO_LENGTH_MISMATCH` instead of garbage output.

### Class `0xF1` — serialized policies

Standard query-size-then-copy pattern:

```
size = SIPolicyGetSerializedPoliciesSize()
if (input_buffer_length < size):
    *return_length = size
    return STATUS_INFO_LENGTH_MISMATCH
if (size != 0):
    SIPolicyGetSerializedPolicies(output_buffer, size)
*return_length = size
return STATUS_SUCCESS
```

No protocol magic, no exact-size requirement. The caller passes a buffer, gets the serialized policies or a required-size hint.

### Caller path: `IumpReadCodeIntegrity`

Caller correlation shows `SkciQueryInformation` is not dead code. It is called from `securekernel!IumpReadCodeIntegrity` using a canonical two-pass sizing pattern:

| Step | Behavior |
|---|---|
| 1 | Call `SkciQueryInformation` with zero output length to query required size |
| 2 | Accept `STATUS_INFO_LENGTH_MISMATCH` as sizing response |
| 3 | For small outputs (class `0x67`): use stack storage |
| 4 | For larger serialized policy data (class `0xF1`): allocate pool |
| 5 | Call `SkciQueryInformation` again and copy output to caller |

This places the two-class surface in a secure-service read path. The two classes are not arbitrary — class `0x67` is the small state summary, class `0xF1` is the heavier serialized policy channel for consumers that need the full policy set.

### The diagnostic gap

What `SkciQueryInformation` does *not* expose:

| Missing class | What a defender would want |
|---|---|
| Page hash validation state | Did the last image load pass or fail page hash coverage? |
| Signing level of last validated image | What level was granted? |
| Catalog source | Was the hash found in an embedded catalog, a file catalog, or generated page hashes? |
| Attestation report excerpt | What does the current policy state attest to? |
| Image validation failure detail | Why did a specific image fail — missing record, hash mismatch, policy denial, or unauthorized page use? |

These are exactly the questions an incident responder wants to answer when investigating a code-integrity event. The current API provides a two-class aperture: a compressed CI options summary and serialized policies. That is sufficient for routine monitoring and policy audit. It is not sufficient for detailed root-cause investigation of a code-integrity failure.

---

## 13. Finding 4: `SkciBuildAttestationReport` — BugCheck on Empty History

This is the finding I find most interesting from a reliability engineering perspective.

```asm
skci!SkciBuildAttestationReport:
    cmp   cx, 1              ; version check
    je    version_ok
    mov   eax, 0C00000EFh   ; STATUS_NOT_IMPLEMENTED (version != 1)
    ret

version_ok:
    SkeEnterCriticalRegion
    SkAcquirePushLockShared(g_CipPolicyLock)

    mov   ecx, [g_CiScenarios+3B0h]   ; attestation history count
    cmp   ecx, 1
    jae   count_ok                    ; count >= 1: proceed

    ; count is 0. Check for non-zero anyway (unreachable for a DWORD):
    test  ecx, ecx
    jne   nonzero_path

    ; BugCheck:
    xor   r9d, r9d
    mov   qword ptr [rsp+20h], 0
    mov   edx, 74614943h              ; ASCII "CiAt" as little-endian immediate
    lea   ecx, [0+29h]               ; BugCheckCode = 0x29 (SECURITY_SYSTEM)
    mov   r8, 0FFFFFFFFC000000Dh     ; 0xC000000D = STATUS_INVALID_PARAMETER
    call  KeBugCheckEx
    int   3
```

| BugCheck Parameter | Value | Meaning |
|---|---|---|
| Code | `0x29` | `SECURITY_SYSTEM` |
| P1 | `0x29` | (same as code, by convention for SECURITY\_SYSTEM) |
| P2 | `0x74614943` | ASCII "CiAt" in little-endian — the CI attestation tag |
| P3 | `0xC000000D` | `STATUS_INVALID_PARAMETER` — the proximate cause |
| P4 | `0x0` | — |

Zero attestation history is not handled as an API error. It is treated as a kernel invariant violation. The reasoning is: if `SkciBuildAttestationReport` is being called, the system must have been initialized. `SkciInitialize` calls `CiUpdatePolicyHistory` as its last step. So if the history count is zero when `SkciBuildAttestationReport` is reached, either initialization did not complete, or the history counter was corrupted. Either way, continuing execution would produce an attestation report whose fundamental premise — "this policy state has a recorded history" — is false. A BSOD is the correct response.

### The rest of the function

When `count >= 1`, the function builds the attestation report from `g_CiScenarios`:

| Field accessed | Offset in `g_CiScenarios` | Content |
|---|---|---|
| State | `+0x390` | State pointer |
| Reference time | `+0x398` | `ShvlGetReferenceTime` result |
| Report pointer | `+0x3A0` | Pointer to built report payload |
| Report size | `+0x3A8` | Report payload size |
| History count | `+0x3B0` | Count of recorded scenarios |

Buffer-size query pattern (for callers who need the size before allocating):

```
if (buffer == NULL || buffer_size < required):
    neg   rsi
    sbb   eax, eax
    and   eax, 0xC0000023    ; STATUS_BUFFER_TOO_SMALL if buffer was non-NULL
    ret
```

The version field (`cx == 1`) exists because the attestation report format is versioned. A future version of the report structure would use a different version byte and would be handled by a separate code path (which does not yet exist in this build — version != 1 returns `STATUS_NOT_IMPLEMENTED`).

### Attestation architecture: two layers

SKCI implements two distinct attestation layers that are often confused:

| Layer | Functions | What it attests |
|---|---|---|
| Image-level | `SkciCalculateImageAttestationData`, `SkciQueryImageAttestationData` | Certificate thumbprint and OPUS program name for a specific validated image |
| Policy / state-level | `SkciBuildAttestationReport`, `CiGenerateAttestationReport`, `CipAddAttestationReportToHistory` | CI policy state, revocation list hash, blocklist update time, policy scenario history |

`CiGenerateAttestationReport` builds the policy-level report with this header format:

| Report offset | Content | Notes |
|---|---|---|
| `0x00` | `1` | Report version |
| `0x04` | `0x3A` | Header size |
| `0x08` | `0x50013` | Type identifier *(observed, no public name)* |
| `0x0C` | `g_CiBlocklistUpdateTime` | Blocklist update timestamp |
| `0x14` | `0x20` | Hash length |
| `0x18` | `0x0B` | Hash algorithm identifier *(observed, no public name)* |
| `0x1A` | Revocation list attestation hash | 32 bytes (or length matching the algorithm) |
| `0x3A+` | Serialized policy attestation payload | `SIPolicyGetSerializedPolicies` output |

Pool allocation tag for the report buffer: `0x74614943` — the same "CiAt" tag that appears in the BugCheck parameter.

### Caller path: `SkpBuildAuthenticatedRuntimeReports`

Caller correlation shows `SkciBuildAttestationReport` is called from `securekernel!SkpBuildAuthenticatedRuntimeReports`, which builds a broader authenticated runtime report bundle:

| Report bit | Caller behavior |
|---|---|
| bit 0 | Build NT drivers report |
| bit 1 | Call `SkciBuildAttestationReport(1, size, buffer, &size)` |

The caller handles buffer-sizing and aggregates report sizes across multiple report types. This means the SKCI policy attestation is not a standalone blob — it is one component of a wider authenticated runtime report that `securekernel.exe` composes. The bugcheck-on-empty-history invariant therefore protects a report that feeds into that larger bundle.

---

## 14. Finding 5: `CipForceImageRevalidation` — Two Instructions

```asm
skci!CipForceImageRevalidation:
    bts   dword ptr [skci!g_CiPolicyState], 7   ; atomically set bit 7 = 0x80
    ret
```

That is the complete function. One memory operation, one return. It sets bit 7 of `g_CiPolicyState` as a revalidation signal.

The bit is not read anywhere inside `skci.dll` — the static analysis confirms no internal consumer of bit 7. **This pass confirms `CipForceImageRevalidation` as the producer of bit 7; the exact consumer in `securekernel.exe` was not proven in this static pass.** The architectural implication is that SKCI raises the signal and the secure kernel is the expected consumer, but the precise reader and the action it takes (re-requesting validation, updating trust decisions) would require a live data watchpoint on `g_CiPolicyState` bit 7 during a policy refresh to confirm. That is a recommended next step if this signal's consumer semantics matter for defensive tooling.

### When it is called

Inside `CipRefreshPolicies` (called by `SkciSetCodeIntegrityPolicy` mode 0):

```asm
test  dword ptr [rbp-68h], 1000000h   ; check incoming policy option bit 24
je    call_force                       ; bit 24 clear → always force revalidation
test  sil, sil                         ; bit 24 set → check secondary flag
jne   skip_force                       ; secondary flag set → skip
call_force:
    call CipForceImageRevalidation
```

So the revalidation signal fires unless the incoming policy *explicitly* carries bit 24 AND a secondary flag is non-zero. The default behavior — no special option — triggers revalidation on every policy push. This means every time a new WDAC policy is applied, any image that was loaded under the old policy has its trust status pending re-evaluation.

### `g_CiPolicyState` bit map

From `CipUpdateCiSettingsFromPolicies` and related functions:

| Bit | Hex | Operation | Meaning |
|---|---|---|---|
| 7 | `0x80` | `bts` (set, `CipForceImageRevalidation`) | Revalidation-pending signal — producer confirmed; consumer outside SKCI static scope |
| 8 | `0x100` | `bts` (set, `CipUpdateCiSettingsFromPolicies`) | CI policy option |
| 9 | `0x200` | `bts` (set, `CipUpdateCiSettingsFromPolicies`) | CI policy option |
| 11 | `0x800` | `btr` (clear, `CipUpdateCiSettingsFromPolicies`) | CI policy option |
| 12 | `0x1000` | `btr` (clear, `CipUpdateCiSettingsFromPolicies`) | CI policy option |
| 22 | `0x400000` | `btr` (clear, `SkciFinishImageValidation`) | consumed via `shr eax, 0x16` — controls hash / page-hash path |
| init | `0x1800` (bits 11+12) | Set at start of `CipUpdateCiSettingsFromPolicies` | Default state before per-policy derivation |

Bit 22 in `SkciFinishImageValidation`: `shr [g_CiPolicyState], 0x16` makes bit 22 become bit 0 of the shifted result. That bit then controls a branch in the image validation path, selecting between the pure-hash and the page-hash validation strategies. The precise semantics require a live trace to confirm, but the observation establishes that `g_CiPolicyState` encodes not just policy state but also runtime validation strategy selection.

---

## 15. Finding 7: `SkciSetCodeIntegrityPolicy` — Mode Map

| Mode (`ecx`) | Size requirement | Action |
|---|---|---|
| `0` | Any | `CiDeserializePolicyFileData` then `CipRefreshPolicies` (full policy replace) |
| `1` | Non-zero buffer | `CiValidateAndSetRevocationList` |
| `2` | Must be exactly `5` | Write 4+1 bytes to `g_CiScenarios+0x3B4/0x3B8` under exclusive lock |
| Anything else | — | `STATUS_NOT_SUPPORTED (0xC00000BB)` |

Mode 2 with size != 5 returns `STATUS_INVALID_PARAMETER (0xC000000D)`. The 5-byte payload goes into two fields of `g_CiScenarios` in a single exclusive-lock section — making it a compact atomic scenario state update. The specific semantics of those 5 bytes (4+1) require correlating with the CI scenario enum, which is not fully reconstructed in this analysis.

### Mode 0 — full policy refresh

The main policy refresh path (`CipRefreshPolicies`) is the most involved:

```
SkciSetCodeIntegrityPolicy(mode=0)
    → CiDeserializePolicyFileData
    → CipRefreshPolicies
        → SIPolicyStateAddPolicy / SIPolicyStateRemovePolicy
        → SIPolicyStateFinalize
        → SIPolicyStatePreparePolicyHashSHA256
        → CiUpdatePolicyHistory         ← increments g_CiScenarios+0x3B0
        → SIPolicyCommitState
        → update g_SiPolicyHash
        → CipUpdateCiSettingsFromPolicies
        → maybe CipForceImageRevalidation  ← sets g_CiPolicyState bit 7
```

SKCI treats policy change as a *generation event*: it finalizes a new state, hashes it, commits it, updates the attestation history, and may signal image revalidation. Policy is not applied piecemeal — it is replaced atomically.

---

## 16. Hotpatch Matching

`SkciMatchHotPatch` is the final identity comparator in a broader hotpatch match sequence driven by `securekernel!SkmiMatchPatchImage`. It is not the sole authorizer — `SkmiMatchPatchImage` performs record field checks and base machine metadata matching before reaching SKCI. `SkciMatchHotPatch` itself compares an identity hash from the caller against a blob field, using one of two algorithm-length combinations.

```
if alg == 0x8004 and len == 0x14:
    compare input_hash against hotpatch_blob + 0x20
elif alg == 0x800C and len == 0x20:
    compare input_hash against hotpatch_blob + 0x00
else:
    return false
```

| Algorithm | Length | Blob offset | Interpretation |
|---|---|---|---|
| `0x8004` | 20 bytes | `+0x20` | 20-byte thumbprint-style identifier |
| `0x800C` | 32 bytes | `+0x00` | SHA-256 hash at the start of the blob |

The helper `CiHotpatchGetSequenceNumberFromHotpatchInfo` requires the blob to be at least `0x0C` bytes and reads the sequence number from `+0x08`. The helper `SIPolicyDoesHotpatchExistForTarget` uses `NtManageHotPatch` with class `0x0B` and a 20-byte (`0x14`) output buffer.

The policy question for hotpatch is framed differently than for image loading. Rather than "is this image signed?", the question is "does the policy know this target, and does the patch identity match what the policy recorded?" The sequence number in the blob is read by `CiHotpatchGetSequenceNumberFromHotpatchInfo`; it likely participates in ordering or versioning, but the enforcement semantics require correlating with the caller in `securekernel.exe`, which is outside the static scope of this binary.

---

## 17. `SkciInitialize` — Version Magic and Initialization

One detail from `SkciInitialize` that belongs in the findings list even though it was the first thing I disassembled: the version magic `0x0A000012`.

The check is:

```asm
cmp   dword ptr [rcx], 0A000012h
jne   revision_mismatch
```

This is a strict ABI contract between `securekernel.exe` and `skci.dll`. There is no tolerance, no negotiation. The `0x0A` prefix is consistent with encoding a major version; the `0x000012` suffix appears to be the minor/patch component. If Microsoft ships a new `skci.dll` that uses a different magic, any `securekernel.exe` built against the old ABI will fail SKCI initialization. The final boot behavior then depends on the caller's policy path and fail-open/fail-closed decision for that boot configuration — which is why this check exists: it is better to fail a protected path explicitly than to operate in a partially initialized state that gives false assurance.

---

## 18. The Full Trust Decision Model

Synthesizing everything above, the authorization decision for a page can be framed as:

```
AllowedExecute(image, page, use) :=
    SecureBootState.valid                              [firmware gate]
    AND PolicyGeneration.current                       [SKCI policy state]
    AND ImageHeader.valid                              [SkciCheckNtHeaderForCompliance]
    AND CiValidateImageHeaderMapping.passed            [header mapping gate]
    AND PageHash(page).valid                           [page hash coverage]
    AND SigningLevel(image) >= RequiredLevel(use)      [signing level gate]
    AND AuthorizedPageUse(image, use) != Deny          [SkciDetermineAuthorizedPageUse]
    AND Policy.allows(image, use)                      [WDAC / SI policy check]
```

Each component is enforced at a different stage:

flowchart LR subgraph Firmware SB[Secure Boot\nvalidates bootmgfw.efi] end subgraph BootManager BM[bootmgfw.efi\npackages policy for kernel] end subgraph OSLoader WL[winload.efi\nVSM policy check\ncatalog registration] end subgraph SecureKernel SK[securekernel.exe\nVTL1 OS services] end subgraph SKCI CTX[SkciCreateSecureImage\nheader + page hash context] VAL[SkciFinishImageValidation\npage hash coverage] AUTH[SkciDetermineAuthorizedPageUse\nsigning level + EKU + policy bits] ATTEST[SkciCalculateImageAttestationData\ncert thumbprint + OPUS] end SB --> BM --> WL --> SK --> CTX --> VAL --> AUTH --> ATTEST 

The mathematical framing is intentionally a *defensive abstraction*, not an implementation bypass map. Its value is in showing which inputs feed which decisions — useful for writing test cases, for building diagnostic tooling, and for understanding which layer a code-integrity failure implicates.

---

## 19. Status Code Diagnostic Map

A full table of NTSTATUS values observed in SKCI and their structural meaning:

| Status | Hex | Source function(s) | Meaning |
|---|---|---|---|
| `STATUS_SUCCESS` | `0x00000000` | Most functions | Normal success |
| `0x12C` | `0x0000012C` | `SkciValidateDynamicCodePages` | Dynamic page success — **not STATUS\_SUCCESS** |
| `STATUS_INFO_LENGTH_MISMATCH` | `0xC0000004` | `SkciQueryInformation` | Buffer size wrong or magic mismatch |
| `STATUS_INVALID_INFO_CLASS` | `0xC0000003` | `SkciQueryInformation` | Class not `0x67` or `0xF1` |
| `STATUS_INVALID_IMAGE_HASH` | `0xC0000428` | Page hash validation | Hash missing or mismatch |
| `STATUS_INTEGER_OVERFLOW` | `0xC0000095` | Context size arithmetic | Record count × size overflow |
| `STATUS_OBJECT_NAME_NOT_FOUND` | `0xC0000225` | `SkciQueryImageAttestationData` | `ctx+0x11C == 0` — no attestation thumbprint |
| `STATUS_ACCESS_DENIED` | `0xC0000022` | `SkciQueryEnclavePackageID` | `enclave_trusted` bit not set |
| `STATUS_NOT_IMPLEMENTED` | `0xC00000EF` | `SkciBuildAttestationReport` | Version != 1 |
| `STATUS_NOT_SUPPORTED` | `0xC00000BB` | `SkciSetCodeIntegrityPolicy` | Unknown mode |
| `STATUS_BUFFER_TOO_SMALL` | `0xC0000023` | `SkciBuildAttestationReport` | Buffer null or too small for report |
| `STATUS_INVALID_PARAMETER` | `0xC000000D` | Multiple | Various protocol violations |
| `STATUS_REVISION_MISMATCH` | `0xC0000059` | `SkciInitialize` | ABI version != `0x0A000012` |
| BSOD `0x29 SECURITY_SYSTEM` | — | `SkciBuildAttestationReport` | `g_CiScenarios+0x3B0 == 0` — invariant violation |
| `0x80430006` | `0x80430006` | Boot-phase VSM | Secure Boot not active |
| `0xC0450000` | `0xC0450000` | Boot-phase VSM | VSM not initialized |
| `0xC0450001` | `0xC0450001` | Boot-phase VSM | No DMA protection from IOMMU |
| `0xC0430001` | `0xC0430001` | Secure Boot policy | Rollback of protected data detected |
| `0xC043000E` | `0xC043000E` | Boot Manager | Required Secure Boot policy not found |

Two observations from this table:

1. `STATUS_INVALID_IMAGE_HASH` (`0xC0000428`) covers both "record not found" and "record found but hash mismatch". From the outside these look identical. Distinguishing them requires either a debugger or SKCI-level diagnostic telemetry that does not currently exist in the public API.
2. The BSOD on zero attestation history is the only kernel-stop condition in the export surface. Everything else recovers gracefully. This is consistent with Microsoft's design intent: attestation integrity is a hard invariant; everything else is a soft policy decision that can be retried or reported.

---

## 20. What Is Missing From the Diagnostic Surface

The most practically useful output of this analysis, for a defender or an enterprise security architect:

| Missing capability | Current state | Impact |
|---|---|---|
| **Per-image CI failure detail** | `STATUS_INVALID_IMAGE_HASH` only; no sub-code | Cannot determine whether failure is missing record, hash mismatch, policy deny, or unauthorized page use — requires kernel debugger |
| **Signing level query** | Not exposed through any public API | Cannot verify what signing level was granted to a specific loaded image without debugging the SKCI context |
| **ESP baseline comparison** | No OS-supported mechanism | Cannot alert on unexpected EFI executables or version-vs-hash mismatches without writing custom tooling |
| **BCD-to-ESP correlation** | `bcdedit /enum all` shows boot entries; no cross-reference to what's on the partition | Unknown EFI executables on ESP are invisible to standard audit tooling |
| **Attestation clarity** | No distinction between "no attestation data" (cert absent) and "attestation failed" | `STATUS_OBJECT_NAME_NOT_FOUND` is ambiguous |
| **WDAC policy source** | `SkciQueryInformation` class `0xF1` returns serialized policies — correct, but large and unstructured for telemetry | Enterprise SIEM cannot directly index policy IDs from this output |
| **Dynamic code audit** | `SkciValidateDynamicCodePages` returns `0x12C`; no event that says "dynamic code was allowed at this address" | Difficult to audit dynamic code authorization from outside VTL1 |

---

## 21. Recommendations

These are addressed to Microsoft, to vendors building security tooling, and to enterprise security architects who own VBS/HVCI deployments.

### For Microsoft

1. **Document `SkciValidateDynamicCodePages`' success code.** `0x12C` as a success value is correct internally, but it is invisible to external callers without the PDB. At minimum, document it in the SKCI API documentation or in the Windows Internals series.
2. **Extend `SkciQueryInformation` with additional information classes.** At minimum: signing level of last validated image, cause of last page hash failure (missing vs mismatch), policy ID that authorized the last image. These would allow WDAC incident response without requiring a kernel debugger.
3. **Ship a first-class ESP audit tool.** A supported PowerShell cmdlet that enumerates EFI executables with SHA-256, signing status, and BCD correlation would eliminate a significant blind spot in the Windows security baseline.
4. **Clarify attestation semantics.** `SkciQueryImageAttestationData` returning `STATUS_OBJECT_NAME_NOT_FOUND` when `ctx+0x11C == 0` is semantically ambiguous. A dedicated "no attestation present" status code or a documented field that distinguishes "not applicable" from "failed" would improve incident diagnosis.
5. **Consider a log event for `CipForceImageRevalidation`.** Currently there is no observable event when a policy push triggers image revalidation. Enterprise telemetry cannot see when previously-loaded images are queued for re-evaluation.

### For security tooling vendors

1. **Record SHA-256, not just version, for all EFI executables in your baseline.** The `bootmgfw.efi` finding in §5.1 is a direct demonstration that two files with the same version and valid signatures can differ in content.
2. **Treat `STATUS_INVALID_IMAGE_HASH` as requiring investigation, not just logging.** The status is returned for both missing records and hash mismatches — two very different conditions from an incident response perspective.
3. **Add ESP monitoring to endpoint agents.** If you monitor files in `C:\Windows\System32`, you should also monitor the EFI System Partition. The ESP is smaller and changes less frequently; anomaly detection is tractable.

### For enterprise architects

1. **Use `SkciQueryInformation` class `0x67` as a sanity check, not a definitive audit.** The 5 derived bits it returns confirm HVCI/WHQL state at a high level but do not replace a full policy review.
2. **Document your expected ESP contents.** Run the inventory once on a known-good baseline, record SHA-256 for every EFI executable, and alert on deviations. The cost is low; the visibility improvement is large.

---

## 22. The Privileged Instrument Problem

I want to end with a structural observation about this kind of analysis.

The public PDB for `skci.dll` is the instrument that made this write-up possible at its current depth. Without it, I would have had to derive all function names analytically — a process that introduces naming ambiguity and inference gaps. With it, the findings I report here are grounded in Microsoft's own naming decisions, which are more reliable than my interpretations.

This is a fundamentally asymmetric situation. Microsoft publishes public PDBs for specific components and not others. The choice of which symbols to publish is a policy decision with real consequences for the security research ecosystem: public symbols lower the barrier for defensive analysis, but they also lower the barrier for offensive analysis. The SKCI symbols are here not because the module is deliberately open — it is not — but because debugging the VTL1 boot path requires symbolic resolution, and public debuggers are a legitimate tool.

I am not arguing that Microsoft should publish more symbols. I am arguing that the existence of public symbols for `skci.dll` does not make the module "known" in any meaningful sense. The symbols give you names. The semantics, the invariants, the non-obvious contracts — those require the kind of analysis recorded here. And as the `SkciValidateDynamicCodePages` finding shows: even with correct function names and correct disassembly, the non-obvious detail (`0x12C` as success, not `0x00`) is invisible until you read the assembly closely enough to notice the `cmovns`.

The lesson I take from this: **public symbols are a starting point, not a finish line**. The research work is in understanding why the code is written the way it is — and in being honest about what that understanding does and does not cover.

---

## Appendix A: Full SKCI Export List with Classification

| Export | Category | Public PDB name | Notes |
|---|---|---|---|
| `SkciInitialize` | Lifecycle | Yes | ABI magic `0x0A000012` |
| `SkciCreateSecureImage` | Image | Yes | Allocates `0x188`-byte context |
| `SkciValidateImageData` | Image | Yes | Streaming input |
| `SkciFinalizeSecureImageHash` | Image | Yes | Hash finalization |
| `SkciFinishImageValidation` | Image | Yes | Policy + page-use convergence |
| `SkciFreeImageContext` | Image | Yes | Teardown |
| `SkciValidateDynamicCodePages` | Dynamic | Yes | Success = `0x12C`, algorithm `0x800E` |
| `SkciDetermineAuthorizedPageUse` | Policy | Yes | Maps trust chain → page-use flags |
| `SkciQueryImageUniqueID` | Query | Yes | Reads `ctx+0x148`, guarded by `ctx+0x118 & 0x08` |
| `SkciQueryImageAuthorID` | Query | Yes | Reads `ctx+0x168`, guarded by `ctx+0x118 & 0x08` |
| `SkciQueryImageAttestationData` | Query | Yes | `STATUS_OBJECT_NAME_NOT_FOUND` if `ctx+0x11C == 0` |
| `SkciQueryEnclavePackageID` | Query | Yes | Authorized = all-zero output |
| `SkciCalculateEnclaveIdentities` | Compute | Yes | Calls `CiCalculateUniqueID` and `CiCalculateAuthorID` |
| `SkciQueryInformation` | Query | Yes | Only classes `0x67` and `0xF1` |
| `SkciSetCodeIntegrityPolicy` | Policy | Yes | Modes 0/1/2 only; mode 2 requires exactly 5 bytes |
| `SkciCreateCodeCatalog` | Catalog | Yes | — |
| `SkciDeleteCatalog` | Catalog | Yes | — |
| `SkciValidateCertChain` | Cert | Yes | — |
| `SkciValidateAmeCertChain` | Cert | Yes | AME-specific chain |
| `SkciMatchHotPatch` | Hotpatch | Yes | Algorithms `0x8004` / `0x800C` |
| `SkciBuildAttestationReport` | Attestation | Yes | BugCheck `0x29` on zero history |
| `CipForceImageRevalidation` | Internal | Yes | Two instructions: `bts [g_CiPolicyState], 7; ret` |

---

## Appendix B: Key Global Variables

| Variable | Role |
|---|---|
| `g_CiOptions` | CI enforcement mode bits. Class `0x67` exposes 5 derived bits |
| `g_CiDeveloperMode` | Developer/flight/weak-crypto mode flags. Bit `0x40` exposed through class `0x67` |
| `g_CiPolicyState` | Runtime policy and revalidation state. Bit 7 = revalidation pending |
| `g_CipPolicyLock` | Push lock protecting policy state reads/writes |
| `g_CiScenarios` | Attestation scenario history array. `+0x3B0` = history count |
| `g_SiPolicyHash` | SHA-256 hash of the current active SI policy state |
| `g_CiBlocklistUpdateTime` | Timestamp of last blocklist update, included in attestation report |
| `g_EnclaveEncodedOid` | Encoded OID for enclave trust path in `CiIsSignatureTrustedForVsmEnclaves` |

---

## Appendix C: `authorized_page_use_flags` Reference

| Bit | Hex | Analytical name | Grant condition |
|---|---|---|---|
| 0 | `0x01` | `IUM_execute` | IUM trust confirmed, or `input_flags & 5`, or CI global option + image flag |
| 1 | `0x02` | `IUM_trusted` | `CiIsSignatureTrustedForIum` full confirmation |
| 2 | `0x04` | `WHQL_level` | `signing_level >= 0x0C` OR EKU `1.3.6.1.4.1.311.76.8.1` at level `0x08..0x0B` |
| 3 | `0x08` | `enclave_trusted` | `CiIsSignatureTrustedForVsmEnclaves` returns true |
| 4 | `0x10` | `whql_bio` | `CiIsSignatureWhqlBio` returns true |
| 5–9 | `0x20..0x1E0` | `signing_level_field` | `(signing_level & 0x0F) << 5` — always recorded |
| 10+ | — | *(unobserved)* | Not seen in this build |

Functions that gate on specific bits:

| Bit tested | Functions that gate on it |
|---|---|
| `0x08` (enclave\_trusted) | `SkciQueryImageUniqueID`, `SkciQueryImageAuthorID`, `SkciQueryEnclavePackageID` |
| Any | `SkciDetermineAuthorizedPageUse` (writer) |

---

---

## Appendix D: The Page Hash Engine in Detail

The page hash model is central to how SKCI enforces executable page authorization, so it deserves a deeper treatment than the high-level description in §8.2.

### D.0 Why page hashes, not just file hashes?

A file hash covers the entire PE image at rest. A page hash covers each 4KB page individually. The distinction matters for kernel images because the kernel's executable section and its data section are mapped into different virtual ranges at different times. A file hash cannot detect a scenario where the file is correct but a specific page has been modified after mapping. Page hashes close this gap: each 4KB chunk of the executable is independently authenticated.

The tradeoff: page hash records are larger (one 36-byte record per page, vs one 32-byte hash per file) and require pre-computation or catalog storage. On a kernel image with a 512KB `.text` section, that is 128 page hash records just for that section. This is why SKCI's page hash context includes a binary search (`bsearch`) over records sorted by file offset — linear scan would be too slow for large images.

### D.0.1 Two page-hash paths

SKCI handles two distinct scenarios for page hashes:

| Path | How records are obtained | Typical user |
|---|---|---|
| **Catalog / embedded signature** | PE Authenticode signature contains a page hash table, or a catalog file provides one | System components, WHQL-signed drivers |
| **Generated page hashes** | SKCI computes page hashes at load time from the PE sections | JIT-compiled code, dynamically generated images |

The generated-hash path is the `SkciValidateDynamicCodePages` path (§10). The catalog/signature path is the `SkciFinishImageValidation` path via `CiValidateImagePages`. Both paths ultimately validate a SHA-256 hash per 4KB page. They differ in where the expected hashes come from.

### D.0.2 Binary search and the sort requirement

`CiValidateImagePages` uses binary search over the page-hash record array. This requires the array to be sorted by file offset. The sort happens during context creation:

```
CiCreateVerificationContextForImageGeneratedPageHashes:
    → allocate record array (n * 0x24 bytes)
    → for each section in section table (sorted by VirtualAddress):
        → for each 4KB page within section:
            → compute file offset
            → compute SHA-256(page_bytes)
            → store (file_offset, sha256) as 0x24-byte record
    → sort record array by file_offset (ascending)
```

The sort is a prerequisite for the validation loop. If records were stored in section order but sections are not contiguous in the file (which can happen), a naive linear scan would not find every page. The sort normalizes the storage.

### D.0.3 Integer overflow protection

The record count computation explicitly guards against overflow:

```asm
; Somewhere in the context allocation path:
imul  rax, rcx, 24h      ; rax = record_count * sizeof(record)
jo    overflow_handler    ; jump if overflow flag set
; Continue with allocation...

overflow_handler:
    mov  eax, 0C0000095h  ; STATUS_INTEGER_OVERFLOW
    ret
```

`STATUS_INTEGER_OVERFLOW` (`0xC0000095`) means the caller supplied a page count so large that the record buffer size wrapped around in 64-bit arithmetic. This is a defense-in-depth check: a malformed image header with an impossibly large section count would cause this status before any allocation is attempted.

---

## Appendix E: `SkciQueryImageAttestationData` — Attestation Data Retrieval

`SkciQueryImageAttestationData` reads the image-level attestation fields from the context and copies them to the caller's output buffer. It is separate from the policy-level attestation in `SkciBuildAttestationReport`.

The function follows a pattern used throughout the Query\* exports:

```
SkciQueryImageAttestationData(context, output_buffer, output_size, return_size):
    if context->attestation_thumbprint_algorithm == 0:   // ctx+0x11C
        return STATUS_OBJECT_NAME_NOT_FOUND (0xC0000225)

    required_size = sizeof(thumbprint_struct) + sizeof(program_name_struct)
    if output_size < required_size:
        *return_size = required_size
        return STATUS_INFO_LENGTH_MISMATCH (0xC0000004)

    copy context+0x11C (algorithm) → output
    copy context+0x120..0x137 (thumbprint bytes) → output
    copy context+0x138 (program_name UNICODE_STRING, if present) → output
    *return_size = actual_copied_size
    return STATUS_SUCCESS
```

The check `ctx+0x11C == 0` is the "no attestation data" condition. This field is set to `0x8004` by `SkciCalculateImageAttestationData` when a certificate thumbprint is found. If it remains 0 (the initial value), no attestation data was computed — either because the image has no Authenticode signature, or because the signature does not contain an OPUS attribute, or because the certificate thumbprint extraction failed.

`STATUS_OBJECT_NAME_NOT_FOUND` (`0xC0000225`) is the return code for "no data present." This is the right status semantically — the attestation data object does not exist for this image — but it is easy to misinterpret as a generic lookup failure. A dedicated status code like `STATUS_NO_ATTESTATION_DATA` would be clearer; this is one of the minor diagnostic gaps noted in §20.

### The OPUS attribute

`SkciCalculateImageAttestationData` looks for an OPUS (Authenticode Program Name) attribute in the signer's authenticated attributes:

```
SkciCalculateImageAttestationData(context, signing_info):
    1. Clear ctx+0x11C through ctx+0x137 (zero attestation fields)
    2. Call SkciGetCertificateThumbprint(signing_info, hash_alg, thumbprint)
       - on success: set ctx+0x11C = 0x8004 (algorithm), ctx+0x120..0x137 = thumbprint
    3. Check for OPUS attribute in authenticated attributes
       - if OPUS present and not a "generic" program name:
           set ctx+0x138 = UNICODE_STRING pointing to OPUS program name
```

Step 1 — zero the fields first — is a correctness invariant: if either step 2 or step 3 fails partway, the context fields are in a deterministic "not present" state rather than whatever garbage the allocator left there.

---

## Appendix F: Signing Levels Reference

`SkciDetermineAuthorizedPageUse` references signing level comparisons against `0x08` and `0x0C`. For context, the known signing level constants used across `ci.dll` and `skci.dll`:

| Level (hex) | Level (dec) | Name / Meaning |
|---|---|---|
| `0x00` | 0 | Unsigned |
| `0x01` | 1 | Unsigned (but scan authorised via policy) |
| `0x02` | 2 | Enterprise-signed (WDAC policy) |
| `0x03` | 3 | Custom-signed (developer mode) |
| `0x04` | 4 | Authenticode / catalog-signed |
| `0x05` | 5 | Microsoft Store app |
| `0x06` | 6 | Antimalware signed |
| `0x07` | 7 | Microsoft signed (non-OS critical) |
| `0x08` | 8 | WHQL submission level |
| `0x09` | 9 | Windows (standard) |
| `0x0A` | 10 | Windows TCB (Trusted Computer Base) |
| `0x0B` | 11 | Windows component |
| `0x0C` | 12 | WHQL (hardware-qualified level) |
| `0x0D` | 13 | Windows protected |
| `0x0E` | 14 | Windows system protected |
| `0x0F` | 15 | IUM (Isolated User Mode) trusted |

The two SKCI thresholds:

- **`0x08` (WHQL submission):** Lower bound for the EKU fallback. Levels `0x08..0x0B` can gain `WHQL_level` trust via EKU `1.3.6.1.4.1.311.76.8.1`.
- **`0x0C` (WHQL):** Unconditional `WHQL_level` grant when bit 0 (`IUM_execute`) is already set.

Levels below `0x08` cannot obtain `WHQL_level` regardless of EKU. This is a floor: a standard Authenticode signature (level `0x04`) can never reach `WHQL_level` trust through any EKU path.

The IUM trust predicates (`CiIsSignatureTrustedForIum`, `CiIsSignatureTrustedForVsmEnclaves`) operate separately from signing level. An image can have a high signing level but still fail `CiIsSignatureTrustedForIum` if its certificate is not on the IUM trust list. Signing level and IUM trust are independent dimensions of authorization; the `authorized_page_use_flags` bitmap encodes both.

---

## Appendix G: Comparing VTL0 `ci.dll` and VTL1 `skci.dll`

For readers who know the VTL0 Code Integrity stack and want to understand the relationship:

| Feature | VTL0 `ci.dll` | VTL1 `skci.dll` |
|---|---|---|
| Location | `C:\Windows\System32\ci.dll` | `C:\Windows\System32\skci.dll` |
| Loader | `ntoskrnl.exe` | `securekernel.exe` |
| Can be patched by VTL0 kernel code? | In principle (if HVCI off); no if HVCI on | No — VTL1 memory is inaccessible from VTL0 |
| Page hash implementation | Similar record format, also SHA-256 | Same record format (0x24 bytes) |
| Algorithm constants | `0x800C` for image pages | `0x800C` for image, `0x800E` for dynamic |
| EKU path | Present (same OIDs) | Present (same OIDs, same resolution) |
| Attestation | No policy-level attestation export | `SkciBuildAttestationReport` |
| Policy update | `CiSetPolicy`-style in-place | `SkciSetCodeIntegrityPolicy` with generation |
| Hot module bypass risk | Depends on HVCI state | VTL boundary materially changes the attack surface; VTL0 patching assumptions no longer apply |
| Public PDB | Yes (usually) | Yes (this build) |

The algorithms and data structures are deliberately consistent across VTL0 and VTL1. This makes sense: both implement the same fundamental operation (page hash validation and signing level evaluation). What VTL1 adds is that the enforcement cannot be weakened from VTL0, even by SYSTEM kernel code. The hypervisor enforces the VTL boundary; SKCI's authorization decisions are binding.

The implication for security research: a technique that works against `ci.dll` in a system without HVCI does not automatically work against `skci.dll` in a HVCI-enabled system. The code paths are similar; the trust boundary is not.

---

## Appendix H: Deeper Dives — Selected Function Walkthroughs

### H.1 `SkciCreateSecureImage` — Phase by Phase

The complete sequence of what `SkciCreateSecureImage` does, in order, derived from tracing the call graph in CDB:

**Phase 1 — Header validation:**

```asm
; After allocating the context block (0x188 bytes, pool tag 0x43495349 = "ISCI"):
call  CiValidateImageHeaderMapping
; verifies the supplied PE header pointer is within the image mapping
; and that the NT signature ("MZ" + PE\0\0) is present and correctly positioned.
; Failure: STATUS_INVALID_IMAGE_FORMAT

call  SkciCheckNtHeaderForCompliance
; additional checks on the optional header fields:
;   - SizeOfImage must be non-zero and page-aligned
;   - SectionAlignment must be page-aligned (>= 0x1000)
;   - NumberOfSections <= maximum allowed
;   - Subsystem must be in the allowed set for kernel images
; Failure: STATUS_INVALID_IMAGE_FORMAT or STATUS_INVALID_PARAMETER
```

**Phase 2 — First page hash:**

```asm
call  CiCalculateFirstPageHash
; computes SHA-256 over the first 0x1000 bytes of the image
; stores result at ctx+0x010
; the "first page" includes the DOS/MZ header, PE header, and optional header
; this hash is a quick identity anchor before full page coverage is computed
```

**Phase 3 — Section table processing (conditional):**

The function checks a flags argument to decide whether to generate page hashes or defer to the signed/catalog path:

```
if (caller_flags & USE_GENERATED_PAGE_HASHES):
    call CiCreateVerificationContextForImageGeneratedPageHashes
    ; allocates a verification context for generated (computed) page hashes
    ; iterates over the section table, sorts sections by file offset
    ; for each section: schedules SHA-256 computation over each 4KB page
    call CiAddSectionDataToVerificationContext
    ; accumulates section data into the verification context
else:
    ; defer: image uses signed catalog page hashes
    ; page hash records will come from the catalog/signature infrastructure
    ; validated later in SkciFinishImageValidation via CiValidateImagePages
```

The two paths produce different intermediate state but converge at `SkciFinishImageValidation`. The generated-page-hash path computes hashes eagerly; the catalog path validates lazily.

**Phase 4 — Optional image copy:**

```asm
; if caller requests a rebased/copied image:
call  RtlCopyMemory       ; copy header to ctx+0x0E0 region
; stores NT headers pointer at ctx+0x0E8
; this enables subsequent validation against a canonical copy,
; important for images that may be relocated at load time
```

### H.2 `CiIsSignatureTrustedForVsmEnclaves` — The Enclave Trust Gate

This function controls whether `enclave_trusted` (bit 3 of `authorized_page_use_flags`) is set. It is more complex than the other trust predicates because enclave authorization layered on top of IUM authorization:

```
CiIsSignatureTrustedForVsmEnclaves(signing_info):
    1. Call CiIsSignatureTrustedForIum(signing_info)
       if false: return false
       // VSM enclave trust requires IUM trust as a prerequisite

    2. Check signing level via g_CipWhichLevelComparisons
       // signing level must satisfy the enclave-specific level requirement
       // the threshold is higher than for ordinary IUM trust

    3. Check EKU via MincryptIsEKUInPolicy
       // EKU lookup against the policy's allowed enclave EKU list

    4. Parse g_EnclaveEncodedOid via ASN.1 decoder
       // there is a dedicated OID/attribute path for enclave identification
       // CiIsEnclaveAttribute(cert_extension, oid) is the final gate
```

The four-step gate means a certificate can be trusted for IUM execution (bit 0/1) but not for VSM enclaves (bit 3) if either the signing level is insufficient or the enclave EKU/OID is absent. This is the correct layering: enclave trust is a subset of IUM trust, not a separate trust domain.

The `g_EnclaveEncodedOid` global contains the encoded form of the enclave attribute OID. It is populated during `SkciInitialize` (or `CiInitializeGlobalState`) and used as a reference for the ASN.1 comparison. I did not reconstruct the full OID string from the binary; it would require reading `g_EnclaveEncodedOid` from a live system or computing the DER encoding of a known enclave attribute OID.

### H.3 `CiGenerateAttestationReport` — Full Format Reconstruction

The complete report structure, as reconstructed from `CiGenerateAttestationReport` and `CipAddAttestationReportToHistory`:

```c
struct CiAttestationReport {
    uint8_t  version;              // 0x00: always 1
    uint8_t  reserved1[3];        // 0x01..0x03
    uint32_t header_size;          // 0x04: always 0x3A
    uint32_t type;                 // 0x08: 0x50013 (observed, no public name)
    uint32_t reserved2;            // 0x0C: padding
    uint64_t blocklist_update_time;// 0x10: g_CiBlocklistUpdateTime (8 bytes)
    uint8_t  hash_length;          // 0x18: 0x20 (SHA-256)
    uint8_t  hash_algorithm;       // 0x19: 0x0B (observed, no public name)
    uint8_t  revocation_list_hash[32]; // 0x1A: SHA-256 of current revocation list
    // [0x3A..] serialized policy attestation payload from SIPolicyGetSerializedPolicies
};
```

Pool tag `0x74614943` — little-endian ASCII "CiAt" — is used for the allocation. This is also the tag that appears in `SkciBuildAttestationReport`'s BugCheck parameter P2.

The `g_CiBlocklistUpdateTime` field at offset `0x10` is particularly interesting for forensics: it records when the blocklist was last updated, not when the report was generated. An attestation report from a system that has been isolated from Windows Update for months will have an old `blocklist_update_time` even if the report was just generated. This is useful for detecting stale protection configurations.

The `type` field `0x50013` and the `hash_algorithm` byte `0x0B` remain unresolved to public names. I am documenting them as observed literals rather than assigning analytical names, because any naming would be speculation.

### H.4 `SkciMatchHotPatch` — Caller-Bound Identity Comparator

The full hotpatch authorization sequence belongs to `securekernel!SkmiMatchPatchImage`. `SkciMatchHotPatch` is its final step — a narrow identity comparator. The caller-side checks that precede it:

flowchart TD A[SkmiMatchPatchImage] --> B{record +0x60 == 0?} B -->|no| X[no match] B -->|yes| C[RtlFindHotPatchBaseMachine] C -->|null| X C --> D{base metadata matches\nat +0x20 / +0x24?} D -->|no| X D -->|yes| E{record hash pointer\nat +0x30 present?} E -->|no| M[match after metadata checks] E -->|yes| F[RtlGetHotPatchHashes] F -->|null| X F --> G[SkciMatchHotPatch] G -->|memcmp == 0| M G -->|memcmp != 0| X 

`SkciMatchHotPatch` is called only when a non-null hash pointer exists. The metadata checks in steps 1–4 are securekernel work. SKCI is the hash oracle at the end of the chain, not the access-control decision maker.

The hotpatch matching design in SKCI embodies a specific philosophy about trust: the identity of a hotpatch is not "who signed it" but "does the CI policy know this target, and does the patch's identity match what the policy recorded." The signing chain matters for authorization, but the key mechanism is content-based identity.

```asm
skci!SkciMatchHotPatch:
    ; ecx = algorithm_id (DWORD)
    ; edx = hash_len (DWORD)
    ; r8  = input_hash (byte*)
    ; r9  = hotpatch_hash_blob*

    cmp  ecx, 8004h       ; 20-byte thumbprint-style path
    jne  try_800C

    cmp  edx, 14h         ; expected length: 20 bytes
    jne  no_match

    ; compare input_hash[0..19] with hotpatch_hash_blob + 0x20
    call skci!memcmp
    jmp  result

try_800C:
    cmp  ecx, 800Ch       ; 32-byte hash path
    jne  no_match
    cmp  edx, 20h         ; expected length: 32 bytes
    jne  no_match

    ; compare input_hash[0..31] with hotpatch_hash_blob + 0x00
    call skci!memcmp
    jmp  result

no_match:
    xor  eax, eax         ; return false
    ret
result:
    ; eax = comparison result (0 or 1)
    ret
```

The comparison uses `skci!memcmp` — a direct fixed-length byte compare. No constant-time property is established from this static trace; no timing-safe wrapper function appears in the disassembly.

The hotpatch blob format, partially reconstructed:

| Blob offset | Size | Content |
|---|---|---|
| `0x00` | 32 | SHA-256 identity hash (read by algorithm `0x800C` path) |
| `0x08` | 4 | Sequence number (read by `CiHotpatchGetSequenceNumberFromHotpatchInfo`) |
| `0x20` | 20 | Thumbprint-style hash (read by algorithm `0x8004` path) |
| `0x0C+` | Variable | Additional fields not yet reconstructed |

The minimum blob size enforced by `CiHotpatchGetSequenceNumberFromHotpatchInfo` is `0x0C` — it reads a 4-byte sequence number from `+0x08` and checks that the blob is large enough to contain it.

The `NtManageHotPatch` call in `SIPolicyDoesHotpatchExistForTarget` uses class `0x0B` and a 20-byte output buffer. The class `0x0B` for `NtManageHotPatch` is not documented, but its presence in SKCI's internal implementation suggests it is the "query if hotpatch exists for target" operation — a policy check separate from the identity matching.

The full hotpatch authorization flow:

```
SIPolicyDoesHotpatchExistForTarget(target_info)
    → NtManageHotPatch(class=0x0B, target_info, output_buf, 0x14)
    → if hotpatch exists in policy:
        → SkciMatchHotPatch(hotpatch_blob, algorithm, hash_len, identity_hash)
        → if match: authorize
```

### H.5 `SkciQueryInformation` Class `0x67` — Protocol Dissection

The exact assembly for the class `0x67` protocol check deserves to be shown in full because the "pre-fill magic" requirement is non-obvious:

```asm
; esi = information_class = 0x67
class_67:
    ; [r12] = input_buffer_ptr, r13 = input_buffer_length
    ; [r14] = output_buffer_ptr, r15 = output_buffer_length
    ; [rbp-something] = return_length_ptr

    cmp   r13d, 8               ; input_buffer_length == 8 exactly?
    jne   length_mismatch

    cmp   r15d, 8               ; output_buffer_length == 8 exactly?
    jne   length_mismatch

    cmp   dword ptr [r12], 8    ; [input_buffer + 0] == 8 (magic pre-fill)?
    jne   length_mismatch

    ; All checks pass. Build the response DWORD at output+4:
    xor   ebx, ebx              ; response_bits = 0

    ; Build CI options bits:
    test  [g_CiOptions], 2      ; bit 1 of g_CiOptions?
    jz    skip_bit0
    or    ebx, 1                ; set response bit 0
    test  [g_CiOptions], 8      ; bit 3 of g_CiOptions?
    jz    skip_bit1
    or    ebx, 2                ; set response bit 1

skip_bit1:
skip_bit0:
    ; Developer mode bit:
    test  [g_CiDeveloperMode], 40h    ; bit 6 of g_CiDeveloperMode?
    jz    skip_bit9
    or    ebx, 200h                   ; set response bit 9

skip_bit9:
    ; WHQL enforcement bits:
    call  CipWhqlEnforcementEnabled
    test  al, al
    jz    skip_whql
    or    ebx, 4000h                  ; set response bit 14
    test  ah, ah
    jz    skip_whql
    or    ebx, 8000h                  ; set response bit 15

skip_whql:
    ; Write response:
    mov   dword ptr [r14], 8    ; output[0] = 8 (echo the magic)
    mov   [r14+4], ebx          ; output[4] = response bits

    ; Set return length:
    mov   dword ptr [return_length_ptr], 8
    xor   ebx, ebx              ; STATUS_SUCCESS
    jmp   exit
```

Two observations from this assembly:

1. **The magic echo.** Output offset `+0x00` receives the value `8` — the same magic the caller had to pre-fill in the input. This is a protocol handshake: the caller confirms it knows the protocol, the response confirms it was understood. If the caller pre-filled a different value, the check at `cmp dword ptr [r12], 8` would have failed with `STATUS_INFO_LENGTH_MISMATCH` before any output was written.
2. **`g_CiDeveloperMode & 0x40`.** This bit maps to what might be called "flight mode" or "test signing with weak crypto allowed" — the exact semantic is not named publicly. It is the bit that would indicate whether the system is in a development/test configuration with relaxed signing requirements. An enterprise system should have this bit clear; seeing it set in the class `0x67` output would be a finding.

### H.6 Policy Lock Consistency in `SkciQueryInformation`

Both classes hold `g_CipPolicyLock` as a shared (read) lock for the duration of the query. The lock acquisition/release sequence:

```
SkeEnterCriticalRegion
SkAcquirePushLockShared(g_CipPolicyLock)
[execute class-specific code]
SkReleasePushLockShared(g_CipPolicyLock)
SkeLeaveCriticalRegion
```

This is the same lock held exclusively (`SkAcquirePushLockExclusive`) during `SkciSetCodeIntegrityPolicy` updates. The shared/exclusive protocol ensures that a policy query never observes a partially-written policy state — it either sees the complete old state or the complete new state, never a torn mix. This is a correct implementation of a read-write lock around policy state.

The `SkeEnterCriticalRegion` call disables kernel APC delivery for the duration of the lock hold, which prevents a kernel APC from interrupting between lock acquisition and release and causing a potential deadlock if the APC handler also tries to acquire the same lock.

---

## Appendix I: Open Questions for Future Analysis

The findings above are bounded by static analysis. Several questions would require a live VTL1 trace to answer definitively:

| Question | What static analysis shows | What live trace would add |
|---|---|---|
| Exact downstream meaning of `0x12C` success from `SkciValidateDynamicCodePages` | Code generates `0x12C`; `securekernel.exe` accepts it with NT sign-bit semantics (`test eax,eax; js`) | Whether telemetry or any later consumer distinguishes `0x12C` from `STATUS_SUCCESS` |
| Runtime value of `g_CiOptions` on a live HVCI system | Bits 1, 3, and 21 are referenced in the code | Actual active bits on a production system |
| Consumer of `g_CiPolicyState` bit 7 in `securekernel.exe` | Not read within `skci.dll` static scope | Which securekernel function triggers revalidation on bit set |
| Full `g_CiScenarios` layout | Offsets `+0x390` through `+0x3B0` reconstructed | Complete structure including scenario array entries |
| `SkciQueryEnclavePackageID` payload on a real enclave load | Returns 40 zero bytes in static analysis | Whether a real enclave load produces non-zero output (if so, this analysis is wrong) |
| `g_EnclaveEncodedOid` content | Exists as a global, used in ASN.1 comparison | Decoded OID string |
| `NtManageHotPatch` class `0x0B` semantics | Called with 20-byte output buffer | Exact output structure and what "hotpatch exists for target" means in policy terms |
| `SkciSetCodeIntegrityPolicy` mode 2 payload | 4+1 bytes written to `g_CiScenarios+0x3B4/0x3B8` | What those 5 bytes encode in the CI scenario model |

These are not gaps in the analysis — they are the natural boundary of what static analysis can reach without a live VTL1 debugger. Documenting them explicitly is part of being honest about the scope and confidence of what is reported here.

---

## Appendix J: Why "Secure Kernel" and What VTL Means for This Analysis

For readers who are less familiar with the VBS/VTL architecture: a brief note on why `skci.dll` running in VTL1 matters for the analysis methodology.

Windows with Virtualization-Based Security (VBS) enabled runs in two Virtual Trust Levels:

| VTL | Name | Runs | Trust level |
|---|---|---|---|
| 0 | "Normal world" | NT kernel (`ntoskrnl.exe`), user-mode processes, drivers | Less trusted |
| 1 | "Secure world" | Secure Kernel (`securekernel.exe`), SKCI (`skci.dll`) | More trusted |

The hypervisor enforces the boundary. VTL0 cannot read or write VTL1 memory. VTL0 kernel code — including any driver, even a kernel-mode driver running as SYSTEM — cannot call into `skci.dll` directly. All calls cross the VTL boundary through a controlled VMCALL interface.

For this analysis, the practical consequences are:

1. **Static analysis is possible** — `skci.dll` is a standard PE image that CDB can disassemble with its public PDB.
2. **Live single-stepping is not possible** — a user-attached debugger runs in VTL0 and cannot single-step VTL1 code.
3. **Memory inspection of VTL1 globals is not possible** — reading `g_CiOptions` at runtime would require a VTL1-aware debugger or a hypervisor trace.
4. **The import/export surface is the primary interface evidence** — what `securekernel.exe` imports from `skci.dll` is the ground truth for what capabilities are actually used.

This means the analysis in this document is necessarily "what would happen" rather than "what did happen." The code paths are real; the actual runtime values and timing of those paths on a live system are inferred.

---

## Appendix K: Connecting to the ForceIO Research

The [ForceIO write-up](https://kvc.pl/research/forceio-minifilter-bypass/) established that `WdFilter.sys` (a VTL0 minifilter) can be bypassed by routing file operations through a legitimately signed kernel driver that issues I/O below the filter. That bypass operates entirely in VTL0.

The question this research addresses is the natural follow-on: **does the VTL1 code-integrity machinery (`skci.dll`) provide a backstop that VTL0 bypasses cannot circumvent?**

The honest answer, based on this analysis: **yes, for code execution; not directly relevant for file operations.**

`skci.dll` controls whether a specific image is authorized to execute as kernel-mode code. It does not control file I/O. The ForceIO bypass is about file operations — specifically, moving and deleting files that `WdFilter.sys` would otherwise block. SKCI is not in that code path.

Where SKCI becomes relevant is in a more sophisticated scenario: if an attacker wants to load a kernel driver that has not been validated by the signing/policy infrastructure, HVCI (enforced by SKCI) prevents that driver's pages from becoming executable. The ForceIO approach works specifically because `IObitUnlocker.sys` is a legitimately signed driver — it passes the signing check, gets through SKCI validation, and executes normally. An unsigned driver or a driver with a revoked certificate would fail at `SkciFinishImageValidation` before any pages are authorized.

So the two pieces of research form a natural pair:

| Layer | Controlled by | ForceIO relevant? | This research relevant? |
|---|---|---|---|
| File I/O | `WdFilter.sys` (VTL0 minifilter) | Yes — ForceIO bypasses this | No |
| Code execution authorization | `skci.dll` (VTL1 SKCI) | Partially — ForceIO uses SKCI-authorized driver | Yes |

The boundary is also the interesting part: BYOVD/BYOPD attacks work specifically because they stay within the VTL1 authorization model. The attacker uses a signed driver, which passes SKCI validation, which authorizes its pages for execution, which gives the attacker kernel-mode capabilities within VTL0. Closing this attack vector requires either revoking the driver (blocklist) or requiring a policy that permits only specific named drivers (WDAC application control). Neither of those is a change to `skci.dll` itself — they are policy changes enforced through `skci.dll`.

---

*This analysis was performed on a personal research system running Windows 11 Pro 26H1, build 28000, using CDB with public PDB from the Microsoft symbol server. The findings are static and reflect the binary as shipped in June 2026. Runtime behavior in VTL1 may differ from what static analysis predicts. If you find errors or gaps, please reach out.*

### Add a comment

---

## Navigation

- Parent: [Research](https://kvc.pl/research.md)
- Previous: [ForceIO — Reverse Engineering IObitUnlocker.sys for Minifilter-Bypass File Operations](https://kvc.pl/research/forceio-minifilter-bypass.md)
- Next: [EfiTool — UEFI Bootkit Achieving NT AUTHORITY\SYSTEM via In-RAM Registry Patching](https://kvc.pl/research/efitool-uefi-registry-patch.md)
