Add reusable certificate and file validation helpers

Fix process-name ownership and module lookup outputs, add explicit SPKI hashing and bounded chain access, and improve WDM integration. Deprecate superseded entry points with migration guidance and align branch code and tests with the library style.

Add CI validation helpers with explicit signature selection and owned results

Document runtime-confirmed CI signer selection and crypto reuse

Expand CI compatibility matrix to twenty verified samples

Extend CI export matrix with Windows 11 x64 and ARM64 samples

Document CI API availability and historical mincrypt layouts

Add explicit Authenticode workflows and correct CI helper contracts

Document CI validation flag 0x20 and add kernel probe

Harden CI digest parsing and failure cleanup
This commit is contained in:
nefarius committed 2026-10-27 00:17:25 +01:00
1 parent 7985b23be5
commit 3fbfe31d15
38 files changed
+8979 -529

No files matched your search

+3 -2
View File
@@ -4,14 +4,15 @@
<PropertyGroup Label="UserMacros" /> <PropertyGroup Label="UserMacros" />
<PropertyGroup> <PropertyGroup>
<_PropertySheetDisplayName>Domito Kernel Library</_PropertySheetDisplayName> <_PropertySheetDisplayName>Domito Kernel Library</_PropertySheetDisplayName>
<DomitoRootPath Condition="'$(DomitoRootPath)' == ''">$(SolutionDir)..\Domito</DomitoRootPath> <DomitoRootPath Condition="'$(DomitoRootPath)' == ''">$(MSBuildThisFileDirectory)</DomitoRootPath>
<DomitoLibraryPlatform Condition="'$(DomitoLibraryPlatform)' == ''">$(Platform)</DomitoLibraryPlatform>
</PropertyGroup> </PropertyGroup>
<ItemDefinitionGroup> <ItemDefinitionGroup>
<ClCompile> <ClCompile>
<AdditionalIncludeDirectories>$(DomitoRootPath)\include;$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>$(DomitoRootPath)\include;$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
<Link> <Link>
<AdditionalDependencies Condition="'$(DriverType)'=='KMDF'">$(DomitoRootPath)\lib\$(DDKPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DDKPlatform)\ci.lib;$(DDK_LIB_PATH)cng.lib;%(AdditionalDependencies);</AdditionalDependencies> <AdditionalDependencies Condition="'$(ConfigurationType)' != 'StaticLibrary' And ('$(DriverType)' == 'KMDF' Or '$(DriverType)' == 'WDM')">$(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\ci.lib;cng.lib;%(AdditionalDependencies);</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup /> <ItemGroup />
+72 -12
View File
@@ -1,16 +1,76 @@
# CI APIs # CI API availability observations
## Available starting with 1511 See the [CI API compatibility matrix](docs/ci-api-compatibility.md) for the
per-API tables covering 20 VM, ISO and modern x64/ARM64 samples, with exact
versions, file hashes and machine-readable results. The six-sample notes below
describe the original VM corpus and its subsequent ISO follow-up.
- CiCheckSignedFile These are export-table observations from six extracted x64 `ci.dll` files, not
- CiFindPageHashesInCatalog claims about every servicing revision or a proof of the original introduction
- CiFindPageHashesInSignedFile build. The companion CodeIntegrity-RE `corpus-index.json` records identities,
- CiFreePolicyInfo extraction aliases, manifest checks, and metadata. VM directory names are labels;
- CiGetPEInformation versions below come from the PE version resources. No target was executed.
- CiInitialize
- CiValidateFileObject
- CiVerifyHashInCatalog
## Added in 1709 ## Named exports common to all six samples
- CiSetTrustedOriginClaimId - `CiCheckSignedFile`
- `CiFindPageHashesInCatalog`
- `CiFindPageHashesInSignedFile`
- `CiFreePolicyInfo`
- `CiGetPEInformation`
- `CiInitialize`
- `CiValidateFileObject`
- `CiVerifyHashInCatalog`
`CiValidateFileObject` is already present in sampled Windows 10 **1507**
`10.0.10240.16463`. The old “starting with 1511” list was therefore too narrow.
Export presence does not establish a complete compatible parameter/structure
contract; consult the per-build static analysis before using it.
## Additional names and absent names
`CiSetTrustedOriginClaimId` is present in sampled `10.0.15063.0` (**1703**) and
in the sampled 1709/1803 files. It is absent from the sampled 1507,1511,1607
export tables. This corrects the old “added in 1709” note, without claiming the
first Windows build on which the API ever existed.
`CiGetCertPublisherName` is absent from **all six** sampled 1507–1803 export
tables. The ISO follow-up also finds it absent in x64 `10.0.19041.1`, present
in x64 `10.0.19041.1865` and `10.0.22000.318`, and the original investigation
finds it present in `10.0.26100.8328`. This brackets Windows 10 servicing
observations without identifying the first update adding the export. The
CodeIntegrity-RE `iso-corpus-index.json` records the exact files and hashes.
The Windows 10 ARM64 21H2 sample additionally exports it in CI
`10.0.19041.1566`; this does not establish x64 availability at that revision.
The matrix keeps architecture-specific observations separate.
A header declaration,
import-library symbol, or `NTDDI_VERSION >= NTDDI_WIN10` alone does not make an
unconditional static import loadable on the older samples. Callers supporting
those samples must avoid that import or use optional resolution with an
explicit unavailable/fallback path. `CiSetTrustedOriginClaimId` likewise cannot
be assumed available on every Windows 10 build; it is a CI-global setter.
The original modern x64 sample has display `FileVersion` **26100.8328**, but
its numeric fixed version is **26100.9444**. The matrix records both under the
same SHA-256 identity and uses numeric fixed versions in its tables.
## Exact sampled files
- Windows 10 1507: `10.0.10240.16463` x64, SHA-256
`e83d900f4675c6e6ffc3fb7dcd3074610876a1398e495796769988df810fd3bc`.
- Windows 10 1511: `10.0.10586.0` x64, SHA-256
`f949bfa614addea6174b28c3818c56b327c9290fd41bd91dfc545414b3df6783`.
- Windows 10 1607: `10.0.14393.2189` x64, SHA-256
`ef0c02ecbd0818e84be9f61421f919825f96a3c59199460cab08548eee2881df`.
- Windows 10 1703: `10.0.15063.0` x64, SHA-256
`3705aaf408f05326e028621380aa5c96a1f3583a2de8a139b188da4b82577863`.
- Windows 10 1709: `10.0.16299.125` x64, SHA-256
`1db7ae5a08bd2361fc7fcdd373fb980e8d680035e67b02e5967095db0443effd`.
- Windows 10 1803: `10.0.17134.1130` x64, SHA-256
`8ce3b8fe38c23b699f30b045383640019a429d3a9d513285c655bffe03a796d9`.
Matching Microsoft public PDB GUIDs/ages were verified in
`historical/ci-<build>-symbols.json` in CodeIntegrity-RE. Public names help map
functions; they are not a full typed SDK. The default Domito declarations remain
conservative. See [current contracts and limits](docs/mincrypt-contracts.md) and
[implementation status](docs/implementation-status.md) for ABI and runtime work.
+30 -2
View File
@@ -24,12 +24,28 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
- I do not want to force any consumer of the library to drag C++ paradigms into their project. I do expect the user to utilize a modern compiler though, so the library sources themselves may have some 'C++-ish touches' here and there, for my own convenience 😉 - I do not want to force any consumer of the library to drag C++ paradigms into their project. I do expect the user to utilize a modern compiler though, so the library sources themselves may have some 'C++-ish touches' here and there, for my own convenience 😉
- Compatibility with every Windows 10 version. - Compatibility with every Windows 10 version.
- APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞 - APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞
- **Caution:** this claim comes with an asterisk though; due to PatchGuard (or some other security mechanism) I couldn't get run-time dynamic linking for `ci.dll` exports to work, so you need to actively avoid implementing code depending on exports that do not exist on earlier versions of Windows. Once I have a compatibility matrix for all of them I'll update the documentation accordingly. - CI entry points are currently static imports from `ci.lib`; consumers must avoid imports unavailable on their target Windows builds. The [CI API compatibility matrix](docs/ci-api-compatibility.md) records all ten named exports across 20 exact Windows 10/11 x64 and ARM64 CI files. Export lookup uses the correct name-ordinal mapping and rejects forwarders. Runtime ABI verification remains separate; see the remaining-work ledger below.
- No conflicts with WDF or DMF - No conflicts with WDF or DMF
- The consuming driver *may* (but doesn't have to) utilize Microsoft WDF or DMF in addition without having to fear any incompatibilities 💪 - The consuming driver *may* (but doesn't have to) utilize Microsoft WDF or DMF in addition without having to fear any incompatibilities 💪
- Reliable SAL annotations. - Reliable SAL annotations.
- I made sure to enrich the majority of the code with correct, tested annotations for Code Analysis to help you spot potential accidental API misuse 😎 - I made sure to enrich the majority of the code with correct, tested annotations for Code Analysis to help you spot potential accidental API misuse 😎
## Authenticode validation helpers
The [CI validation helpers](docs/ci-validation-helpers.md) provide shared result
ownership/cleanup, file-object validation with explicit selection policy, and
algorithm-explicit embedded verification using CI. First-success selection is
an opt-in x64 experiment; existing APIs retain their behavior. Always enforce
the expected signer/trust policy separately from a successful CI return status.
## Consumer helpers
[Migration and contracts](docs/consumer-helpers.md) cover file-handle/path CI
validation, bounded x64 chain views, optional publisher-name extraction,
certificate/SPKI hashing (including explicit legacy content-only pins), strict
hex conversion, process-name cleanup and combined module/export lookup.
Application authorization and trust policies remain caller-provided.
## How to use ## How to use
- Add the `include` directory to your project's headers search path. - Add the `include` directory to your project's headers search path.
@@ -55,7 +71,11 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
### Using the property sheet ### Using the property sheet
Copy the provided `Domito.props` file into the directory of your `.vcxproj` file and customize it like below: Import `Domito.props` from the Domito package, or set `DomitoRootPath` explicitly
if copying the sheet elsewhere. Its default root is its own directory. The sheet
links both WDM and KMDF final drivers; static-library consumers must also import
it into the final driver project. `DomitoLibraryPlatform` defaults to `$(Platform)`.
For example:
```xml ```xml
<!-- The rest of the project file --> <!-- The rest of the project file -->
@@ -71,6 +91,14 @@ Copy the provided `Domito.props` file into the directory of your `.vcxproj` file
<!-- The rest of the project file --> <!-- The rest of the project file -->
``` ```
## Code Integrity API notes
- [Algorithm-explicit Authenticode hashing and legacy catalog policy](docs/authenticode-digests.md)
- [Mincrypt contracts, lifetimes, flags and opt-in x64 26100 layouts](docs/mincrypt-contracts.md)
- [Implemented corrections and remaining VM/binary work](docs/implementation-status.md)
- [CiValidateFileObject flag 0x20: evidence, limitations, and kernel test procedure](docs/ci-validate-file-object.md)
## Sources & 3rd party credits ## Sources & 3rd party credits
This library benefits from these awesome projects ❤ (appearance in no special order): This library benefits from these awesome projects ❤ (appearance in no special order):
+142
View File
@@ -0,0 +1,142 @@
# Authenticode digests and legacy verification
Prefer `DomitoCalculatePortableExecutableDigestEx` when computing a PE digest.
It takes an explicit SHA-1/256/384/512 algorithm, validates its supported PE layout,
and returns a digest independently of whether the file has an embedded signature.
This allows SHA-256 catalog lookup for a file without an embedded certificate.
Hash calculation is separate from signature verification and signer acceptance.
The older `DomitoCalculatePortableExecutableDigest` and
`DomitoGetPortableExecutableDigestKind` retain their compatibility behavior:
linear byte hashing and heuristic pattern discovery with a SHA-1 fallback. They
remain bounded, but should not be used to discover a general signature policy.
The new API is additive so existing callers do not silently acquire different
selection or rejection rules.
## Supported PE layout
The explicit API validates DOS/NT/optional-header spans, PE32 and PE32+ magic,
declared data-directory count, the complete section table, `SizeOfHeaders`, every
nonempty section's raw interval, and the certificate table. It sorts section
descriptors by raw file offset before validating their sequence. At most 96
sections are accepted, keeping stack storage and sorting work bounded.
Nonempty sections must form a contiguous sequence starting at `SizeOfHeaders`.
Section-header order can differ from raw-file order. Zero-raw-size sections
contribute no bytes. An overlay between the raw sections and the certificate
table, or EOF for an unsigned file, is included. The checksum and physical
certificate-directory slot are excluded. A present certificate table must be
8-byte aligned and end at EOF; every entry must have a bounded length, version
`0x200`, type PKCS#7 SignedData, and complete 8-byte alignment storage.
For this subset, sorted section bytes coincide with linear file order, so the
implementation can stream validated ranges directly to BCrypt without copying
or allocating a whole-file scratch buffer. Digest allocation is the only pool
allocation in the hashing path and is released on every failure.
Gaps, overlapping raw sections, certificates in the middle of a file, and absent
certificate-directory slots are unsupported. Some malformed inputs return a
format error; ambiguous or excluded layouts return `STATUS_NOT_SUPPORTED`.
The missing-slot boundary is deliberate: Windows SIP produced a different hash
than the old linear helper on a fixture with zero declared directories. Do not
infer Windows parity from the older helper's successful return on that input.
This is a validated subset, not a claim that every PE accepted by every Windows
SIP is supported. Microsoft's [PE format documentation](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format)
describes section ordering and exclusions, but unusual overlay behavior requires
measurements against the relevant Windows implementation. The local tests use
the actual [CryptSIPCreateIndirectData](https://learn.microsoft.com/en-us/windows/win32/api/mssip/nf-mssip-cryptsipcreateindirectdata)
provider as a differential oracle, rather than treating a prose summary as the
complete algorithm. SIP hash agreement does not establish CI trust-policy parity.
## Discovering the first embedded content's digest algorithm
`DomitoGetPortableExecutableDigestKindEx` navigates a bounded DER subset:
PKCS#7 ContentInfo / SignedData / encapsulated `SpcIndirectDataContent` /
`DigestInfo.AlgorithmIdentifier`. It reads the algorithm associated with the
file digest, avoiding unrelated algorithm patterns in certificates, attributes,
timestamps, or signer information. SHA-1/256/384/512 are recognized. Algorithm
parameters may be absent or ASN.1 NULL; the digest OCTET STRING must have the
corresponding length. DER lengths must be bounded, definite, and minimally
encoded. Up to seven zero bytes of aligned padding after the outer DER are
accepted when included in `WIN_CERTIFICATE.dwLength`, as observed on Microsoft
system binaries. Other trailing bytes are rejected.
An absent certificate table returns `STATUS_NOT_FOUND`, an unsupported algorithm
or certificate type returns `STATUS_NOT_SUPPORTED`, and malformed supported
structure returns a format error. The output algorithm is zero on failure; no
default algorithm is silently manufactured.
The caller supplies an array containing the security-directory entry and a
stable input buffer. The helper inspects the first table entry. It does not
enumerate nested/multiple signatures, validate the entire ASN.1 document,
validate signerInfos, verify cryptographic signatures, or establish trust.
Indefinite-length BER is outside this supported DER subset. Choose explicit
algorithms when handling catalog-only files or application-defined lookup order.
## Explicit legacy catalog policy
`DomitoValidateFileLegacyModeEx` takes a caller-selected
`AcceptedCatalogPolicyBits` mask, a `RequireTrustedCatalog` marker gate, and a
`ValidationSource` output. Source is `DOMITO_CI_SOURCE_EMBEDDED` or
`DOMITO_CI_SOURCE_CATALOG` on success and zero on failure.
This helper accepts SHA-1 with exactly 20 bytes or SHA-256 with exactly 32 bytes.
Unsupported algorithms return `STATUS_NOT_SUPPORTED`; mismatched lengths return
`STATUS_INVALID_PARAMETER`. The underlying observed embedded API supports more
algorithms than this helper's catalog workflow, as documented in
[the mincrypt contracts](mincrypt-contracts.md).
Both policy objects must be zero-initialized before first use, or own valid CI
results. The helper frees previous contents on entry and before each retry and
clears signing time between attempts. The caller owns both final policy results,
including any returned on failure, and frees them with `CiFreePolicyInfo`.
Embedded-to-catalog fallback and catalog reload occur only for
`STATUS_INVALID_IMAGE_HASH`; resource, I/O, and other policy errors propagate.
Before certificate allocation, the helper references the file object and checks
the supplied table interval against the current file size. It then uses the
exact-read `DomitoReadFileAt`, which waits on a private completion event. An EOF
short read fails before CI sees any uninitialized bytes. The caller must keep
file identity/bytes stable between hashing and consumption; a size check cannot
prevent concurrent mutation. Handles must be valid in the current process
context and satisfy the documented kernel I/O/APC requirements.
The older `DomitoValidateFileLegacyMode` delegates with the existing
`DOMITO_CI_LEGACY_CATALOG_POLICY_BITS` (`0x2007F`) and a false marker gate. That
value is a compatibility default with broad categories, not an application trust
recommendation. The explicit mask uses **any-overlap** catalog filtering.
For a catalog-only file, choose an allowed algorithm sequence as application
policy (for example SHA-256 followed by SHA-1 when legacy acceptance is intended).
For each algorithm, call `DomitoCalculatePortableExecutableDigestEx`, then
`DomitoValidateFileLegacyModeEx` with a zero security directory. Free the digest
and both policy objects before retrying. Continue to another algorithm only on
the specifically intended lookup/signature-mismatch result, not on all failures.
Preserve the actual algorithm and validation source in the application's result.
This supports an explicit search strategy; it does not reproduce every native
CI scenario-selection rule or WinVerifyTrust policy.
## Validation performed
On the local Windows 26100 system, host tests compare all four digest algorithms
against Windows SIP for PE32/PE32+, unsigned/signed fixtures, overlays, reverse
section-header order, multiple certificate entries, and certificate alignment.
The installed `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` form a
real-file corpus. Embedded digest algorithms are independently extracted with
Crypt32, and computed file digests are compared with the signed
`SpcIndirectDataContent` values where an embedded signature exists. This reads
files and hashes bytes; it does not load or execute them as code.
Guard-page truncations, malformed offsets/lengths, unsupported layouts,
algorithm selection, injected CNG/allocation failures, retry ownership, exact
read failures, and explicit catalog controls are also covered. Actual CI calls
are mocked in those host tests. The kernel translation units and layout
assertions compile for x64/ARM64 with warnings treated as errors; the opt-in
26100 runtime-layout observations remain x64-specific.
Still pending are historical/ARM64 CI runtime compatibility, Driver Verifier,
real catalog-only acceptance across algorithms, nested/multiple-signature
selection, weak-crypto/revocation/timestamp policy, and extension of supported PE
layouts. Use the [remaining-work ledger](implementation-status.md) and the
[flag probe](ci-validate-file-object.md) to plan the VM run.
+651
View File
@@ -0,0 +1,651 @@
{
"schema_version": 2,
"scope": "Observed named PE exports in exact architecture-specific ci.dll samples; not runtime or ABI certification",
"source_hash_encoding": "Canonical JSON: UTF-8, sorted keys, comma/colon separators, ensure_ascii=false",
"generated_from": {
"corpus-index.json": "d2c6507794314ca38a2015c74f3b66e81a4bd82d982be0ed1911dcff1d79475a",
"iso-corpus-index.json": "ba526a8548f3be100e8d13019aed57de9703a142615c509870720572bc636217",
"manifest.json": "bd215ebd3deb480a39b5d71a6ed709dad20368cbe90879a52070b56b023284d4",
"ci-26100.8328-evidence.json": "ea0f939df9d8b7abde40e3402c01b2ac398253728f329e8c2b7134de8df6305f"
},
"apis": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"samples": [
{
"label": "Windows 10 1507",
"ci_file_version": "10.0.10240.16463",
"architecture": "x64",
"sha256": "e83d900f4675c6e6ffc3fb7dcd3074610876a1398e495796769988df810fd3bc",
"source": "corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": false,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1511",
"ci_file_version": "10.0.10586.0",
"architecture": "x64",
"sha256": "f949bfa614addea6174b28c3818c56b327c9290fd41bd91dfc545414b3df6783",
"source": "corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": false,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1607",
"ci_file_version": "10.0.14393.2189",
"architecture": "x64",
"sha256": "ef0c02ecbd0818e84be9f61421f919825f96a3c59199460cab08548eee2881df",
"source": "corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": false,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1703",
"ci_file_version": "10.0.15063.0",
"architecture": "x64",
"sha256": "3705aaf408f05326e028621380aa5c96a1f3583a2de8a139b188da4b82577863",
"source": "corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1709",
"ci_file_version": "10.0.16299.125",
"architecture": "x64",
"sha256": "1db7ae5a08bd2361fc7fcdd373fb980e8d680035e67b02e5967095db0443effd",
"source": "corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1803",
"ci_file_version": "10.0.17134.1130",
"architecture": "x64",
"sha256": "8ce3b8fe38c23b699f30b045383640019a429d3a9d513285c655bffe03a796d9",
"source": "corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1809 initial x64 ISO",
"ci_file_version": "10.0.17763.1",
"architecture": "x64",
"sha256": "eb177481f2aea3cae0c2f97f7058e7447c0695e4489428406598f168d4dcc5e8",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1809 October v2 x64 ISO",
"ci_file_version": "10.0.17763.348",
"architecture": "x64",
"sha256": "da8282173cbce7ea38f8fca64db0972c3d9614ddbca98f6b96d11a4cadde92ad",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1903 v1 x64 ISO",
"ci_file_version": "10.0.18362.1",
"architecture": "x64",
"sha256": "5d01bb84fa516e23c25def8ea03fc6b696082ccc67a61575fd00d1fe7c273d72",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1903 v2 x64 ISO",
"ci_file_version": "10.0.18362.329",
"architecture": "x64",
"sha256": "d12131f14f781f8c314a795384e02afc3c2902bf67da9c2e75d8163c891bdd89",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 1909 x64 ISO",
"ci_file_version": "10.0.18362.418",
"architecture": "x64",
"sha256": "c3c5d32da42fa69b0795cd0f8c57252670d673fb036685db5c7d6392ffee0293",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 2004 ISO",
"ci_file_version": "10.0.19041.1",
"architecture": "x64",
"sha256": "2955710d34db26e352e62b20861ba338f0216cc36b378d128057be955e184282",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": false,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 22H2 consumer ISO",
"ci_file_version": "10.0.19041.1865",
"architecture": "x64",
"sha256": "d0408a9f64eafb72e8aa1cf127d07d919f0cab38a6bdf2b406255909c85e3a8d",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 11 English x64 v1 ISO",
"ci_file_version": "10.0.22000.318",
"architecture": "x64",
"sha256": "301ae49ac5b87cf1cbe07d9d3c6486a066d045b6bf55fc8eb392d52951ec69ff",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 11 23H2 English ISO",
"ci_file_version": "10.0.22621.2792",
"architecture": "x64",
"sha256": "9ed77b8a53946ced8c3540edb26a765060dff908544634e7b069aa49f2b41559",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 11 24H2 sample",
"ci_file_version": "10.0.26100.9444",
"architecture": "x64",
"sha256": "c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b",
"source": "ci-26100.8328-evidence.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
},
"display_file_version": "10.0.26100.8328"
},
{
"label": "Windows 10 21H2 ARM64 ISO (archive Raspberry Pi folder)",
"ci_file_version": "10.0.19041.1566",
"architecture": "ARM64",
"sha256": "a35f651261691cc67acc9c8bfd00bba7a64dd9f16d2c4b6fe1d183e34aeead78",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 10 22H2.7 ARM64 ISO",
"ci_file_version": "10.0.19041.2965",
"architecture": "ARM64",
"sha256": "33f2d1256f60392bbb84c4251d6435461dfa852a100e065376f83dc8a4dd2ec6",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 11 22H2.9 ARM64 ISO",
"ci_file_version": "10.0.22621.1702",
"architecture": "ARM64",
"sha256": "4164ac08fd19628c5ab6cbca6234265ac2bf7f39d5149f57d3845646d8a8ad60",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
},
{
"label": "Windows 11 24H2 ARM64 ISO",
"ci_file_version": "10.0.26100.1591",
"architecture": "ARM64",
"sha256": "a0eba64a9a3f2006d5832079412eca42cc1b9ed66d19b9b0adb7e59ea354415d",
"source": "iso-corpus-index.json",
"named_exports": [
"CiCheckSignedFile",
"CiFindPageHashesInCatalog",
"CiFindPageHashesInSignedFile",
"CiFreePolicyInfo",
"CiGetCertPublisherName",
"CiGetPEInformation",
"CiInitialize",
"CiSetTrustedOriginClaimId",
"CiValidateFileObject",
"CiVerifyHashInCatalog"
],
"ordinal_only_export_count": 3,
"api_presence": {
"CiCheckSignedFile": true,
"CiFindPageHashesInCatalog": true,
"CiFindPageHashesInSignedFile": true,
"CiFreePolicyInfo": true,
"CiGetCertPublisherName": true,
"CiGetPEInformation": true,
"CiInitialize": true,
"CiSetTrustedOriginClaimId": true,
"CiValidateFileObject": true,
"CiVerifyHashInCatalog": true
}
}
]
}
+184
View File
@@ -0,0 +1,184 @@
# CI API compatibility matrix
This matrix records **named export presence in 20 exact `ci.dll` files**,
with architecture stated separately for each sample.
**Yes** means the complete PE export directory contains the name; **No** means
it does not. Neither result is inferred from a header, import library or PDB.
Export presence alone does not establish a compatible ABI or successful verification.
Versions in the tables are **CI file versions**, not necessarily the kernel version.
In particular, the Windows 10 22H2 consumer ISO contains CI **19041.1865**.
Do not extrapolate a cell to every servicing revision of that Windows release.
[Machine-readable matrix](ci-api-compatibility.json) includes exact SHA-256 identities,
all per-API results and canonical JSON hashes of the retained source inventories.
Source hashes ignore whitespace and line-ending differences between checkouts;
the binary SHA-256 identities always cover the exact original bytes.
The tables use numeric `VS_FIXEDFILEINFO` versions. The original modern x64
sample has **fixed version 26100.9444** but its string `FileVersion` reports
**26100.8328**. Original research/header labels retain that string version;
they identify the same SHA-256 file, not two different CI samples.
## Windows 10 1507–1803 x64 samples
| API | 10240.16463 | 10586.0 | 14393.2189 | 15063.0 | 16299.125 | 17134.1130 |
| --- | --- | --- | --- | --- | --- | --- |
| `CiCheckSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiFindPageHashesInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiFindPageHashesInSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiFreePolicyInfo` | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiGetCertPublisherName` | No | No | No | No | No | No |
| `CiGetPEInformation` | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiInitialize` | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiSetTrustedOriginClaimId` | No | No | No | Yes | Yes | Yes |
| `CiValidateFileObject` | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiVerifyHashInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes |
## Later Windows 10 x64 samples
| API | 17763.1 | 17763.348 | 18362.1 | 18362.329 | 18362.418 | 19041.1 | 19041.1865 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `CiCheckSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiFindPageHashesInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiFindPageHashesInSignedFile` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiFreePolicyInfo` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiGetCertPublisherName` | No | No | No | No | No | No | Yes |
| `CiGetPEInformation` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiInitialize` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiSetTrustedOriginClaimId` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiValidateFileObject` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| `CiVerifyHashInCatalog` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
## Windows 11 x64 samples
| API | 22000.318 | 22621.2792 | 26100.9444 |
| --- | --- | --- | --- |
| `CiCheckSignedFile` | Yes | Yes | Yes |
| `CiFindPageHashesInCatalog` | Yes | Yes | Yes |
| `CiFindPageHashesInSignedFile` | Yes | Yes | Yes |
| `CiFreePolicyInfo` | Yes | Yes | Yes |
| `CiGetCertPublisherName` | Yes | Yes | Yes |
| `CiGetPEInformation` | Yes | Yes | Yes |
| `CiInitialize` | Yes | Yes | Yes |
| `CiSetTrustedOriginClaimId` | Yes | Yes | Yes |
| `CiValidateFileObject` | Yes | Yes | Yes |
| `CiVerifyHashInCatalog` | Yes | Yes | Yes |
## Windows 10 ARM64 samples
| API | 19041.1566 | 19041.2965 |
| --- | --- | --- |
| `CiCheckSignedFile` | Yes | Yes |
| `CiFindPageHashesInCatalog` | Yes | Yes |
| `CiFindPageHashesInSignedFile` | Yes | Yes |
| `CiFreePolicyInfo` | Yes | Yes |
| `CiGetCertPublisherName` | Yes | Yes |
| `CiGetPEInformation` | Yes | Yes |
| `CiInitialize` | Yes | Yes |
| `CiSetTrustedOriginClaimId` | Yes | Yes |
| `CiValidateFileObject` | Yes | Yes |
| `CiVerifyHashInCatalog` | Yes | Yes |
## Windows 11 ARM64 samples
| API | 22621.1702 | 26100.1591 |
| --- | --- | --- |
| `CiCheckSignedFile` | Yes | Yes |
| `CiFindPageHashesInCatalog` | Yes | Yes |
| `CiFindPageHashesInSignedFile` | Yes | Yes |
| `CiFreePolicyInfo` | Yes | Yes |
| `CiGetCertPublisherName` | Yes | Yes |
| `CiGetPEInformation` | Yes | Yes |
| `CiInitialize` | Yes | Yes |
| `CiSetTrustedOriginClaimId` | Yes | Yes |
| `CiValidateFileObject` | Yes | Yes |
| `CiVerifyHashInCatalog` | Yes | Yes |
## Exact sample identities
- Windows 10 1507: **10.0.10240.16463 x64**, SHA-256
`e83d900f4675c6e6ffc3fb7dcd3074610876a1398e495796769988df810fd3bc`.
- Windows 10 1511: **10.0.10586.0 x64**, SHA-256
`f949bfa614addea6174b28c3818c56b327c9290fd41bd91dfc545414b3df6783`.
- Windows 10 1607: **10.0.14393.2189 x64**, SHA-256
`ef0c02ecbd0818e84be9f61421f919825f96a3c59199460cab08548eee2881df`.
- Windows 10 1703: **10.0.15063.0 x64**, SHA-256
`3705aaf408f05326e028621380aa5c96a1f3583a2de8a139b188da4b82577863`.
- Windows 10 1709: **10.0.16299.125 x64**, SHA-256
`1db7ae5a08bd2361fc7fcdd373fb980e8d680035e67b02e5967095db0443effd`.
- Windows 10 1803: **10.0.17134.1130 x64**, SHA-256
`8ce3b8fe38c23b699f30b045383640019a429d3a9d513285c655bffe03a796d9`.
- Windows 10 1809 initial x64 ISO: **10.0.17763.1 x64**, SHA-256
`eb177481f2aea3cae0c2f97f7058e7447c0695e4489428406598f168d4dcc5e8`.
- Windows 10 1809 October v2 x64 ISO: **10.0.17763.348 x64**, SHA-256
`da8282173cbce7ea38f8fca64db0972c3d9614ddbca98f6b96d11a4cadde92ad`.
- Windows 10 1903 v1 x64 ISO: **10.0.18362.1 x64**, SHA-256
`5d01bb84fa516e23c25def8ea03fc6b696082ccc67a61575fd00d1fe7c273d72`.
- Windows 10 1903 v2 x64 ISO: **10.0.18362.329 x64**, SHA-256
`d12131f14f781f8c314a795384e02afc3c2902bf67da9c2e75d8163c891bdd89`.
- Windows 10 1909 x64 ISO: **10.0.18362.418 x64**, SHA-256
`c3c5d32da42fa69b0795cd0f8c57252670d673fb036685db5c7d6392ffee0293`.
- Windows 10 2004 ISO: **10.0.19041.1 x64**, SHA-256
`2955710d34db26e352e62b20861ba338f0216cc36b378d128057be955e184282`.
- Windows 10 22H2 consumer ISO: **10.0.19041.1865 x64**, SHA-256
`d0408a9f64eafb72e8aa1cf127d07d919f0cab38a6bdf2b406255909c85e3a8d`.
- Windows 11 English x64 v1 ISO: **10.0.22000.318 x64**, SHA-256
`301ae49ac5b87cf1cbe07d9d3c6486a066d045b6bf55fc8eb392d52951ec69ff`.
- Windows 11 23H2 English ISO: **10.0.22621.2792 x64**, SHA-256
`9ed77b8a53946ced8c3540edb26a765060dff908544634e7b069aa49f2b41559`.
- Windows 11 24H2 sample: **10.0.26100.9444 x64**, SHA-256
`c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b`.
String FileVersion: **10.0.26100.8328** (original research label).
- Windows 10 21H2 ARM64 ISO (archive Raspberry Pi folder): **10.0.19041.1566 ARM64**, SHA-256
`a35f651261691cc67acc9c8bfd00bba7a64dd9f16d2c4b6fe1d183e34aeead78`.
- Windows 10 22H2.7 ARM64 ISO: **10.0.19041.2965 ARM64**, SHA-256
`33f2d1256f60392bbb84c4251d6435461dfa852a100e065376f83dc8a4dd2ec6`.
- Windows 11 22H2.9 ARM64 ISO: **10.0.22621.1702 ARM64**, SHA-256
`4164ac08fd19628c5ab6cbca6234265ac2bf7f39d5149f57d3845646d8a8ad60`.
- Windows 11 24H2 ARM64 ISO: **10.0.26100.1591 ARM64**, SHA-256
`a0eba64a9a3f2006d5832079412eca42cc1b9ed66d19b9b0adb7e59ea354415d`.
## What callers can rely on
- `CiValidateFileObject` is already exported in the sampled Windows 10 1507 file.
- `CiSetTrustedOriginClaimId` is present in the sampled 1703 file and absent in
the three earlier samples. This does not prove its first introduction build.
- On sampled x64 files, `CiGetCertPublisherName` is absent in 19041.1
and present in 19041.1865.
Its first Windows 10 servicing update remains unknown. It is also present
in the sampled 22000.318 and 26100.9444 fixed-version files.
- The Windows 10 ARM64 21H2 sample already exports the publisher-name API
in CI 19041.1566. That does not establish x64 availability at that revision.
- CI entry points currently use static imports from `ci.lib`. An unavailable
import can prevent driver loading; this matrix does not add an automatic
`STATUS_NOT_IMPLEMENTED` fallback. Supporting these older samples requires
avoiding unavailable imports or implementing optional resolution.
- Choose structures using a verified runtime profile. The sampled x64 chain
headers differ: 1507 uses 56 bytes, 1511 uses 72, and 1607–1803 use 104.
Export presence, compile-target `NTDDI_VERSION` and total allocation `Size`
do not select a compatible layout. See [mincrypt contracts](mincrypt-contracts.md).
## Coverage gaps and evidence
**Unknown / not sampled:** x86 export availability; pre-Windows-10;
earlier Windows 10 ARM64 releases; and other servicing revisions
or architecture/release combinations absent from these tables.
These are gaps, not negative export results. No interval between
two samples is certified. Runtime verification and cross-version ABI checks
remain in the [implementation ledger](implementation-status.md).
The tables cover every named export found in these samples, including
`CiInitialize`; inclusion is not a recommendation to call initialization or
global-policy routines. Each sample also has three ordinal-only exports,
which are outside this named-API matrix. Private `Mincrypt*` PDB symbols are
not automatically callable exports.
Source inventories are retained in the companion
[CodeIntegrity-RE repository](https://git.nefarius.at/nefarius/CodeIntegrity-RE):
`corpus-index.json` (VM samples), `iso-corpus-index.json` (ISO samples), and
`ci-26100.8328-evidence.json` with `manifest.json` (original 26100 sample).
Regenerate with its `generate_compatibility_matrix.py --output <Domito/docs>`.
ARM64 cells establish export presence only; the historical x64 layout views
must not be selected for ARM64 solely because the same API name is exported.
No Windows binary or PDB is distributed with the matrix.
+236
View File
@@ -0,0 +1,236 @@
# CiValidateFileObject: validation flag 0x00000020
Issue #3 is **documented; its TESTSIGNING-off runtime experiment is pending**.
A related signer-selection experiment has now run successfully on Windows 11
22631 with TESTSIGNING enabled (below). That run does not close the requested
19041/19045 flag 0 versus 0x20 TESTSIGNING-off comparison.
## Meaning supported by the evidence
`DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE` is Domito's descriptive name for
`0x00000020` in the second argument, `ValidationFlags`. It is not a recovered
Microsoft macro name. On the analyzed **ci.dll 10.0.26100.8328 x64**, the flag
causes `CipGetAcceptedRootKeys` to OR `0xA4FF` into its accepted-root mask in the
branch where the global at RVA `0x45A88` is zero. Other global state, flags, and
the requested signing level also affect the result.
The assembly at preferred image base `0x180000000` establishes the operation:
```text
0x180089e40: MOV EDX, 0xa4ff
0x180089e4c: BT ECX, EDI ; ECX = 0x8c in this branch; EDI = signing level
0x180089e4f: JC 0x180089e57
0x180089e51: TEST SIL, 0x20 ; saved ValidationFlags
0x180089e55: JZ 0x180089e59
0x180089e57: OR EBX, EDX ; broaden accepted-root mask
```
For requested level 0, flags 0, the branch above, and all other optional global
additions disabled, the mask is `0x80AA`; setting only `0x20` makes it `0xA4FF`
(additional bits `0x2455`). This is an arithmetic witness, **not a measurement of
the running OS's initialized policy**. Levels 2, 3, and 7 already trigger the OR
through `0x8C`. When the global at RVA `0x45A88` is nonzero, the helper returns
`0xA4FF` directly, so the flag need not change its result.
Later processing can still remove bit `0x8` when the helper's third argument is
nonzero and validation flag `0x00400000` is present. The surrounding
`CipDisallowEmbeddedPicrootIfTrusted` path can add that latter flag itself.
Thus `0xA4FF` is not a promise about every final policy result.
The widened mask includes categories associated with test roots and unresolved
chains as well as other roots. It must not be named "trust any Authenticode
signer", "require Microsoft", or "enable TESTSIGNING". It changes the acceptance
policy for this operation; it does not change the system boot setting. Further
signature, chain, signing-level, and policy checks still matter. A successful
call alone does not establish equivalence with WinVerifyTrust or the caller's
intended publisher trust policy.
`CipGetHashAlgorithmForLegacyScenario` does not test `0x20`: the flag is not a
fixed SHA-1/SHA-256 selector. Also distinguish this **input flag** from the
unrelated `MINCRYPT_POLICY_INFO.PolicyBits` output bit `0x20`, associated with
the built-in DMD root category in this build.
## Historical generation differences
The same numeric flag does not imply the same internal root mask across Windows
versions. Static analysis of the extracted x64 corpus adds these bounded facts:
- On sampled `10.0.10240.16463` (1507), `10.0.10586.0` (1511),
`10.0.14393.2189` (1607) and `10.0.15063.0` (1703), the recovered
`CipGetAcceptedRootKeys` bodies have no direct `flag & 0x20` broadening branch.
They still test larger masks containing that bit, so this is not evidence that
the flag has no effect anywhere in those verifiers.
- On sampled `10.0.16299.125` (1709) and `10.0.17134.1130` (1803), when the
helper's main global mode is zero,
`flag & 0x20` selects mask `0x224FF` (assignment), rather than the modern
`mask |= 0xA4FF` operation. A signing-level predicate can select the same mask.
Other global additions occur afterward. The other global mode returns
`0x204FF` directly.
The observed helper masks include branch-specific diagnostics/categories and
must not be interpreted as an official stable root enum. The descriptive Domito
constant names the intended broadening use without promising a single mask or
identical behavior on every build.
The ISO-derived **10.0.19041.1 x64** helper provides direct static evidence for
the reported build family: in its main global mode it uses `mask |= 0x24FF`
when flag `0x20` or the signing-level predicate applies; the alternate mode
returns `0x24FF`. With requested level zero and optional global additions
disabled, `0xAA` becomes `0x24FF` (added bits `0x2455`). This is an arithmetic
witness, not a measurement of initialized OS policy. The exact CI SHA-256 is
`2955710d34db26e352e62b20861ba338f0216cc36b378d128057be955e184282`.
CodeIntegrity-RE retains matched-PDB pseudocode and assembly in
`historical/ci-19041.1-contracts.json` and `historical/ci-19041.1-assembly.json`,
with batch evidence ID
`ev_13f89502777ee38ea98410f7a07f078bb934e78d792b662ab242f7b11d27e747`.
Other 19041/19045 servicing revisions and the TESTSIGNING-off runtime workaround
still require their own evidence.
Historical records in CodeIntegrity-RE:
`historical/ci-14393-contracts.json`
`ev_e63a4c4b7613e231795051b784b9571605b9bf81258b5dcd04d32ddc681abb41`,
`historical/ci-15063-contracts.json`
`ev_338658fb82559fa6daa8ae5487b90fe84b8e408ea7578feed6d8dff5d590e114`,
`historical/ci-16299-contracts.json`
`ev_fe5731175f0726f0d46af2f78ca615d284f8598939296815e26b5589db3001cf`,
and `historical/ci-17134-contracts.json`
`ev_0c3e3359b8b7486318006afa6b95eb2257a18abeec9041a86a32db487bcbe996`.
## Evidence and compatibility limits
The [upstream report](https://github.com/Ido-Moshe-Github/CiDllDemo/issues/5#issuecomment-2725851295)
describes success with flags `0x20`, signing level 0, and TESTSIGNING off on
Windows builds 19041 and 19045. That is third-party runtime evidence, not a
reproduction by this project. Its behavior is consistent with broader root
acceptance; it does not establish identical internals across those builds.
Our static evidence uses REA 6.8.0 / Ghidra 12.1.3 and the binary SHA-256
`c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b`.
Public-symbol mapping uses ci.pdb GUID `1CAE574B-CF1F-3564-5F74-49567A128629`, age 1.
The companion CodeIntegrity-RE repository retains:
- `ci-flag20-assembly-evidence.json`: assembly evidence
`ev_ea763b9be7643a6241b0c805eae5f735a4b4a0a82df1383b1e68da4207f85fb4`.
- `evidence/CipGetAcceptedRootKeys.c`: decompilation
`ev_c905f03db9470c8b2d89a6295080143f25d5780ca0909de0ca554e40d078a3f3`.
- `evidence/CipGetHashAlgorithmForLegacyScenario.c`:
`ev_addf8f1bc18da58db80db54eeab0dd78ec5ab88b219ece3e082ebd9a8e50b257`.
- `evidence/CipDisallowEmbeddedPicrootIfTrusted.c`:
`ev_2475a5804fc87683a54b06acebd3785e5e8eb31f4f542f699fc2ff414c1ab7fe`.
- `evidence/CipValidateFileObjectByFileOrPageHash.c`: capacity contract
`ev_3897aa76c17cc64a30f60e72b994e695b88c8eda2ae6f246766bdc9314f06cca`.
No complete enumeration of downstream uses of the flag, historical Microsoft
name, or cross-version/architecture guarantee is claimed. The public declaration
keeps the existing parameter widths. Names now describe the known inputs; on
26100 x64 the third argument is consumed as a byte. Narrowing the ABI everywhere
requires separate evidence.
## Kernel experiment prepared for the VM
Add `tests/ci-file-object-probe.cpp` to an existing signed test driver and add
`include/` to its include path. Link the existing CI imports (`ci.lib`). Include
`ntifs.h`, `Domito.MinCrypt.h`, and `tests/ci-file-object-probe.h` at the call site.
Invoke `DomitoProbeCiValidateFileObject` at PASSIVE_LEVEL with a referenced
file object for the fixture and an array of `DOMITO_CI_PROBE_RESULT_COUNT` results.
Keep the file bytes stable and hold the object reference for the entire probe.
The surrounding driver owns opening/referencing/closing the file and exporting
the result records. The probe has no DriverEntry or installation side effects.
It makes six calls, always at requested level 0:
1. Flags `0`, then `0x20`, then `0x20`, then `0`, with 64-byte capacity each time.
2. Flags `0`, then `0x20`, with declared capacity 63 (64-byte backing storage).
Each call uses fresh policies, hash capacity, algorithm, and signing-time storage.
Both policy objects are freed after capture, including on failure. The records
capture status, both policy statuses/bits, chain presence, signing time, digest
algorithm/length/bytes, and a trailing buffer guard. The digest is a **file hash**,
not a certificate thumbprint. Failed-call outputs are raw diagnostics, not valid
verification results. Chain internals are deliberately not dereferenced.
The repeated order helps reveal cache/order effects but does not eliminate them.
Also run the reversed initial order in a fresh VM snapshot when comparing policy.
For the analyzed 26100 helper, both 63-byte-capacity calls should return
`STATUS_BUFFER_TOO_SMALL` and set length 64, regardless of the eventual algorithm.
Treat older-build differences as observations to investigate, not silently as
test success. Every guard must remain intact; successful hash sizes must be at
most 64. Inspect non-success statuses rather than treating all failures as
invalid signatures (I/O and resource errors are possible).
Run on Windows 10 19041 and 19045 and a current Windows 11 build. The decisive
reproduction uses a driver loadable with TESTSIGNING **off**. Record that setting,
Secure Boot, HVCI/WDAC policy, OS build, CI version/hash, driver identity, fixture
SHA-256, and each complete result. Keep policy constant between paired calls.
Use these fixtures:
- A known third-party RSA Authenticode executable (ideally the reported fixture).
- A Microsoft-signed executable and a catalog-signed file as controls.
- An unsigned executable and a copy of the signed file with a byte changed in a
hashed section, leaving its certificate table unchanged.
- A self-signed/test-root fixture to characterize the broader acceptance policy.
- If available, timestamped, expired, revoked, and ECC variants, with expectations
recorded in advance and revocation/configuration differences retained.
Compare with `signtool verify /pa /v` as a separate user-mode policy observation,
not an equality oracle. The report is reproduced only when the same valid
third-party fixture changes from rejection at 0 to success at `0x20`, under the
reported environment, with usable outputs. Unchanged success can mean the
existing policy already accepts it. Unchanged failure is a result to investigate.
Unsigned and tampered fixtures must not become accepted as valid signatures.
Self-signed acceptance must be reported explicitly, not confused with public-CA
publisher trust. Do not use this flag as a new production default based only on
one positive fixture.
## Multiple signatures: a separate kernel experiment
`0x20` is an accepted-root control, not a recovered "PE image" type flag.
In the examined 22621.2792 and modern 26100 x64 bodies it leaves the
preferred-signing-level search active. `PolicyInfo` can therefore describe a
nested Microsoft signature even when the primary company signature also verifies.
The first-success guard tests aggregate masks `0xE47FFF87` (22621.2792) and
`0xE41FFF87` (modern 26100). `0x20` intersects neither; `0x80` intersects both.
Compare flags `0x20` versus `0xA0`, requested signing level zero, on the same
stable dual-signed file. Record the actual selected leaf/thumbprint, both policy
statuses and signing time. Do not interpret status success alone as selection
of the company certificate. First successful does not necessarily mean first
listed: CI may reject the primary and continue.
`0x80` has additional policy and fallback effects. Domito exposes it only as
`DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL` and through the opt-in
`DomitoCiSignatureSelectionFirstSuccessfulExperimental` mode in
`DomitoValidateFileObjectEx`. It is not a production default or a stable signer
index. See the [validation helper contracts](ci-validation-helpers.md). The companion research
`SIGNATURE-SELECTION.md` provides the full static evidence and an alternative
primary-signature path using `CiCheckSignedFile`.
A user-run kernel probe on Windows 11 **10.0.22631 x64**, CI options **0x00080207**
(TESTSIGNING enabled), confirms the selection difference for the exact dual-signed
BthPS3 fixture: `0x20` returns Microsoft, while `0xA0` returns the Nefarius leaf
`FF0CCF318FD8C14775023A0B52C28FFCCA59D7F7` and signing time
**2026-10-02T18:32:01.000Z**. Direct primary SHA-256 `CiCheckSignedFile` returns
the same Nefarius leaf, signer/timestamp policies and signing time. All three
calls return STATUS_SUCCESS; all policy VerificationStatus fields also succeed.
The file digest is identical across calls, before/after file hashes match, and
TraceFmt reports 149 events with no losses, format errors or unknown formats.
The companion `signature-selection/runtime-22631-bthps3.json` records exact
trace hashes and the on-disk CI hash. This is one test-signing-enabled run;
TESTSIGNING-off, other builds/policies and general signature enumeration remain
unverified. `0xA0` does not mean "select any desired publisher".
## Checks available without the VM
From an x64 Visual Studio developer shell:
```powershell
python tests\run-ci-probe-tests.py
```
This compiles the actual probe body with mock CI calls and constants extracted
from the public header. It verifies call order, input forwarding, fresh capacity
and outputs, result capture, and freeing both policies after success and failure.
It does **not** emulate the flag's trust semantics. Kernel compilation checks
the real headers separately; neither compilation nor mock tests substitute for
the VM run above.
+118
View File
@@ -0,0 +1,118 @@
# CI validation helpers
These additive APIs are declared in `Domito.h` for Windows 10 compile targets.
They reuse CI for cryptographic verification. They do not implement RSA/ECDSA,
certificate-chain building or timestamp-signature verification themselves.
Raw CI declarations and existing digest/legacy helpers are unchanged.
## Result lifetime
Zero-initialize `DOMITO_CI_VALIDATION_RESULT` before first use. Each validation
call frees its previous signer/timestamp policies before reuse and resets the
result. Do not pass uninitialized storage or copy an owning result as a second
owner. On every exit, including a failed call, release it with
`DomitoFreeCiValidationResult`. The destructor accepts NULL and empty results.
All three APIs require PASSIVE_LEVEL. Referenced input objects/buffers must stay
valid and file bytes must stay stable throughout validation and consumption.
On success, `Hash`/`HashSize`/`HashAlgId` describe a known SHA-1/256/384/512 file
digest with a consistent length. Unused digest bytes are zero. This is not a
certificate thumbprint. On failure, digest bytes/length/algorithm and signing
time are zero; policy objects may retain owned diagnostic allocations. Input
flags/level forwarded to the file-object export remain recorded if it was called.
Those fields are not CI's final internally normalized policy flags.
Check the returned NTSTATUS first. Policy verification status, policy bits,
certificate identity, timestamps and the application's intended trust policy
remain separate observations/checks. Returning STATUS_SUCCESS does not pin a
company signer, reproduce WinVerifyTrust policy or prove driver-load acceptance.
No helper traverses the private chain layout or matches a certificate for you.
## File-object path
`DomitoValidateFileObjectEx` takes a referenced FILE_OBJECT, caller-selected
validation flags, a byte-sized requested signing level and a selection mode.
It supplies fresh 64-byte hash capacity and initialized policy/time outputs on
every call. It does not read, duplicate or acquire ownership of the file object.
- `DomitoCiSignatureSelectionNative` forwards the caller's flags unchanged.
This is native policy, not a guarantee of preferred-level selection: the
caller's other flags may already change the search/exit path.
- `DomitoCiSignatureSelectionFirstSuccessfulExperimental` additionally sets
`DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL` (0x80). On examined x64
builds this changes a successful-candidate early-exit predicate. It also
affects other policy/fallback paths. This explicit mode returns
STATUS_NOT_SUPPORTED on non-x64 builds. No runtime behavior on ARM64 is implied.
Neither mode automatically adds `0x20`. Broadening accepted roots is a separate
caller decision. Neither mode selects an arbitrary signer index or guarantees
that signature zero succeeds. A rejected primary may still lead to another signer.
```c
DOMITO_CI_VALIDATION_RESULT result = {0};
NTSTATUS status = DomitoValidateFileObjectEx(
fileObject,
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
0,
DomitoCiSignatureSelectionFirstSuccessfulExperimental,
&result
);
if (NT_SUCCESS(status))
{
// Apply your expected-certificate/trust policy to result.PolicyInfo.
// result.SigningTime belongs to that selected verification result.
}
DomitoFreeCiValidationResult(&result);
```
The native kernel experiment on the supplied dual-SHA256 BthPS3 fixture returned
Microsoft for flags 0x20 and Nefarius for flags 0xA0 on a Windows 11 22631 x64
TESTSIGNING-enabled machine. It does not establish all older builds, TESTSIGNING-off
behavior or every side effect of 0x80. See [the flag evidence](ci-validate-file-object.md).
Do not rename this mode "require our company" or "select primary signature".
## Embedded PE-buffer path
`DomitoValidatePortableExecutableEmbeddedSignature` accepts a stable PE buffer
and an explicit CALG_SHA1/CALG_SHA_256/CALG_SHA_384/CALG_SHA_512. It calls
`DomitoCalculatePortableExecutableDigestEx` and supplies the resulting digest
and original first WIN_CERTIFICATE to `CiCheckSignedFile`. The digest allocation
is always freed, and only copied digest bytes/owned CI policies remain in the
result. Unsupported algorithm/layout and CI errors propagate without trying a
different algorithm or a catalog. An absent certificate returns
STATUS_INVALID_IMAGE_HASH; the helper cannot turn an unsigned file into a
successful catalog verification.
```c
DOMITO_CI_VALIDATION_RESULT result = {0};
NTSTATUS status = DomitoValidatePortableExecutableEmbeddedSignature(stablePeBytes, peSize, CALG_SHA_256, &result);
if (NT_SUCCESS(status))
{
// Enforce the expected certificate and intended trust policy here.
}
DomitoFreeCiValidationResult(&result);
```
This uses the explicit digest helper's [supported PE layout subset](authenticode-digests.md):
bounded headers, contiguous nonoverlapping raw sections in file order and an EOF
certificate table. It is not an arbitrary PE/SIP parser. CI can select nested
signatures according to its algorithm rules, so the helper is deliberately named
**embedded**, not **force primary**. The matching SHA-256 request selected the
company primary in the tested BthPS3 layout; another layout may select another
signer or fail. General enumeration remains separate work.
## Support and validation
The functions are C-compatible and compile for x64/ARM64. The experimental
file-object mode is deliberately gated to x64; native forwarding/embedded code
compilation does not certify every architecture/build's CI runtime ABI. CI
imports are static and must exist on the target OS, as described in the
[availability matrix](ci-api-compatibility.md). Structure/chain interpretation
remains subject to the [mincrypt contracts](mincrypt-contracts.md).
The new host suite checks production code forwarding, hash invariants, failures,
partial allocations, cleanup, result reuse and the non-x64 conditional branch.
The existing digest suite independently compares Windows SIP and Crypt32 results.
Kernel compilation covers the full library, public C clients and layout checks.
CodeIntegrity-RE probe v2 now calls the new APIs; a further kernel run is needed
to validate these wrappers rather than merely their previously tested raw calls.
+247
View File
@@ -0,0 +1,247 @@
# Migrating consumer Authenticode helpers
These helpers centralize file validation, certificate inspection, hashing and
allocation cleanup. The contracts keep application identity and trust policy
explicit. Host tests and kernel compilation do not establish new CI runtime
guarantees.
## Verified fixes and existing coverage
- **Implementation defect fixed:** `DomitoGetProcessImageName` freed its allocation
on a failed second query but left that freed address in the caller's output.
It also left old caller output untouched on earlier failures. It now clears
the output first, publishes only a successful bounded result, and frees its
local allocation on all other paths. The process handle uses limited-query
access (`0x1000`) instead of `PROCESS_ALL_ACCESS`. A PID is resolved once to a
process handle; no later query re-resolves that PID.
- **Output-contract defect fixed:** `DomitoFindModuleBaseAddress` could preserve
an old address when lookup failed. It now clears the optional output first and
validates the counted module name. The export-by-module wrapper preserves the
underlying status, including missing exports and unsupported forwarders.
- **Annotation defect fixed:** the `DomitoReadFileAt` definition's single-object
output annotation disagreed with its public byte-count annotation. Both now
describe `BufferSize` writable bytes.
- **Already addressed before this review:** exact file reads return
`STATUS_END_OF_FILE` on short reads and wait for their own pending request.
Explicit digest helpers bound PE32/PE32+ headers, sections, certificate spans
and DER navigation. Certificate pointers borrow the stable input buffer.
[CI validation results](ci-validation-helpers.md) initialize outputs and own
both policies, including diagnostic allocations on failure. The current raw
CI declaration names the flags and byte-sized signing level; `0x20` broadens
root acceptance in examined builds, rather than declaring a file to be PE.
Applications should avoid truncating file lengths to `EndOfFile.LowPart`,
reparsing fixed-kind PE headers, consuming uninitialized CI policies, skipping
cleanup on failure, or accepting invalid hexadecimal digits. The helpers below
provide reusable primitives without defining application authorization policy.
## File object, handle and path validation
`DomitoValidateFileObjectEx`, `DomitoValidateFileHandleEx` and
`DomitoValidateFilePathEx` expose the same explicit flags, requested signing
level, selection mode and owning `DOMITO_CI_VALIDATION_RESULT`. None adds flags
or falls back to a different validation policy. They require PASSIVE_LEVEL and
Windows 10 compile targets; the CI imports must exist on the target OS.
They do not require `DomitoInit`.
The handle wrapper requires a trusted **kernel handle** with `FILE_READ_DATA`.
It references its FILE_OBJECT, releases that reference on every exit and leaves
the caller's handle open. Never pass an unchecked user handle: the reference is
made using KernelMode. The caller must keep the handle valid during validation.
The path wrapper accepts a counted absolute native path, for example
`\??\C:\Program Files\Example\app.exe`. It creates a read-only kernel handle,
shares only reads, references that same opened file for CI, and closes its own
handle. Existing incompatible writers cause an open failure. Special kernel APCs
must be enabled for synchronous I/O. Reparse points may redirect the open;
validation describes the opened object. It does not establish the identity of
a previously looked-up process image or certify in-memory process contents.
File-object/handle callers must independently provide stable file bytes.
```c
DOMITO_CI_VALIDATION_RESULT result = {0};
NTSTATUS status = DomitoValidateFilePathEx(&nativePath, selectedCiFlags, 0, DomitoCiSignatureSelectionNative, &result);
if (NT_SUCCESS(status) && NT_SUCCESS(result.PolicyInfo.VerificationStatus))
{
// Apply caller-provided certificate/publisher/pin policy here.
// A nonzero SigningTime is a separate consumer requirement, if desired.
}
DomitoFreeCiValidationResult(&result); // Also on CI or application-policy failure.
```
Modern file-object verification can select a preferred Microsoft nested signer
where direct embedded verification selected the company signer. Changing paths
therefore requires an explicit migration decision. Keep
`DomitoValidatePortableExecutableEmbeddedSignature` for the stable PE-buffer,
explicit-algorithm route. Its supported PE subset and nested-selection limitations
still apply. It verifies the original first WIN_CERTIFICATE through CI, with no
algorithm retry or catalog fallback. Existing legacy/catalog APIs remain
available with explicit catalog policy; new convenience wrappers do not invoke
them implicitly. An absent optional API is not permission to relax trust policy.
## Certificate fingerprints and SPKI pins
`DomitoGetCertificateSpki` extracts two borrowed views from stable certificate
DER: `Encoded` contains the complete SPKI SEQUENCE TLV; `Content` omits that
sequence's tag and length. It handles optional X.509 version fields and bounded,
minimal definite DER lengths. It validates navigation/framing and the SPKI field
shape, rather than every X.509 field, extension, name, algorithm parameter or
signature. It does not verify trust. Failed extraction clears both views.
`DomitoHashBuffer` hashes exact bytes using SHA-1/256/384/512 and Windows CNG.
`HashSize` is capacity on entry, actual length on success, required length on
`STATUS_BUFFER_TOO_SMALL`, and zero on other failures. A NULL hash buffer queries
the required size. Operational CNG failures erase the selected digest bytes.
Input/output buffers must not overlap. Only consume a digest after success.
`DomitoHashCertificate` takes an explicit `DOMITO_CERTIFICATE_HASH_KIND`:
- `DomitoCertificateHashDer`: exact complete certificate bytes, suitable for a
DER thumbprint. This mode hashes bytes; it does not parse or validate them.
- `DomitoCertificateHashSpkiDer`: complete SPKI encoding, including outer TLV.
- `DomitoCertificateHashSpkiContentLegacy`: **preserves the consumers' existing
content-only convention**. Existing pins must use this mode. Switching to the
complete encoding requires recomputing pins from the intended certificates;
do not silently treat hashes from the two representations as interchangeable.
```c
UCHAR pin[MINCRYPT_SHA256_LENGTH];
ULONG pinSize = sizeof(pin);
status =
DomitoHashCertificate(cert.Data, cert.Size, DomitoCertificateHashSpkiContentLegacy, CALG_SHA_256, pin, &pinSize);
```
`DomitoDecodeHex` accepts an exact counted Unicode hex string for a caller-selected
binary length, rejects invalid digits/odd or mismatched lengths, and zeroes its
destination on malformed input. `DomitoEncodeHex` writes uppercase Unicode hex
with a NUL terminator; its capacity is in WCHARs including the terminator.
Decode configuration pins once into binary and compare like representations.
The library embeds no publisher names, root pins, or certificate allowlists.
Hashing does not prove that a certificate is a root or trusted issuer.
## Chain inspection and publisher ownership
`DomitoGetCiChainView` requires a known, readable, initialized CI-owned policy
and an explicit `DomitoCiChainLayoutObservedX64` argument. This asserts the
sampled x64 common prefix (+0x28 elements, +0x30 count, +0x34 opaque RootId,
minimum 56-byte header, 120-byte elements). It is **not** automatic OS/layout
detection. Non-x64 builds return `STATUS_NOT_SUPPORTED`; allocation size and
compile-target NTDDI cannot establish runtime layout compatibility.
The accessor bounds the element array and every exposed certificate/name blob
within CI's owned allocation before returning a view. It rejects oversized hash
lengths and absent certificate bytes. It exposes neither trailing attributes nor
EKU/public-key arrays. It cannot make a forged or freed kernel pointer safe;
allocation extent/readability and the asserted layout remain caller preconditions.
All returned pointers expire when the policy is freed or its result reused.
Sampled CI results are leaf-first. Inspect `Elements[0]` for that selected leaf.
`Elements[Count-1]` is the last represented certificate; this API does **not**
promise it is self-signed, the configured trust anchor, or that the chain includes
every certificate through a root. `RootId` is opaque and does not establish those
properties. A consumer requiring a specific root must establish its intended CI
policy and represented-anchor assumptions instead of inferring trust from position.
The accessor deliberately imposes no bytewise issuer/next-subject equality.
CI name blobs are not an independent chain verifier;
their encoding and representation are version-sensitive. Current evidence does
not establish universal byte equality as a CI API contract. Preserve such a check
as an explicit downstream restriction if compatibility demands it, and assess its
false-rejection risk. CI return status, policy verification status and application
identity/pin requirements remain separate checks.
`DomitoGetCertificatePublisherName` dynamically resolves the optional CI export
instead of adding a static import. It requires `DomitoInit`; missing modules,
exports and unsupported export forwarders propagate their lookup status.
The caller passes an empty `UNICODE_STRING`, and uses
`DomitoFreeCertificatePublisherName` to release/reset it. Partial allocations on
failure are cleaned internally; the destructor is safe again afterward. Its
private pool allocator is independent of configurable Domito callbacks. Never
free this string through `G_Memory`, RtlFreeUnicodeString or the process-name free
API. Display-name comparison remains caller policy, not certificate identity.
## Process names, allocators and optional exports
`DomitoGetProcessImageName` returns one Domito allocation containing the
`UNICODE_STRING` and its buffer. `DomitoFreeProcessImageName` frees the entire
allocation once, without querying callbacks in consumer code. Its output is
NULL on failure. The existing ULONG PID interface remains; a PID lookup can race
process exit/reuse before the initial open and is not a durable process identity.
`DomitoGetProcessImageNameEx` instead accepts a caller-referenced PEPROCESS.
It uses `SeLocateProcessImageName`, copies the name into the same Domito-owned
representation and frees the OS allocation internally. The caller must keep the
process referenced for the call. No Domito initialization is needed for this Ex
API. The PID API still requires `DomitoInit` for its query routine.
Neither name API proves that a subsequent path open addresses the original
image file. WDF/requestor-process lifetime and authorization remain downstream.
**Allocator contract:** the existing callback API does not attach allocator
provenance to each result and is not synchronized for concurrent replacement.
Configure a compatible allocate/free pair during initialization, before concurrent
calls, and keep it unchanged until all callback-backed allocations and in-flight
calls are released. A dedicated free name does not make changing callback pairs
with outstanding allocations safe. Do not retrofit a hidden prefix onto existing
allocations: callers may already free the public base address with their callback.
CI policies and the publisher helper use their dedicated free APIs instead.
`DomitoFindModuleExportedFunctionAddress` combines the existing full-module-path
and export-name lookups, clears its output and preserves useful failure status.
It requires `DomitoInit`, and does not pin the module. The caller must guarantee
that the module remains mapped throughout lookup and use of its returned address.
## Integration and consumer boundary
Import `Domito.props` from the Domito package location or override
`DomitoRootPath`. The default root is the sheet's own directory. x64/ARM64 and
Debug/Release remain selected by the project; `DomitoLibraryPlatform` can override
the library platform directory. Both WDM and KMDF final driver projects receive
Domito, CI and CNG dependencies. Importing a sheet into a static library does not
make link dependencies transitive: import it into the final driver as well.
WDF caches, PID caches, administrator checks, developer bypasses, request allowlists,
build expiry and selected-signing-time requirements stay in consumer code.
No default trust evaluator is added: that would silently choose policy absent
from the generic SDK. WPP diagnostics can enumerate the bounded chain view without
fixed-depth indentation arrays. Report the returned NTSTATUS and local policy
failure reason separately rather than mapping all failures inside the SDK.
The new host suite compiles the production code unchanged apart from its kernel
include, uses deterministic kernel doubles for ownership/failure paths, compares
SPKI bytes and fingerprints with Crypt32, and uses real CNG for hashing. It tests
both architecture conditional branches, which is not ARM64 runtime emulation.
Real WDK compilation separately covers x64 and ARM64. These checks do not certify
kernel CI behavior, every chain layout, or full X.509 validation.
References: [SeLocateProcessImageName ownership](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-selocateprocessimagename),
[BCryptCreateHash memory management and kernel requirements](https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash).
## Deprecated compatibility entry points
The public header uses MSVC `__declspec(deprecated("migration guidance"))` for
four older functions. Calling or taking their address produces C4996 in C and
C++; declarations and binary entry points remain available. Projects treating
warnings as errors must migrate, or suppress C4996 locally around a deliberate
compatibility call. Do not disable the warning globally to conceal accidental use.
- `DomitoGetPortableExecutableDigestKind` →
`DomitoGetPortableExecutableDigestKindEx`: check NTSTATUS; unrecognized input
no longer silently chooses SHA-1. Neither function validates signature trust.
- `DomitoCalculatePortableExecutableDigest` →
`DomitoCalculatePortableExecutableDigestEx`: select the algorithm explicitly
and review the supported layout/rejection rules. This is not a drop-in semantic
change for arbitrary files previously accepted by linear hashing.
- `DomitoValidateFileLegacyMode` → `DomitoValidateFileLegacyModeEx`: select catalog
policy and trusted-catalog requirements explicitly, and consume the validation
source. The Ex legacy path remains available where catalog/older-OS behavior
is required; migration does not implicitly switch to modern signer selection.
- `DomitoGetProcessImageName` → `DomitoGetProcessImageNameEx`: acquire and hold the
intended PEPROCESS reference, then use the dedicated name destructor. Looking
up a PID immediately before the Ex call cannot eliminate an earlier PID-reuse
race; obtain the process reference from the operation's intended identity.
This migration does not solve subsequent path-reopen identity races.
No old type has an unconditional replacement. Raw CI/mincrypt types still
describe the native ABI; the owning validation result and borrowed chain view
serve different purposes. Raw Windows CI exports, allocator callbacks, module
lookup primitives, exact file reads and digest cleanup remain supported.
+127
View File
@@ -0,0 +1,127 @@
# CI SDK implementation status
This ledger tracks the source audit S01–S18 and the remaining binary research.
The original CodeIntegrity-RE audit remains a snapshot of commit `7985b23`;
it should not be read as describing the corrected source unchanged.
## Implemented with host tests or compilation
- Additive CI result/selection helpers now package native file-object validation,
opt-in x64 first-success selection and explicit embedded verification, with
shared policy/digest cleanup. Existing APIs retain their behavior. See the
[helper contracts](ci-validation-helpers.md). Host failure/forwarding tests and
x64/ARM64 builds cover the wrappers; WPP probe v2 consumes them and awaits a
returned kernel trace. The previous v1 trace establishes the raw native routes.
- **S01–S04:** subtraction-safe PE ranges, no fixed 4 MiB copy, bounded certificate
intervals, deterministic failure outputs, and digest cleanup. Compatibility
and explicit paths share the bounded hashing core.
- **S05:** actual certificate read length checked; allocation bounded against
file size; per-operation completion waits through the exact-read helper.
- **S06:** `DomitoReadFileAt` uses an explicit offset and private kernel event;
`DomitoReadFile` reads exactly from offset zero. Pending completion cannot
outlive its stack state or caller buffer. Short reads return EOF.
- **S10:** export lookup uses the name ordinal to index the EAT, bounds tables,
names and resolved RVAs, and rejects forwarders. A valid loader-mapped module
remains a precondition because the API has no externally supplied image size.
- **S11–S12:** corrected policy pointer typedef and publisher return/ownership docs.
- **S14:** policy cleanup before embedded/catalog/reload retries, with host tests
allocating results even on failures. Real CI failure ownership still needs
the VM tests below.
- **S17:** legacy hash algorithm/length combinations validated and calculation
versus verification capabilities documented.
- **S18:** byte-count SAL, buffer-capacity rules, scalar input annotations, and
kernel read context/APC requirements documented.
## Implemented within an explicit support boundary
- **S07:** new bounded DER discovery reads the first outer SPC file-digest
AlgorithmIdentifier; new explicit digest API needs no discovery. The old
heuristic entry point retains compatibility behavior. Arbitrary BER, nested
signatures, and full signature enumeration remain outside this parser.
- **S08:** unsigned/catalog-only files can be hashed with a caller-chosen
SHA-1 or SHA-256 strategy. The policy-explicit legacy helper forwards catalog
controls and reports the successful source. Real catalog lookup comparisons
across OS versions are pending; no implicit algorithm retry policy is imposed.
- **S09:** the explicit digest path enforces a contiguous raw-section subset and
EOF certificate tables, with Windows SIP and real embedded-digest comparisons.
Gaps, overlaps, missing directory slots and nonterminal certificates are
rejected. The old linear compatibility entry point is not a universal hasher.
- **S13/S15/S16:** recovered fields, parameter meanings, policy categories, sizes,
name encoding, borrowed lifetimes, and allocator ownership are documented.
The opt-in x64 26100 header provides distinct observed layouts and assertions.
Default declarations retain source compatibility; later chain fields require
a confirmed runtime layout. The sampled 1507 chain header is 56 bytes and has
no attributes descriptor at +0x38; 1511 uses 72 bytes; 1607–1803 samples use 104.
Distinct views are available in `Domito.MinCrypt.Historical.h`. Neither
`NTDDI_VERSION` nor total allocation Size identifies the layout.
## Requires the test VM or additional matching binaries
The first user-run kernel trace is available for Windows 11 22631 x64 with
TESTSIGNING enabled. It confirms the BthPS3 signer-selection comparison and
primary-signature CI path below. That one run, extracted artifacts, host mocks
and compilation do not close the remaining work:
1. Run the paired `CiValidateFileObject` flag `0`/`0x20` probe on 19041/19045
with TESTSIGNING off and a loadable signed test driver. Preserve complete
statuses, policies and fixture/environment identities (issue #3).
2. Complete historical and ARM64 ABI/structure verification and runtime tests.
The [published export matrix](ci-api-compatibility.md) now covers all ten
named APIs across 20 exact Windows 10/11 x64 and ARM64 CI files.
ISO coverage includes Windows 10 1809/1903 revisions, 1909, 21H2/22H2 ARM64,
and Windows 11 23H2 x64 plus 22H2/24H2 ARM64.
The ARM64 observations establish export presence; their structure layouts
and runtime behavior remain unverified. This closes the initial export-presence
matrix; additional releases, servicing revisions and architectures remain
unsampled. It does not certify ABI or runtime behavior. The six 1507–1803
sampled bodies establish the
48-byte policy storage and 64-byte hash-capacity gate. Chain constructors
establish 56/72/104-byte headers; opt-in views prevent treating 1507 payload
as the later attributes descriptor. Unsampled layouts/runtime remain open.
3. Test real CI policy ownership on every failure/retry path under Driver
Verifier, including allocation failures and initialized outputs.
4. Compare real embedded/catalog-only SHA-1/SHA-256 results, catalog cache/reload
behavior, trust-mask choices, requested signing levels and flag combinations.
5. Measure signing-time propagation, zero/sentinel values, timestamp expiry,
revocation freshness/network policy and weak-crypto policy. Record OS policy,
Secure Boot/HVCI/WDAC and exact files, not just a boolean result.
6. Extend the observed multiple/nested selection result beyond the first run.
On Windows 11 22631 x64 with TESTSIGNING enabled, `0x20` returns Microsoft;
`0xA0` and direct primary SHA-256 `CiCheckSignedFile` return Nefarius with the
same signing time and matching signer/timestamp policy observations. All
calls and policy statuses succeed, the file snapshots match, and no trace
events are lost. TESTSIGNING-off, general enumeration, full `0x80` semantics
and cross-build/policy behavior remain open. Extend supported PE gaps/overlays
only after differential evidence.
7. Resolve page-hash allocation destruction, normalized-entry/truncation meaning,
terminal-entry handling and cross-version layouts before publishing a general
page-hash API. `CiFreePolicyInfo` is not an established destructor for it.
8. Investigate protected-media/initialization and ordinal-only exports separately
if they are needed. Recovered export names alone are insufficient contracts.
## Checks
Run the six host runners in `tests/README.md`. Kernel compilation also covers
the default modern x64/ARM64 structures, the pre-Windows-10 x64 compile variant,
and 14 assertions in the 26100 opt-in header plus 12 historical-view assertions. Those are build checks,
not a claim that historical or ARM64 CI runtime ABI has been verified.
## Consumer-driven helpers
The [consumer migration review](consumer-helpers.md) separates verified defects,
existing corrections, new helpers and downstream policy. Additions include
handle/path native CI wrappers, explicit SPKI representation/hash helpers,
strict hex conversion, an explicit-layout x64 chain view, optional publisher
resolution with matching cleanup, a referenced-process name API and dedicated
process-name cleanup. The PID path fixes dangling failure output and excessive
process access; module-base lookup clears stale output. WDM/KMDF linking is
covered by the package-relative property sheet. No default publisher/root trust
policy, automatic legacy fallback, or process authorization/cache is introduced.
Four compatibility functions carry MSVC deprecation annotations with explicit
migration messages: heuristic digest-kind discovery, compatibility PE hashing,
implicit-policy legacy validation and PID-based process-name lookup. Native CI
types/functions remain available for low-level callers. The compile-only
deprecation suite checks actual public headers in C/C++, including modern API
controls and expected C4996 diagnostics for each older entry point.
+327
View File
@@ -0,0 +1,327 @@
# Code Integrity and mincrypt contracts
The default header retains the existing structure layouts and calling shapes.
Parameter names and ownership comments now distinguish the known controls from
the verification result. `VerificationStatus` is an `NTSTATUS` with the same
32-bit storage. The additional `Domito.MinCrypt.26100.h` header is an **opt-in
description of ci.dll 10.0.26100.8328 x64**. It neither adds imports nor extends
the default structures on every Windows target.
The evidence is static REA/Ghidra analysis of SHA-256
`c7d23814f3202c51fb23e3ca3d4328855787383cee5d14c4a986a13ec2aeda0b`, with
Microsoft ci.pdb GUID `1CAE574B-CF1F-3564-5F74-49567A128629`, age 1 supplying the
address/name map. The companion CodeIntegrity-RE repository retains the full
records and readable `evidence/` files. The historical corpus below adds
sampled x64 Windows 10 observations; ARM64 and unsampled-build compatibility
remain open. Compile-target `NTDDI_VERSION` is not runtime layout negotiation.
The original modern research and `Domito.MinCrypt.26100.h` use the string
`FileVersion` label **26100.8328**. That same exact binary has numeric
`VS_FIXEDFILEINFO` version **26100.9444**, which the export matrix uses. Its
SHA-256 and recovered contracts are unchanged; these labels identify one file.
## Historical x64 corpus: availability and structure evolution
The [CI API compatibility matrix](ci-api-compatibility.md) lists observed named
exports for 20 exact x64/ARM64 CI files, including the later ISO samples. Consult it
for import availability, and the contracts below for layout and ownership limits.
[NOTES.md](../NOTES.md) lists the exact identities of six sampled Windows 10
files: `10.0.10240.16463`, `10.0.10586.0`, `10.0.14393.2189`, `10.0.15063.0`,
`10.0.16299.125`, and `10.0.17134.1130`. All export `CiValidateFileObject` and
the digest/catalog functions. The global setter is already present in sampled
1703, correcting the old 1709 note. The publisher-name export is absent from
all six. ISO samples additionally show absence in 19041.1 and presence in
19041.1865 and 22000.318 x64; the original 26100 file also exports it. The first
Windows 10 servicing update adding it remains unresolved. See CodeIntegrity-RE
`iso-corpus-index.json`. Header guards do not negotiate runtime import availability.
All six sampled x64 file-object exports transfer 48 bytes into each caller
policy, with the owned chain pointer at `+0x10`; cleanup zeros `0x30` bytes. Their
file-object helpers require hash capacity >=64, return `STATUS_BUFFER_TOO_SMALL`
and required length 64 for a smaller buffer, and consume the third input as a
byte. This supports the stable policy storage prefix on these exact artifacts;
it does not establish all field semantics or every servicing build's behavior.
**The chain header evolves within Windows 10. The default header's
`NTDDI_VERSION >= NTDDI_WIN10` condition cannot safely expose every later field
on Windows 10 1507.** Constructor evidence establishes:
- `10.0.10240.16463` (1507): fixed header 56 bytes (`0x38`), root ID at `+0x34`,
variable payload beginning at `+0x38`. There is **no AuthenticodeAttributes
header field at `+0x38`**. Accessing the default structure's member on this
layout would interpret variable payload bytes as a blob descriptor.
- `10.0.10586.0` (1511): fixed header 72 bytes (`0x48`), attributes descriptor at
`+0x38`, payload beginning at `+0x48`.
- Sampled 1607/1703/1709/1803: fixed header 104 bytes (`0x68`), root ID still at
`+0x34`, attributes at `+0x38`, payload beginning at `+0x68`. Constructor evidence
establishes space for the 32-byte extension at `+0x48`, not its semantic
contents on every build. The historical view leaves this region reserved.
`Domito.MinCrypt.Historical.h` supplies distinct opt-in x64 views for those
observed profiles with 12 compile-time assertions. The common prefix ends after
root ID at `+0x34`; chain elements use 120-byte strides in these constructors,
but the prefix views expose their storage as opaque. Do not assume every element
field is populated on every version. The 26100-specific header separately names
its recovered binary-ID field.
Choose a view only after establishing the runtime artifact/layout. `Size` is
**total allocation bytes including payload**: a 1507 allocation can be larger
than 72 or 104 bytes, so `Size >= sizeof(later_header)` is not version detection.
Once layout identity is established, a size check is an additional field-read
bound, not a substitute for identity. The default declarations remain unchanged
for source compatibility; their later fields require this explicit runtime scope.
Successful exported/helper records in CodeIntegrity-RE retain the evidence:
- `historical/1507-export-contracts.json`:
`ev_489c3b57057b0bc5c86950d7bb4ea25108bbc7762abc363e1be7eb5cca034796`.
- `historical/ci-10240-helpers.json`:
`ev_6d602c57de9614e7c5c28b9532c139f750f0e014b151000ecb41ad36f54bf764`.
- `historical/ci-10586-exports.json`:
`ev_a09e04976cfd90e9ba69cc6618892f15c3da166c64fa9241efae0db85234fd6e`.
- `historical/ci-10586-helpers.json`:
`ev_3403d5b8bf7c322fd924880d4cd9b296b98e94238cac2e1c1e9605c4b4d27255`.
- `historical/ci-14393-contracts.json`:
`ev_e63a4c4b7613e231795051b784b9571605b9bf81258b5dcd04d32ddc681abb41`.
- `historical/ci-15063-contracts.json`:
`ev_338658fb82559fa6daa8ae5487b90fe84b8e408ea7578feed6d8dff5d590e114`.
- `historical/ci-16299-contracts.json`:
`ev_fe5731175f0726f0d46af2f78ca615d284f8598939296815e26b5589db3001cf`.
- `historical/ci-17134-contracts.json`:
`ev_0c3e3359b8b7486318006afa6b95eb2257a18abeec9041a86a32db487bcbe996`.
## Results and trust policy
Check the API's returned `NTSTATUS` first. `PolicyInfo.VerificationStatus`,
`PolicyBits`, signer details, and timestamps supply additional observations.
Successful chain processing does not imply that the caller's file digest matched
the signed digest; the comparison can occur afterward. Diagnostic policy data
may exist on failure. An extracted publisher name is display information and
does not establish publisher identity or signer trust.
Define the policy the application needs: embedded signature validation,
membership in an accepted installed catalog, an expected publisher/key, or a
Windows signing scenario. Preserve which path produced a result. A nonzero
`PolicyBits` value is not automatically failure, and any one root bit is not a
complete trust decision. File bytes and object identity must remain tied to the
bytes the application will subsequently use.
The [file-object flag document](ci-validate-file-object.md) covers `0x20`, hash
capacity, signing level, and its pending VM experiment. Other observed flag
effects on 26100.8328 x64 are:
- `0x1` selects hash scenario 5.
- Otherwise `0x2` selects scenario 4 for signing levels <=4; higher levels use
the level mapping.
- Otherwise `0x80000000` selects scenario 5 for levels <=4; higher levels use
the level mapping. It takes precedence over `0x4`.
- Otherwise `0x4` selects scenario 1; without these choices the signing level
maps to a scenario.
- `0x00400000` suppresses accepted-root bit `0x8` when the relevant helper mode
is enabled; CI can add this flag internally. Bits `0x2` and `0x8` also affect
later signing-level/EKU restrictions.
The scenarios resolve through initialized signing-policy state. Static defaults
are not a guarantee of a running system's algorithm, and the flags should not
be named as universal fixed SHA selectors. Unknown bits remain unknown. Evidence:
`CipGetHashAlgorithmForLegacyScenario`, `CipGetAcceptedRootKeys`, and
`CipMinCryptToSigningLevel` in the research bundle.
## Ownership and initialization
Zero-initialize both `MINCRYPT_POLICY_INFO` output objects before their first
use. Free both with `CiFreePolicyInfo` after use and before reuse, including
failure paths. Never call the destructor on arbitrary uninitialized stack data.
Do not clear a nonzero `Size` while a chain is still owned: the observed cleanup
helper skips an object whose `Size` is zero.
`ChainInfo` owns one allocation. Public-key blobs, names, encoded certificates,
EKUs, attributes, and chain elements are borrowed views into it. Do not free
children separately or retain them after releasing the owner. Copying the policy
structure with `memcpy` creates two aliases to one allocation, not independent
ownership. CI's internal deep-copy helper uses the total allocation size and
rebases interior pointers; Domito does not expose that helper.
The default `CiFreePolicyInfo` declaration retains its legacy `PVOID` return to
avoid a broad source compatibility change. **Ignore that value**: observed x64
assembly provides no defined pointer result. The opt-in build-specific function
pointer type uses `VOID`.
`CatalogName` is a separate `RtlDuplicateUnicodeString` allocation in the observed
catalog export. Initialize it empty, release a returned buffer with
`RtlFreeUnicodeString`, and release both policies separately. Publisher-name
storage is allocated through the caller's callback and must be released with
that allocator's matching free operation, including an allocated buffer on a
later conversion failure. It is not owned by either policy object.
Evidence: `MincryptFreePolicyInfo`, `MincryptCopyPolicyInfo`,
`I_MincryptAddChainInfo`, `CiVerifyHashInCatalog`, and `CiGetCertPublisherName`.
The additive [validation helpers](ci-validation-helpers.md) package safe result
ownership, explicit file-object selection and embedded CI verification. They do
not change raw CI declarations or existing legacy-helper behavior.
## Multiple signatures and selected results
`CiValidateFileObject` returns one selected signature result and its timestamp
policy. Those two policy outputs do not enumerate two file signatures. The
output file digest and algorithm are not expected-certificate selectors.
REA tracing of x64 CI 22621.2792 and the original 26100 sample found a
flag-dependent distinction: ordinary `0x20` can continue nested-signature
verification and retain a policy-preferred signing level. Adding `0x80` enters
an early-exit path after the first successful full candidate verification.
`0xA0` is therefore a concrete kernel experiment, not a proved stable
"select primary signature" flag: `0x80` also affects other policy/fallback paths.
An arbitrary signer index, expected-certificate predicate and continuation
interface are not exposed by these examined wrappers.
For the supplied BthPS3 fixture, host SignTool verifies a primary Nefarius
SHA-256 signature and a nested Microsoft SHA-256 signature. On both examined CI
builds, `CiCheckSignedFile` with the matching SHA-256 digest follows the primary
signature directly. This gives a CI-based alternative worth testing without
implementing RSA/ECDSA, chain or timestamp verification. A subsequent user-run
probe on Windows 11 22631 x64 with TESTSIGNING enabled confirms that `0x20`
returns Microsoft while `0xA0` and direct SHA-256 `CiCheckSignedFile` return the
Nefarius leaf and its signing time. Every top-level and signer/timestamp policy
status succeeds; the two company-signer routes have matching policy observations.
The companion `signature-selection/runtime-22631-bthps3.json` retains exact
fixture/trace identities. This does not establish TESTSIGNING-off or general
cross-build behavior. A different layout may still require bounded signature
framing/enumeration.
The companion CodeIntegrity-RE `SIGNATURE-SELECTION.md` retains fixture hashes,
56 procedure bodies, assembly, flag side effects and the kernel test plan.
The 23H2 batch is
`ev_049bd7d3aa492a9e3e4d8fcbf365ae00a6057751b26f93f3c1663c9436952ff7`;
its primary-selection helper is in
`ev_c867c73ca7eae7e4fbca2f44c6f6fb5904fb00459d0b84f2da8294b5b05a3866`.
## Digest-based verification inputs
`CiCheckSignedFile` consumes a caller-computed file digest. It does not reopen
the file or compute its Authenticode hash. On the observed build it accepts
SHA-1/256/384/512 with exactly 20/32/48/64 bytes respectively. A wrong algorithm,
length, or digest fails the final comparison.
Its signature input points to a `WIN_CERTIFICATE` header. The observed wrapper
requires 4-byte pointer alignment, supplied size >=12, revision `0x200`, type 2,
and `8 < dwLength <= supplied size`. The PKCS#7 payload begins at header+8.
The wrapper selects the first certificate-table entry. Multiple table entries
and nested signatures are separate concepts; this is not a complete signature
enumeration interface.
These exports are present in all six sampled Windows 10 x64 files and the
analyzed Windows 11 file. The older header's “Win7–8.1 only” comments described
a workflow, not verified export availability. Earlier first-available builds
and pre/post-KB3033929 ABI changes still require matching Windows 7/8 artifacts.
Import-library names and stdcall stack-byte counts alone do not settle semantics.
## Catalog controls
`ReloadCatalogs` controls catalog-store reload and related context setup.
`RequireTrustedCatalog` is a nonzero gate requiring an observed catalog extension
marker: its matching bytes include `PE`, integer `0x10001`, and `TRUSTED`.
The marker is an additional catalog condition, not proof of a Microsoft signer.
`AcceptedCatalogPolicyBits` accepts a candidate when
`(RequestedMask & Catalog.PolicyInfo.PolicyBits) != 0`: **any overlap** is enough.
It neither enables policy flags nor requires every requested bit. The existing
Domito compatibility value is named `DOMITO_CI_LEGACY_CATALOG_POLICY_BITS`
(`0x2007F`). It includes test/DMD categories and diagnostic bit `0x20000`, while
omitting other categories. It is retained as an existing behavior, not promoted
as a universally safe trust mask. Establish and test the application's accepted
signer policy before changing it.
Observed file-hash lookup supports SHA-1 and SHA-256; the internal path has no
SHA-384/512 file-hash case. Embedded-signature algorithm support and catalog
lookup support are different capabilities. Catalog-only files need an explicit,
tested algorithm strategy rather than assuming SHA-1 from an absent embedded
table. Catalog stores, caches, silo/process context, and signing policy affect
results. The new `DomitoValidateFileLegacyModeEx` forwards a caller-selected mask and
marker gate and reports embedded/catalog origin. Its compatibility wrapper retains
the existing mask. See [digest and workflow details](authenticode-digests.md).
Domito's older-Windows helper keeps its system-process attachment until
historical runtime evidence supports changing it.
Evidence: `I_FindFileOrHeaderHashInLoadedCatalogs` and its assembly
`ev_c42e7ee76099d55e7cf7cdd3d9f79e99030ba637680365612afb4906694ae3a8`,
`I_ParseCatalog`, and marker matcher
`ev_a1d3a5844a2b6be6c16175e307956c9819d0b64a4fe70216f292c12e4ea38d82`.
## Opt-in x64 26100 structure observations
The version header has distinct type names and 14 compile-time layout assertions.
Use it only after independently establishing the matching runtime build and
architecture. A large `ChainInfo->Size` alone does not identify the header: it
includes variable trailing data, so an older allocation could also be large.
Only after layout identity is established may a size check protect a field read.
- `DOMITO_CI26100_NAME_BLOB`: pointer at +0, byte length at +8, ASN.1 string tag
at +10. Names are not necessarily NUL-terminated. Observed conversion handles
UTF8String tag `0x0C` and BMPString `0x1E`; `PCHAR` in the default declaration
must not be read as a guarantee of ANSI encoding.
- `DOMITO_CI26100_CHAIN_INFO`: `Size` is total allocation bytes; fixed header is
104 bytes. Root ID at +0x34 is an opaque integer, not policy bits. The 32 bytes
at +0x48 are a platform-manifest binary ID from an authenticated attribute,
not a file digest or certificate thumbprint. Zero/nonzero does not by itself
prove trust. Require the matching layout and `Size >=104` before reading it.
- `DOMITO_CI26100_POLICY_INFO`: 48 bytes; `NotBefore`/`NotAfter` come from the
signer certificate validity fields, not an intersection of the whole chain.
Interpret `RevocationTime` only with the relevant result. A zero signing time
is not a real timestamp to display as a signing date.
Construction/copy evidence: `ev_d7110cfc711074a4af1874b5c8e701c13bcc3dc58a1596c64dca4ca98966c79e`,
`ev_858afecf2170484325379b6a8bc307fb72dad2f9a5a765169192c7363a3386af`,
`ev_f82cbee977715399323bb398781c125a9d19b4ea29acd05063ec55b711cfc670`.
## Partial policy-bit dictionary
Names in the opt-in header are Domito descriptions of observed 26100 branches,
not official Microsoft constants or a universal trusted-root enumeration.
Root/provenance categories include `0x1` self-issued terminal path, `0x2` selected
built-in Microsoft roots, `0x4` test roots, `0x8` the built-in root associated with
embedded picroot handling, `0x10` tolerated partial/unresolved chain, `0x20` DMD,
`0x40` DMD test, `0x80` another built-in Microsoft root, `0x400` flight root,
`0x2000` third-party marketplace, `0x4000` explicit supplied certificate/key
acceptance, and `0x8000` Microsoft Identity Root 2020. `0x800` and `0x1000` are
additional discriminators on some root entries; their independent meanings
remain unresolved. These categories do not establish a full trust result.
Observed diagnostic branches set `0x20000` chain failure, `0x40000` invalid
cryptographic/signature data, `0x80000` unmet EKU, `0x100000` page-hash failure,
`0x200000` revocation, `0x400000` certificate validity failure (including
not-yet-valid), `0x08000000` path-length constraints, `0x10000000` generic fallback,
and `0x20000000` CA/BasicConstraints failure. `0x04000000` marks a no-usable-
timestamp branch during revoked-certificate recovery, not universal timestamp
absence. `0x50000` and `0x60000` are observed composites; do not infer a unique
meaning for every constituent bit. Timestamp-before-revocation processing can
clear a revocation bit. Internal checks of high bits are not a replacement for
the API status and application policy.
## Remaining runtime and API coverage
The missing page-hash exports have recovered argument shapes in the research
report. Their normalized allocation uses a 24-byte x64 header and 24-byte entries
even for stronger source algorithms. Field meaning, truncation intent, source
terminal-entry handling, and a supported destructor remain open. `CiFreePolicyInfo`
is not a demonstrated destructor for that allocation. No universal page-hash SDK
or static import is added until those ownership and cross-version contracts can
be tested.
`CiGetPEInformation` is a protected-media dispatcher, not a general PE parser.
`CiInitialize` is initialization machinery. `CiSetTrustedOriginClaimId` writes a
CI global; it is not an ordinary per-file trust selector. Ordinal-only exports
and other initialization interfaces require their own evidence and callers.
Still required: complete sampled-build/ARM64 layout and parameter verification, real CI
output lifetimes and retry behavior under failures, timestamp propagation,
revocation freshness/network policy, weak-crypto selection, multiple/nested
signatures, catalog-only algorithms, and extension of the bounded digest subset to additional PE layouts.
The explicit digest helper now has local Windows SIP and real embedded-digest
comparisons; this does not close those historical or unusual-layout cases. Run the prepared probes in disposable VMs with exact OS,
CI binary, configuration, and fixture identities. Compilation and host mocks do
not close those runtime questions.
+200
View File
@@ -0,0 +1,200 @@
#pragma once
#include "Domito.h"
// Consumer helpers: see docs/consumer-helpers.md for ownership and trust limits.
typedef struct _DOMITO_BYTE_VIEW
{
const UCHAR* Data;
ULONG Size;
} DOMITO_BYTE_VIEW;
typedef struct _DOMITO_CERTIFICATE_SPKI
{
DOMITO_BYTE_VIEW Encoded; // Complete SPKI SEQUENCE, including tag and length.
DOMITO_BYTE_VIEW Content; // Legacy consumer convention: excludes that header.
} DOMITO_CERTIFICATE_SPKI;
typedef enum _DOMITO_CERTIFICATE_HASH_KIND
{
DomitoCertificateHashDer = 0,
DomitoCertificateHashSpkiDer = 1,
DomitoCertificateHashSpkiContentLegacy = 2
} DOMITO_CERTIFICATE_HASH_KIND;
// Bounded positional DER extraction, not X.509 validation. Views borrow CertData.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoGetCertificateSpki(
_In_reads_bytes_(CertSize) const UCHAR* CertData,
_In_ ULONG CertSize,
_Out_ DOMITO_CERTIFICATE_SPKI* Spki
);
// SHA-1/256/384/512. HashSize is capacity on entry, bytes on success, required
// size on BUFFER_TOO_SMALL, zero on other failures. Hash may be NULL to size.
// Input/output must not overlap. Hash contains zero bytes on operational failure.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoHashBuffer(
_In_reads_bytes_opt_(Size) const UCHAR* Bytes,
_In_ ULONG Size,
_In_ ALG_ID Algorithm,
_Out_writes_bytes_to_opt_(*HashSize, *HashSize) UCHAR* Hash,
_Inout_ ULONG* HashSize
);
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoHashCertificate(
_In_reads_bytes_(CertSize) const UCHAR* CertData,
_In_ ULONG CertSize,
_In_ DOMITO_CERTIFICATE_HASH_KIND Kind,
_In_ ALG_ID Algorithm,
_Out_writes_bytes_to_opt_(*HashSize, *HashSize) UCHAR* Hash,
_Inout_ ULONG* HashSize
);
// Strict length-delimited hex: no spaces, prefixes, separators or invalid digits.
// Nonoverlapping buffers. Decode clears its output on malformed input.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoDecodeHex(
_In_ const UNICODE_STRING* Hex,
_Out_writes_bytes_(ByteCount) UCHAR* Bytes,
_In_ ULONG ByteCount
);
// Writes uppercase hex plus NUL; HexChars includes terminator capacity.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoEncodeHex(
_In_reads_bytes_opt_(ByteCount) const UCHAR* Bytes,
_In_ ULONG ByteCount,
_Out_writes_(HexChars) WCHAR* Hex,
_In_ ULONG HexChars
);
// Resolve optional exports without creating a static import. Requires DomitoInit.
// ModuleName is the full loaded-module path used by FindModuleBaseAddress.
// Caller must keep that module mapped; this does not acquire a loader reference.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoFindModuleExportedFunctionAddress(
_In_ PANSI_STRING ModuleName,
_In_ PANSI_STRING FunctionName,
_Outptr_result_maybenull_ PVOID* FunctionAddress
);
// Results from either process-name API use one Domito allocation. The memory
// callback pair must remain unchanged until all allocations are released.
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
void
DomitoFreeProcessImageName(
_In_opt_ PUNICODE_STRING ImageName
);
// Caller holds a referenced PEPROCESS throughout the call. No PID re-lookup.
// Returns a copy owned by Domito; use DomitoFreeProcessImageName once.
// Name lookup does not establish the identity of a later reopened file.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoGetProcessImageNameEx(
_In_ PEPROCESS Process,
_Outptr_result_maybenull_ PUNICODE_STRING* ImageName
);
#if (NTDDI_VERSION >= NTDDI_WIN10)
// These wrappers do not require DomitoInit. Handle must be a trusted kernel
// handle with FILE_READ_DATA; it is borrowed, referenced for the call, not closed.
// Same initialized-result/cleanup rules and explicit flags as FileObjectEx.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoValidateFileHandleEx(
_In_ HANDLE FileHandle,
_In_ UINT32 ValidationFlags,
_In_ UINT8 RequestedSigningLevel,
_In_ DOMITO_CI_SIGNATURE_SELECTION Selection,
_Inout_ DOMITO_CI_VALIDATION_RESULT* Result
);
// Absolute native path (e.g. \??\C:\...). Opens read-only, shares only reads,
// closes its own handle. The path can be reparsed; this validates the opened
// file, not process memory or a previous path identity. No fallback is implicit.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoValidateFilePathEx(
_In_ const UNICODE_STRING* Path,
_In_ UINT32 ValidationFlags,
_In_ UINT8 RequestedSigningLevel,
_In_ DOMITO_CI_SIGNATURE_SELECTION Selection,
_Inout_ DOMITO_CI_VALIDATION_RESULT* Result
);
typedef enum _DOMITO_CI_CHAIN_LAYOUT
{
// Explicit caller assertion: observed x64 +0x28 array/+0x30 count, 120-byte
// elements and a minimum 56-byte header. NOT selected by allocation Size.
DomitoCiChainLayoutObservedX64 = 1
} DOMITO_CI_CHAIN_LAYOUT;
typedef struct _DOMITO_CI_CHAIN_VIEW
{
const MINCRYPT_CHAIN_ELEMENT* Elements;
ULONG Count;
ULONG RootId; // Opaque CI identifier, not proof the last element is a root.
} DOMITO_CI_CHAIN_VIEW;
// Only accepts a readable, initialized CI-owned policy, never arbitrary pointers.
// Checks every exposed name/certificate span against CI's owned allocation.
// Leaf-first order is observed in sampled layouts. Last element is not promised
// to be a self-signed/trusted root. No issuer/subject byte equality is imposed.
// View borrows policy storage until CiFreePolicyInfo/result reuse. x64 only.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoGetCiChainView(
_In_ const MINCRYPT_POLICY_INFO* Policy,
_In_ DOMITO_CI_CHAIN_LAYOUT Layout,
_Out_ DOMITO_CI_CHAIN_VIEW* View
);
// Optional export, resolved at call time; requires DomitoInit. No static import
// of CiGetCertPublisherName. Extracts a display name, not certificate trust.
// Start with {0}; release even after failure using the dedicated free function.
// Never pass an owning previous string: free it before making another call.
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoGetCertificatePublisherName(
_In_ const MINCERT_BLOB* Certificate,
_Out_ PUNICODE_STRING PublisherName
);
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
void
DomitoFreeCertificatePublisherName(
_Inout_ PUNICODE_STRING PublisherName
);
#endif
+212
View File
@@ -0,0 +1,212 @@
/* Opt-in observed-build declarations, not a universal Windows CI SDK.
* Observed: ci.dll 10.0.26100.8328, x64, SHA-256 documented in docs/mincrypt-contracts.md.
* Include after ntddk.h. Uses distinct names and function-pointer types so
* including this file does not change the default layouts or add static imports.
* Verify the actual CI build/architecture before use; Size alone cannot prove
* layout identity. See docs/mincrypt-contracts.md for provenance and limits.
* Field/parameter names are descriptive analyst names unless stated otherwise.
*/
#pragma once
#if !defined(_M_AMD64) && !defined(__x86_64__)
#error This research layout is validated only for x64.
#endif
#pragma pack(push, 8)
typedef struct _DOMITO_CI26100_BLOB
{
ULONG Size;
UCHAR* Data;
} DOMITO_CI26100_BLOB;
typedef struct _DOMITO_CI26100_NAME_BLOB
{
UCHAR* Data; /* Borrowed, length-delimited encoded bytes. */
USHORT Size; /* Bytes, not characters. */
UCHAR Asn1StringTag; /* Observed: 0x0c UTF8String, 0x1e BMPString. */
} DOMITO_CI26100_NAME_BLOB;
typedef struct _DOMITO_CI26100_CHAIN_ELEMENT
{
ULONG HashAlgId;
ULONG HashSize;
UCHAR Hash[64];
DOMITO_CI26100_NAME_BLOB Subject;
DOMITO_CI26100_NAME_BLOB Issuer;
DOMITO_CI26100_BLOB Certificate;
} DOMITO_CI26100_CHAIN_ELEMENT;
typedef struct _DOMITO_CI26100_CHAIN_INFO
{
ULONG Size; /* Entire allocation, including trailing data. */
DOMITO_CI26100_BLOB* PublicKeys;
ULONG NumberOfPublicKeys;
DOMITO_CI26100_BLOB* EKUs;
ULONG NumberOfEKUs;
DOMITO_CI26100_CHAIN_ELEMENT* ChainElements;
ULONG NumberOfChainElements;
ULONG RootId; /* Opaque classification, NOT policy bits. */
DOMITO_CI26100_BLOB AuthenticodeAttributes;
UCHAR PlatformManifestBinaryId[32]; /* NOT a file or certificate hash. */
} DOMITO_CI26100_CHAIN_INFO;
typedef struct _DOMITO_CI26100_POLICY_INFO
{
ULONG Size;
NTSTATUS VerificationStatus; /* Does not replace the API return status. */
ULONG PolicyBits;
DOMITO_CI26100_CHAIN_INFO* ChainInfo;
LARGE_INTEGER RevocationTime;
LARGE_INTEGER NotBefore;
LARGE_INTEGER NotAfter;
} DOMITO_CI26100_POLICY_INFO, *PDOMITO_CI26100_POLICY_INFO;
#pragma pack(pop)
C_ASSERT(sizeof(DOMITO_CI26100_BLOB) == 16);
C_ASSERT(sizeof(DOMITO_CI26100_NAME_BLOB) == 16);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_NAME_BLOB,
Asn1StringTag
) == 10
);
C_ASSERT(sizeof(DOMITO_CI26100_CHAIN_ELEMENT) == 120);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_CHAIN_ELEMENT,
Certificate
) == 104
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_CHAIN_INFO,
RootId
) == 52
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_CHAIN_INFO,
AuthenticodeAttributes
) == 56
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_CHAIN_INFO,
PlatformManifestBinaryId
) == 72
);
C_ASSERT(sizeof(DOMITO_CI26100_CHAIN_INFO) == 104);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_POLICY_INFO,
ChainInfo
) == 16
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_POLICY_INFO,
RevocationTime
) == 24
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_POLICY_INFO,
NotBefore
) == 32
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI26100_POLICY_INFO,
NotAfter
) == 40
);
C_ASSERT(sizeof(DOMITO_CI26100_POLICY_INFO) == 48);
/* Zero-initialize policies before first use. Free existing owned chains before
* reuse. Never copy these structs as independently owned objects with memcpy.
* Size is an output/initialization marker here, not runtime ABI negotiation.
*/
typedef VOID(NTAPI* DOMITO_CI26100_FREE_POLICY_INFO)(DOMITO_CI26100_POLICY_INFO* PolicyInfo);
typedef NTSTATUS(NTAPI* DOMITO_CI26100_VALIDATE_FILE_OBJECT)(
PFILE_OBJECT FileObject,
ULONG ValidationFlags,
UCHAR RequestedSigningLevel,
DOMITO_CI26100_POLICY_INFO* PolicyInfo,
DOMITO_CI26100_POLICY_INFO* TimeStampPolicyInfo,
LARGE_INTEGER* SigningTime,
UCHAR* Hash, /* Provide 64-byte capacity, even for SHA-1. */
ULONG* HashSize, /* In: capacity >=64. Out success: actual size. */
ULONG* HashAlgId
);
typedef NTSTATUS(NTAPI* DOMITO_CI26100_CHECK_SIGNED_FILE)(
PVOID Hash,
ULONG HashSize,
ULONG HashAlgId,
PVOID WinCertificate, /* WIN_CERTIFICATE header, not bCertificate. */
ULONG SizeOfSecurityDirectory,
DOMITO_CI26100_POLICY_INFO* PolicyInfo,
LARGE_INTEGER* SigningTime,
DOMITO_CI26100_POLICY_INFO* TimeStampPolicyInfo
);
/* The existing BOOLEAN reload parameter ABI was not independently resolved
* beyond the full-width use in this build; retain Domito's declaration until
* cross-architecture call-site evidence settles its formal type.
* Fifth argument nonzero filters on the catalog TRUSTED marker.
* Sixth argument requires ANY overlap with catalog PolicyInfo.PolicyBits.
*/
typedef NTSTATUS(NTAPI* DOMITO_CI26100_VERIFY_HASH_IN_CATALOG)(
PVOID Hash,
ULONG HashSize,
ULONG HashAlgId,
BOOLEAN ReloadCatalogs,
ULONG RequireTrustedCatalog,
ULONG AcceptedCatalogPolicyBits,
DOMITO_CI26100_POLICY_INFO* PolicyInfo,
UNICODE_STRING* CatalogName, /* Optional; separate RtlFreeUnicodeString. */
LARGE_INTEGER* SigningTime,
DOMITO_CI26100_POLICY_INFO* TimeStampPolicyInfo
);
typedef PVOID(NTAPI* DOMITO_CI26100_ALLOCATE)(SIZE_T ByteSize);
typedef NTSTATUS(NTAPI* DOMITO_CI26100_GET_CERT_PUBLISHER_NAME)(
DOMITO_CI26100_BLOB* Certificate,
DOMITO_CI26100_ALLOCATE AllocateRoutine,
UNICODE_STRING* PublisherName
); /* Free with callback's allocator pair. */
/* Proposed descriptive diagnostic names. Not official Microsoft names, not
* an exhaustive dictionary, and not accepted-input validation flags.
* See the report for narrower branch conditions and composite values.
*/
#define DOMITO_CI26100_POLICY_SELF_ISSUED_TERMINAL 0x00000001UL
// Also present on rgbMicrosoftRoot4 together with bit 0x800.
#define DOMITO_CI26100_POLICY_MICROSOFT_ROOT_0_1_2 0x00000002UL
#define DOMITO_CI26100_POLICY_TEST_ROOT 0x00000004UL
#define DOMITO_CI26100_POLICY_MICROSOFT_ROOT_3 0x00000008UL
#define DOMITO_CI26100_POLICY_DMD_ROOT 0x00000020UL
#define DOMITO_CI26100_POLICY_DMD_TEST_ROOT 0x00000040UL
#define DOMITO_CI26100_POLICY_MICROSOFT_ROOT_5 0x00000080UL
#define DOMITO_CI26100_POLICY_FLIGHT_ROOT 0x00000400UL
#define DOMITO_CI26100_POLICY_MARKETPLACE_ROOT 0x00002000UL
#define DOMITO_CI26100_POLICY_SUPPLIED_CERT_OR_KEY 0x00004000UL
#define DOMITO_CI26100_POLICY_IDENTITY_ROOT_2020 0x00008000UL
#define DOMITO_CI26100_POLICY_PARTIAL_CHAIN 0x00000010UL
#define DOMITO_CI26100_POLICY_CHAIN_FAILURE 0x00020000UL
#define DOMITO_CI26100_POLICY_CRYPTO_FAILURE 0x00040000UL
#define DOMITO_CI26100_POLICY_EKU_FAILURE 0x00080000UL
#define DOMITO_CI26100_POLICY_PAGE_HASH_FAILURE 0x00100000UL
#define DOMITO_CI26100_POLICY_CERT_REVOKED 0x00200000UL
#define DOMITO_CI26100_POLICY_CERT_VALIDITY_FAILURE 0x00400000UL
#define DOMITO_CI26100_POLICY_PATH_LENGTH_FAILURE 0x08000000UL
#define DOMITO_CI26100_POLICY_GENERIC_FAILURE 0x10000000UL
#define DOMITO_CI26100_POLICY_CA_CONSTRAINT_FAILURE 0x20000000UL
/* Narrow branch observation: no usable timestamp during revoked-cert recovery;
* not a universal flag for all signatures without a timestamp.
*/
#define DOMITO_CI26100_POLICY_REVOCATION_NO_TIMESTAMP 0x04000000UL
+120
View File
@@ -0,0 +1,120 @@
/* Opt-in x64 chain views for exact observed Windows 10 CI artifacts.
* Include after ntddk.h. This does not alter Domito's default declarations.
* Select a view only after establishing the matching runtime layout from
* build/artifact evidence. ChainInfo->Size is TOTAL allocation size; it cannot
* identify the header version, because variable payload follows the header.
* See docs/mincrypt-contracts.md and NOTES.md for exact sampled versions/hashes.
*/
#pragma once
#include "Domito.MinCrypt.h"
#if !defined(_M_AMD64) && !defined(__x86_64__)
#error Historical CI views are validated only for the documented x64 artifacts.
#endif
#pragma pack(push, 8)
// Observed ci.dll 10.0.10240.16463. The variable payload begins at +0x38;
// AuthenticodeAttributes does not exist at that offset in this header.
typedef struct _DOMITO_CI10240_CHAIN_INFO
{
UINT32 Size;
MINCERT_BLOB* PublicKeys;
UINT32 NumberOfPublicKeys;
MINCERT_BLOB* EKUs;
UINT32 NumberOfEKUs;
PVOID ChainElements; // Constructor copies 120-byte elements; borrowed storage.
UINT32 NumberOfChainElements;
UINT32 RootId; // Opaque identifier, not PolicyBits.
} DOMITO_CI10240_CHAIN_INFO, *PDOMITO_CI10240_CHAIN_INFO;
// Observed ci.dll 10.0.10586.0. The variable payload begins at +0x48.
typedef struct _DOMITO_CI10586_CHAIN_INFO
{
UINT32 Size;
MINCERT_BLOB* PublicKeys;
UINT32 NumberOfPublicKeys;
MINCERT_BLOB* EKUs;
UINT32 NumberOfEKUs;
PVOID ChainElements;
UINT32 NumberOfChainElements;
UINT32 RootId;
MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
} DOMITO_CI10586_CHAIN_INFO, *PDOMITO_CI10586_CHAIN_INFO;
// Observed 10.0.14393.2189, 15063.0, 16299.125 and 17134.1130 x64.
// The variable payload begins at +0x68. Constructor evidence establishes the
// 32-byte extension's space, not its semantic contents on every sampled build.
typedef struct _DOMITO_CI14393_CHAIN_INFO
{
UINT32 Size;
MINCERT_BLOB* PublicKeys;
UINT32 NumberOfPublicKeys;
MINCERT_BLOB* EKUs;
UINT32 NumberOfEKUs;
PVOID ChainElements;
UINT32 NumberOfChainElements;
UINT32 RootId;
MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
UINT8 ReservedExtension[32];
} DOMITO_CI14393_CHAIN_INFO, *PDOMITO_CI14393_CHAIN_INFO;
#pragma pack(pop)
C_ASSERT(sizeof(DOMITO_CI10240_CHAIN_INFO) == 56);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI10240_CHAIN_INFO,
PublicKeys
) == 8
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI10240_CHAIN_INFO,
EKUs
) == 24
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI10240_CHAIN_INFO,
ChainElements
) == 40
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI10240_CHAIN_INFO,
RootId
) == 52
);
C_ASSERT(sizeof(DOMITO_CI10586_CHAIN_INFO) == 72);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI10586_CHAIN_INFO,
RootId
) == 52
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI10586_CHAIN_INFO,
AuthenticodeAttributes
) == 56
);
C_ASSERT(sizeof(DOMITO_CI14393_CHAIN_INFO) == 104);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI14393_CHAIN_INFO,
RootId
) == 52
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI14393_CHAIN_INFO,
AuthenticodeAttributes
) == 56
);
C_ASSERT(
FIELD_OFFSET(
DOMITO_CI14393_CHAIN_INFO,
ReservedExtension
) == 72
);
+129 -76
View File
@@ -259,24 +259,20 @@ typedef unsigned int ALG_ID;
#endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1) #endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1)
#include <pshpack8.h> #include <pshpack8.h>
typedef struct _MINCRYPTOAPI_BLOB typedef struct _MINCRYPTOAPI_BLOB
{ {
UINT32 Size; UINT32 Size;
_Field_size_bytes_(Size) UINT8* Data; _Field_size_bytes_(Size) UINT8* Data;
} MINCRYPT_INTEGER_BLOB, * PMINCRYPT_INTEGER_BLOB, } MINCRYPT_INTEGER_BLOB, *PMINCRYPT_INTEGER_BLOB, MINCRYPT_UINT_BLOB, *PMINCRYPT_UINT_BLOB, MINCRYPT_OBJID_BLOB,
MINCRYPT_UINT_BLOB, * PMINCRYPT_UINT_BLOB, *PMINCRYPT_OBJID_BLOB, MINCERT_NAME_BLOB, *PMINCERT_NAME_BLOB, MINCERT_RDN_VALUE_BLOB, *PMINCERT_RDN_VALUE_BLOB,
MINCRYPT_OBJID_BLOB, * PMINCRYPT_OBJID_BLOB, MINCERT_BLOB, *PMINCERT_BLOB, MINCRL_BLOB, *PMINCRL_BLOB, MINCRYPT_DATA_BLOB, *PMINCRYPT_DATA_BLOB,
MINCERT_NAME_BLOB, * PMINCERT_NAME_BLOB, MINCRYPT_HASH_BLOB, *PMINCRYPT_HASH_BLOB, MINCRYPT_DIGEST_BLOB, *PMINCRYPT_DIGEST_BLOB, MINCRYPT_DER_BLOB,
MINCERT_RDN_VALUE_BLOB, * PMINCERT_RDN_VALUE_BLOB, *PMINCRYPT_DER_BLOB, MINCRYPT_ATTR_BLOB, *PMINCRYPT_ATTR_BLOB;
MINCERT_BLOB, * PMINCERT_BLOB,
MINCRL_BLOB, * PMINCRL_BLOB,
MINCRYPT_DATA_BLOB, * PMINCRYPT_DATA_BLOB,
MINCRYPT_HASH_BLOB, * PMINCRYPT_HASH_BLOB,
MINCRYPT_DIGEST_BLOB, * PMINCRYPT_DIGEST_BLOB,
MINCRYPT_DER_BLOB, * PMINCRYPT_DER_BLOB,
MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB;
// Encoded, length-delimited name bytes; not necessarily ANSI or NUL-terminated.
// The observed 26100 x64 string tag occupies padding; see the opt-in version header.
typedef struct _MINCRYPT_NAME_BLOB typedef struct _MINCRYPT_NAME_BLOB
{ {
_Field_size_bytes_(Size) PCHAR Data; _Field_size_bytes_(Size) PCHAR Data;
@@ -287,6 +283,8 @@ typedef struct _MINCRYPT_NAME_BLOB
#define MINCRYPT_MAX_HASH_LENGTH (64) #define MINCRYPT_MAX_HASH_LENGTH (64)
#define MINCRYPT_SHA1_LENGTH (160 / 8) #define MINCRYPT_SHA1_LENGTH (160 / 8)
#define MINCRYPT_SHA256_LENGTH (256 / 8) #define MINCRYPT_SHA256_LENGTH (256 / 8)
#define MINCRYPT_SHA384_LENGTH (384 / 8)
#define MINCRYPT_SHA512_LENGTH (512 / 8)
typedef struct _MINCRYPT_CHAIN_ELEMENT typedef struct _MINCRYPT_CHAIN_ELEMENT
{ {
@@ -301,9 +299,11 @@ typedef struct _MINCRYPT_CHAIN_ELEMENT
} MINCRYPT_CHAIN_ELEMENT, *PMINCRYPT_CHAIN_ELEMENT; } MINCRYPT_CHAIN_ELEMENT, *PMINCRYPT_CHAIN_ELEMENT;
// CI owns one chain allocation; nested pointers borrow that allocation.
// Do not free child blobs separately or use memcpy to create a second owner.
typedef struct _MINCRYPT_CHAIN_INFO typedef struct _MINCRYPT_CHAIN_INFO
{ {
UINT32 Size; UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data.
MINCERT_BLOB* PublicKeys; MINCERT_BLOB* PublicKeys;
UINT32 NumberOfPublicKeys; UINT32 NumberOfPublicKeys;
@@ -316,10 +316,17 @@ typedef struct _MINCRYPT_CHAIN_INFO
PMINCRYPT_CHAIN_ELEMENT ChainElements; PMINCRYPT_CHAIN_ELEMENT ChainElements;
UINT32 NumberOfChainElement; UINT32 NumberOfChainElement;
// ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc. // REQUIRES a confirmed runtime layout (10586 and later sampled profiles).
// Sampled 10240 has only 56 header bytes: +0x38 begins variable payload.
// NTDDI_VERSION and allocation Size cannot establish field availability.
// See Domito.MinCrypt.Historical.h for distinct observed x64 prefix views.
MINCRYPT_ATTR_BLOB AuthenticodeAttributes; MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
// UINT8 Hash[32]; // RootId occupies padding at +0x34 in all six historical x64 samples and
// 26100. The historical views name it without extending this default layout.
// 26100's 32-byte platform-manifest binary ID at +0x48 is named only in its
// explicit version header. Earlier 104-byte views reserve that extension.
// Do not infer field availability solely from NTDDI_VERSION or Size.
#endif // NTDDI_VERSION >= NTDDI_WIN10 #endif // NTDDI_VERSION >= NTDDI_WIN10
@@ -338,7 +345,7 @@ typedef struct _MINCRYPT_CHAIN_INFO
typedef struct _MINCRYPT_POLICY_INFO typedef struct _MINCRYPT_POLICY_INFO
{ {
UINT32 Size; UINT32 Size;
UINT32 VerificationStatus; NTSTATUS VerificationStatus; // Check API return status separately.
UINT32 PolicyBits; UINT32 PolicyBits;
MINCRYPT_CHAIN_INFO* ChainInfo; MINCRYPT_CHAIN_INFO* ChainInfo;
@@ -347,46 +354,52 @@ typedef struct _MINCRYPT_POLICY_INFO
#if (NTDDI_VERSION >= NTDDI_WIN10) #if (NTDDI_VERSION >= NTDDI_WIN10)
LARGE_INTEGER NotBefore; // The certificate is not valid before this time LARGE_INTEGER NotBefore; // Observed: signer certificate validity start.
LARGE_INTEGER NotAfter; // The certificate is not valid after this time LARGE_INTEGER NotAfter; // Observed: signer certificate validity end.
#endif // NTDDI_VERSION >= NTDDI_WIN10 #endif // NTDDI_VERSION >= NTDDI_WIN10
}MINCRYPT_POLICY_INFO, PMINCRYPT_POLICY_INFO; } MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO;
#include <poppack.h> #include <poppack.h>
/** /**
* Resets a PolicyInfo struct - frees the dynamically allocated buffer in PolicyInfo (ChainInfo) if not null. * Release a known initialized policy's owned ChainInfo allocation and reset it.
* Zeros the entire PolicyInfo struct. * Zero-initialize policy objects before first use. Release both signer and TSA
* policies after use, including failures, and before reusing output storage.
* Borrowed child pointers become invalid when the owner is released. Never clear
* Size while a chain is still owned. Size does not negotiate the runtime layout.
* *
* @param PolicyInfo - the struct to reset. * @param PolicyInfo - initialized policy, not arbitrary uninitialized storage.
* *
* @return the struct which was reset. * Ignore the return value. The legacy PVOID declaration is retained for source
* compatibility; observed 26100 x64 code provides no defined pointer return.
*/ */
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI MINCRYPTAPI
PVOID PVOID NTAPI
NTAPI
CiFreePolicyInfo( CiFreePolicyInfo(
_Inout_ MINCRYPT_POLICY_INFO* PolicyInfo _Inout_ MINCRYPT_POLICY_INFO* PolicyInfo
); );
/** /**
* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10! * Digest-based workflow, also implemented on observed Windows 11 build 26100.
* Prefer the file-object workflow when its policy fits. Older-build ABI and
* KB3033929 requirements need a separately verified compatibility matrix.
* *
* Given a file digest and signature of a file, verify the signature and provide information regarding * Given a file digest and signature of a file, verify the signature and provide information regarding
* the certificates that was used for signing (the entire certificate chain) * the certificates that was used for signing (the entire certificate chain)
* *
* @param Hash - buffer containing the digest * @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
* *
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256 * @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
* *
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C) * @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
* *
* @param SecurityDirectory - pointer to the start of the security directory * @param SecurityDirectory - WIN_CERTIFICATE header, not the PKCS#7 payload.
* Observed 26100 x64 checks 4-byte pointer alignment, revision 0x200,
* type 2, supplied size >=12 and 8 < dwLength <= supplied size.
* This wrapper selects the first entry; it does not enumerate the table.
* *
* @param SizeOfSecurityDirectory - size the security directory * @param SizeOfSecurityDirectory - size the security directory
* *
@@ -396,94 +409,109 @@ CiFreePolicyInfo(
* *
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain * @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain
* *
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified. * @return Overall verification NTSTATUS. A successful policy VerificationStatus
* does not replace this return value: the final file-digest comparison
* can still fail. Both policy outputs may carry diagnostics on failure.
* See docs/mincrypt-contracts.md for ownership and policy limits.
* Various error values otherwise, for example: * Various error values otherwise, for example:
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature * STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked * STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired * STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/ */
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI MINCRYPTAPI
NTSTATUS NTSTATUS NTAPI
NTAPI
CiCheckSignedFile( CiCheckSignedFile(
_In_ PVOID Hash, _In_reads_bytes_(HashSize) PVOID Hash,
_In_ UINT32 HashSize, _In_ UINT32 HashSize,
_In_ ALG_ID HashAlgId, _In_ ALG_ID HashAlgId,
_In_ PVOID SecurityDirectory, _In_reads_bytes_(SizeOfSecurityDirectory) PVOID SecurityDirectory,
_In_ UINT32 SizeOfSecurityDirectory, _In_ UINT32 SizeOfSecurityDirectory,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_ LARGE_INTEGER* SigningTime, _Out_ LARGE_INTEGER* SigningTime,
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo
); );
// Existing Domito compatibility filter, not a universal trusted-root policy.
// Includes test/DMD categories and a diagnostic bit; select policy deliberately.
#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
/** /**
* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10! * Digest-based workflow, also implemented on observed Windows 11 build 26100.
* Prefer the file-object workflow when its policy fits. Older-build ABI and
* KB3033929 requirements need a separately verified compatibility matrix.
* *
* Checks if the SHA-1 message digest is contained within a verified system catalog * Finds a caller-computed file digest in an accepted installed catalog.
* Observed 26100 x64 file-hash lookup supports SHA-1 and SHA-256.
* *
* @note must be attached to the PsInitialSystemProcess first! * @note must be attached to the PsInitialSystemProcess first!
* *
* @param Hash - buffer containing the digest * @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
* *
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256 * @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
* *
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C) * @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
* *
* @param IsReloadCatalogs - is reload catalogs cache. * @param ReloadCatalogs - request catalog-store reload; process/silo and cache state matter.
* *
* @param Always0 - this is for IsReloadCatalogs, Always0 != 0 ? 16 : 24; * @param RequireTrustedCatalog - nonzero requires the observed catalog TRUSTED
* extension marker. This does not itself establish Microsoft signer trust.
* *
* @param Always2007F - unknown, always 0x2007F, maybe a mask. * @param AcceptedCatalogPolicyBits - candidate catalog policy-bit filter.
* Observed 26100 x64 requires ANY overlap with catalog PolicyBits, not
* every requested bit. This is not a flags-to-enable input.
* *
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain. * @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
* *
* @param CatalogName[out option] - catalog file name. * @param CatalogName[out option] - initialize empty; observed 26100 x64 returns
* a separate duplicated string allocation. Release a returned Buffer
* with RtlFreeUnicodeString, separately from both policy outputs.
* *
* @param SigningTime[out] - when the file was signed (FILETIME format) * @param SigningTime[out] - when the file was signed (FILETIME format)
* *
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain. * @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain.
* *
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified. * @return Overall verification NTSTATUS. A successful policy VerificationStatus
* does not replace this return value. Catalog selection and the
* selected policy still apply. Outputs may carry diagnostics on failure.
* See docs/mincrypt-contracts.md for ownership and policy limits.
* Various error values otherwise, for example: * Various error values otherwise, for example:
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature * STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked * STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired * STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/ */
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI MINCRYPTAPI
NTSTATUS NTSTATUS NTAPI
NTAPI
CiVerifyHashInCatalog( CiVerifyHashInCatalog(
_In_ PVOID Hash, _In_reads_bytes_(HashSize) PVOID Hash,
_In_ UINT32 HashSize, _In_ UINT32 HashSize,
_In_ ALG_ID HashAlgId, _In_ ALG_ID HashAlgId,
_In_ BOOLEAN IsReloadCatalogs, _In_ BOOLEAN ReloadCatalogs,
_In_ UINT32 Always0, _In_ UINT32 RequireTrustedCatalog,
_In_ UINT32 Always2007F, _In_ UINT32 AcceptedCatalogPolicyBits,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_opt_ UNICODE_STRING* CatalogName, _Out_opt_ UNICODE_STRING* CatalogName,
_Out_ LARGE_INTEGER* SigningTime, _Out_ LARGE_INTEGER* SigningTime,
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo
); );
#if (NTDDI_VERSION >= NTDDI_WIN10) #if (NTDDI_VERSION >= NTDDI_WIN10)
typedef typedef _IRQL_requires_same_ _Function_class_(MINCRYPT_ALLOCATE_ROUTINE)
_IRQL_requires_same_ __drv_allocatesMem(
_Function_class_(MINCRYPT_ALLOCATE_ROUTINE) Mem
__drv_allocatesMem(Mem) )
PVOID PVOID
NTAPI NTAPI
MINCRYPT_ALLOCATE_ROUTINE( MINCRYPT_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize);
_In_ SIZE_T ByteSize
);
typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE; typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE;
/** /**
* Parse the publisher name from the certificate * Parse the publisher name from the certificate.
* Export absent from sampled Windows 10 x64 builds 10240–17134; observed present
* on 19041.1865, 22000.318 and 26100 x64; absent on sampled 19041.1 x64.
* The NTDDI guard does not establish runtime availability. See NOTES.md.
* *
* @param Certificate - &PolicyInfo.ChainInfo->ChainElements[x].Certificate * @param Certificate - &PolicyInfo.ChainInfo->ChainElements[x].Certificate
* *
@@ -491,7 +519,10 @@ typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE;
* *
* @param PublisherName[out] - publisher name. * @param PublisherName[out] - publisher name.
* *
* @return buffer length. * @return NTSTATUS. The string byte count is returned in PublisherName->Length.
* Free PublisherName->Buffer with the allocator paired with AllocateRoutine,
* including on failure if a buffer was allocated. This routine extracts a name;
* it does not verify the certificate or its signer.
*/ */
MINCRYPTAPI MINCRYPTAPI
NTSTATUS NTSTATUS
@@ -502,23 +533,40 @@ CiGetCertPublisherName(
_Out_ PUNICODE_STRING PublisherName _Out_ PUNICODE_STRING PublisherName
); );
// Export observed already in 15063.0 x64 (1703); absent from sampled
// 10240/10586/14393 files. This writes CI-global state, not per-file policy.
MINCRYPTAPI MINCRYPTAPI
VOID VOID NTAPI
NTAPI
CiSetTrustedOriginClaimId( CiSetTrustedOriginClaimId(
_In_ UINT32 ClaimId _In_ UINT32 ClaimId
); );
// Domito descriptive name, not an official Microsoft flag name.
// Observed on CI 10.0.26100.8328 x64: broadens the accepted-root mask in
// CipGetAcceptedRootKeys. Sampled 1709/1803 instead select 0x224FF; earlier helper
// generations differ. This is not a WinVerifyTrust-equivalent trust policy.
// See docs/ci-validate-file-object.md for evidence and version limitations.
#define DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE 0x00000020UL
// Descriptive experimental bit, not an official or isolated selector flag.
// Participates in first-success guards in examined 22621/26100 x64 CI bodies;
// also changes default-path policy and fallback. One 22631 TESTSIGNING-enabled
// BthPS3 run confirms 0x20|0x80 returns its primary company signer. It is not an
// arbitrary signer index, guaranteed primary selection, or a production default.
#define DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL 0x00000080UL
/** /**
* Given a file object, verify the signature and provide information regarding * Given a file object, verify the signature and provide information regarding
* the certificates that was used for signing (the entire certificate chain) * the certificates that was used for signing (the entire certificate chain)
* *
* @param FileObject - FileObject of the PE in question * @param FileObject - FileObject of the PE in question
* *
* @param Unkonwn1 - unknown, 0 is a valid value. (Unkonwn1 and Unkonwn2 together calculate the minimum support algorithm) * @param ValidationFlags - validation policy flags; 0 is a valid value.
* See DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE and the accompanying documentation.
* *
* @param Unkonwn2 - unknown, 0 is a valid value. (^ the words above refer to 'CipGetHashAlgorithmForLegacyScenario') * @param RequestedSigningLevel - requested signing level; 0 is used by the probe.
* CI 26100 x64 consumes the low byte. UINT32 is retained for ABI compatibility
* until other supported architectures/builds are verified; use values in 0..255.
* *
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain. * @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
* *
@@ -526,30 +574,35 @@ CiSetTrustedOriginClaimId(
* *
* @param SigningTime[out] - when the file was signed (FILETIME format) * @param SigningTime[out] - when the file was signed (FILETIME format)
* *
* @param Hash - buffer containing the digest * @param Hash[out] - file digest, not a certificate thumbprint. Supply at least
* MINCRYPT_MAX_HASH_LENGTH bytes (required by the observed 26100 x64 helper).
* *
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256 * @param HashSize[in,out] - buffer capacity on entry; actual digest size on success.
* Initialize to sizeof(Hash) for every call, including retries.
* *
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C) * @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
* *
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified. * Initialize both policy outputs to empty objects and free both with CiFreePolicyInfo
* before reuse. Do not interpret failed-call outputs as a verification result.
*
* @return STATUS_SUCCESS when verification succeeds under the selected CI policy.
* This does not by itself establish the application's signer trust policy.
* Various error values otherwise, for example: * Various error values otherwise, for example:
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature * STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked * STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired * STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/ */
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI MINCRYPTAPI
NTSTATUS NTSTATUS NTAPI
NTAPI
CiValidateFileObject( CiValidateFileObject(
_In_ FILE_OBJECT* FileObject, _In_ FILE_OBJECT* FileObject,
_In_opt_ UINT32 Unkonwn1, _In_ UINT32 ValidationFlags,
_In_opt_ UINT32 Unkonwn2, _In_ UINT32 RequestedSigningLevel,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo, _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
_Out_ LARGE_INTEGER* SigningTime, _Out_ LARGE_INTEGER* SigningTime,
_Out_ UINT8* Hash, _Out_writes_bytes_(*HashSize) UINT8* Hash,
_Inout_ UINT32* HashSize, _Inout_ UINT32* HashSize,
_Out_ ALG_ID* HashAlgId _Out_ ALG_ID* HashAlgId
); );
+214 -36
View File
@@ -2,7 +2,6 @@
#include <Domito.MinCrypt.h> #include <Domito.MinCrypt.h>
/* ___ /* ___
* / __|___ _ __ _ __ ___ _ _ * / __|___ _ __ _ __ ___ _ _
* | (__/ _ \ ' \| ' \/ _ \ ' \ * | (__/ _ \ ' \| ' \/ _ \ ' \
@@ -25,7 +24,6 @@ EXTERN_C
void void
DomitoShutdown(); DomitoShutdown();
/* __ __ __ __ _ /* __ __ __ __ _
* | \/ |___ _ __ ___ _ _ _ _ | \/ |__ _ _ _ __ _ __ _ ___ _ __ ___ _ _| |_ * | \/ |___ _ __ ___ _ _ _ _ | \/ |__ _ _ _ __ _ __ _ ___ _ __ ___ _ _| |_
* | |\/| / -_) ' \/ _ \ '_| || | | |\/| / _` | ' \/ _` / _` / -_) ' \/ -_) ' \ _| * | |\/| / -_) ' \/ _ \ '_| || | | |\/| / _` | ' \/ _` / _` / -_) ' \/ -_) ' \ _|
@@ -36,28 +34,20 @@ DomitoShutdown();
// //
// Allocator function the library uses. // Allocator function the library uses.
// //
typedef typedef _IRQL_requires_same_ _Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE)
_IRQL_requires_same_ __drv_allocatesMem(
_Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE) mem
__drv_allocatesMem(mem) )
PVOID PVOID
NTAPI NTAPI
EVT_DOMITO_ALLOCATE_ROUTINE( EVT_DOMITO_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize);
_In_ SIZE_T ByteSize
);
typedef EVT_DOMITO_ALLOCATE_ROUTINE* PFN_DOMITO_ALLOCATE_ROUTINE; typedef EVT_DOMITO_ALLOCATE_ROUTINE* PFN_DOMITO_ALLOCATE_ROUTINE;
// //
// Freeing function the library uses. // Freeing function the library uses.
// //
typedef typedef _IRQL_requires_same_ _Function_class_(EVT_DOMITO_FREE_ROUTINE) void NTAPI
_IRQL_requires_same_ EVT_DOMITO_FREE_ROUTINE(_In_ __drv_freesMem(mem) PVOID Memory);
_Function_class_(EVT_DOMITO_FREE_ROUTINE)
void
NTAPI
EVT_DOMITO_FREE_ROUTINE(
_In_ __drv_freesMem(mem) PVOID Memory
);
typedef EVT_DOMITO_FREE_ROUTINE* PFN_DOMITO_FREE_ROUTINE; typedef EVT_DOMITO_FREE_ROUTINE* PFN_DOMITO_FREE_ROUTINE;
// //
@@ -81,7 +71,9 @@ DomitoGetMemoryFunctions(
); );
// //
// Replace Domito's memory allocation functions with a custom set // Replace Domito's memory allocation functions with a compatible pair during
// quiescent initialization. No in-flight calls or outstanding callback-backed
// allocations may span a pair change. Replacement is not synchronized.
EXTERN_C EXTERN_C
void void
DomitoSetMemoryFunctions( DomitoSetMemoryFunctions(
@@ -89,7 +81,6 @@ DomitoSetMemoryFunctions(
_In_opt_ PFN_DOMITO_FREE_ROUTINE Free _In_opt_ PFN_DOMITO_FREE_ROUTINE Free
); );
/* ___ _ _ /* ___ _ _
* / __|_ _ _ _ _ __| |_ ___ __ _ _ _ __ _ _ __| |_ _ _ * / __|_ _ _ _ _ __| |_ ___ __ _ _ _ __ _ _ __| |_ _ _
* | (__| '_| || | '_ \ _/ _ \/ _` | '_/ _` | '_ \ ' \ || | * | (__| '_| || | '_ \ _/ _ \/ _` | '_/ _` | '_ \ ' \ || |
@@ -141,6 +132,7 @@ typedef struct _WIN_CERTIFICATE
// Converts a WinCrypt CALG_ID to a BCRYPT_ALGORITHM identifier. // Converts a WinCrypt CALG_ID to a BCRYPT_ALGORITHM identifier.
// //
PCWSTR PCWSTR
FORCEINLINE FORCEINLINE
DOMITO_CALG_TO_BCRYPT_ALGORITHM( DOMITO_CALG_TO_BCRYPT_ALGORITHM(
_In_ UINT32 Calg _In_ UINT32 Calg
@@ -161,7 +153,6 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM(
} }
} }
/* ___ _ ___ _ _ _ /* ___ _ ___ _ _ _
* / __|___ __| |___ |_ _|_ _| |_ ___ __ _ _ _(_) |_ _ _ * / __|___ __| |___ |_ _|_ _| |_ ___ __ _ _ _(_) |_ _ _
* | (__/ _ \/ _` / -_) | || ' \ _/ -_) _` | '_| | _| || | * | (__/ _ \/ _` / -_) | || ' \ _/ -_) _` | '_| | _| || |
@@ -170,26 +161,74 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM(
*/ */
// //
// Extracts the CALG_ID from a signed PE that was used to // Heuristically detects the CALG_ID in the first WIN_CERTIFICATE using byte patterns.
// calculate the message digest when it was signed // Returns CALG_SHA1 when absent, malformed, or unrecognized; this is not ASN.1
// validation or signature verification. The directory array must contain the
// IMAGE_DIRECTORY_ENTRY_SECURITY entry.
// //
_IRQL_requires_max_(DISPATCH_LEVEL) _IRQL_requires_max_(DISPATCH_LEVEL)
EXTERN_C EXTERN_C
UINT32 __declspec(deprecated(
"Use DomitoGetPortableExecutableDigestKindEx; heuristic detection silently falls back to SHA-1. Check the returned NTSTATUS."
)) UINT32
DomitoGetPortableExecutableDigestKind( DomitoGetPortableExecutableDigestKind(
_In_ PUCHAR pPeBytes, _In_ PUCHAR pPeBytes,
_In_ ULONG PeSize, _In_ ULONG PeSize,
_In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory _In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory
); );
// // Reads the first PKCS#7 entry's SpcIndirectDataContent DigestInfo algorithm
// Extracts Authenticode signing information and calculates the file digest of a PE file. // using bounded DER navigation. Does not validate the signature or select among
// // multiple/nested signatures. Absent table => STATUS_NOT_FOUND; unsupported OID
// or certificate type => STATUS_NOT_SUPPORTED; malformed DER => format error.
// Unlike the compatibility helper above, it never silently falls back to SHA-1.
_IRQL_requires_max_(DISPATCH_LEVEL)
EXTERN_C
NTSTATUS
DomitoGetPortableExecutableDigestKindEx(
_In_reads_bytes_(PeSize) const UCHAR* PeBytes,
_In_ ULONG PeSize,
_In_reads_(IMAGE_DIRECTORY_ENTRY_SECURITY + 1) const IMAGE_DATA_DIRECTORY* Directories,
_Out_ PUINT32 DigestCalg
);
// Algorithm-explicit Authenticode digest for the supported layout subset:
// bounded PE32/PE32+ headers, contiguous nonoverlapping raw sections in file
// offset order, optional overlay before a complete EOF certificate table.
// SHA-1/256/384/512 supported. Gaps/overlaps/nonterminal certificates are rejected.
// No signature trust decision is made. Outputs are cleared on failure; the
// certificate borrows stable input memory, and only the digest must be freed.
// See docs/authenticode-digests.md for policy and Windows SIP comparisons.
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C EXTERN_C
NTSTATUS NTSTATUS
DomitoCalculatePortableExecutableDigestEx(
_In_reads_bytes_(PeSize) const UCHAR* PeBytes,
_In_ ULONG PeSize,
_In_ UINT32 HashAlgId,
_Out_ PULONG DigestSize,
_Outptr_result_bytebuffer_(*DigestSize) PVOID* Digest,
_Outptr_result_maybenull_ LPWIN_CERTIFICATE* Certificate,
_Out_ PULONG SecuritySize
);
//
// Calculates a PE digest using the existing linear hashing convention.
// Validates accessed header/certificate ranges and clears outputs on failure
// when all output pointers are valid. The input buffer must remain stable.
// The returned certificate pointer borrows the input buffer; free only the digest
// with DomitoFreePortableExecutableDigest. Algorithm detection is heuristic;
// general section-ordered Authenticode hashing is not implemented here.
//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
__declspec(deprecated(
"Use DomitoCalculatePortableExecutableDigestEx with an explicit algorithm; review its supported PE layout and failure statuses."
)) NTSTATUS
DomitoCalculatePortableExecutableDigest( DomitoCalculatePortableExecutableDigest(
_In_ PUCHAR pPeBytes, _In_ PUCHAR pPeBytes,
_In_ ULONG PeSize, _In_ ULONG PeSize,
@@ -210,14 +249,121 @@ DomitoFreePortableExecutableDigest(
_In_ PVOID pDigestOut _In_ PVOID pDigestOut
); );
// #if (NTDDI_VERSION >= NTDDI_WIN10)
// Verifies if the Authenticode signature of a give PE file matches the provided (e.g. SHA1) file digest. // Native forwards caller flags unchanged. The experimental x64 mode additionally
// // sets 0x80; it stops after the first accepted candidate in examined CI builds,
// and also changes other policy/fallback paths. Neither mode selects a publisher
// or enumerates signatures. No root-acceptance flag is added automatically.
typedef enum _DOMITO_CI_SIGNATURE_SELECTION
{
DomitoCiSignatureSelectionNative = 0,
DomitoCiSignatureSelectionFirstSuccessfulExperimental = 1
} DOMITO_CI_SIGNATURE_SELECTION;
// Zero-initialize before first use. Both policies own their CI chain allocations,
// including possible diagnostic allocations on failure. Do not copy as an owner.
// Hash is a file digest, not a certificate thumbprint. Hash/algorithm/signing time
// are cleared on failure; policies may retain diagnostics. Check return NTSTATUS.
typedef struct _DOMITO_CI_VALIDATION_RESULT
{
MINCRYPT_POLICY_INFO PolicyInfo;
MINCRYPT_POLICY_INFO TimeStampPolicyInfo;
LARGE_INTEGER SigningTime;
UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH];
UINT32 HashSize;
ALG_ID HashAlgId;
UINT32 FileObjectValidationFlags; // Flags passed to CI export; zero for embedded helper.
UINT8 RequestedSigningLevel; // File-object helper only.
} DOMITO_CI_VALIDATION_RESULT, *PDOMITO_CI_VALIDATION_RESULT;
// Safe for NULL or an empty/initialized result; resets all fields after cleanup.
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
void
DomitoFreeCiValidationResult(
_Inout_opt_ DOMITO_CI_VALIDATION_RESULT* Result
);
// Referenced FILE_OBJECT at PASSIVE_LEVEL; keep file bytes stable. Previous result
// policies are released before reuse, including invalid-argument failures.
// Supplies a fresh 64-byte hash buffer on every call. FirstSuccessfulExperimental
// is opt-in and x64-only; other architectures return STATUS_NOT_SUPPORTED for it.
// Only one selected signer plus its timestamp policy is returned. The caller
// must enforce its expected certificate and intended trust policy separately.
_Success_(return >= 0)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoValidateFileObjectEx(
_In_ PFILE_OBJECT FileObject,
_In_ UINT32 ValidationFlags,
_In_ UINT8 RequestedSigningLevel,
_In_ DOMITO_CI_SIGNATURE_SELECTION Selection,
_Inout_ DOMITO_CI_VALIDATION_RESULT* Result
);
// Hash and verify the first WIN_CERTIFICATE in a stable PE buffer. Uses the
// explicit digest helper's supported contiguous-section/EOF-certificate subset.
// SHA-1/256/384/512 are explicit; no algorithm retry or catalog fallback occurs.
// CI may select a nested signature under its algorithm rules: this is not a
// universal force-primary API. Matching SHA-256 selects the company primary in
// the tested dual-signed BthPS3 fixture. Caller still checks the returned signer.
_Success_(return >= 0)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoValidatePortableExecutableEmbeddedSignature(
_In_reads_bytes_(PeSize) const UCHAR* PeBytes,
_In_ ULONG PeSize,
_In_ ALG_ID HashAlgId,
_Inout_ DOMITO_CI_VALIDATION_RESULT* Result
);
#endif // NTDDI_VERSION >= NTDDI_WIN10
#define DOMITO_CI_SOURCE_EMBEDDED 1UL
#define DOMITO_CI_SOURCE_CATALOG 2UL
// Policy-explicit legacy helper. SHA-1/256 only; hash size must match algorithm.
// Policy objects must be zero-initialized or own valid CI results; both are freed
// before each attempt. Caller owns both final policy outputs, including failure.
// Embedded validation is tried first; only INVALID_IMAGE_HASH permits catalog
// fallback/reload. AcceptedCatalogPolicyBits uses ANY-overlap catalog filtering;
// RequireTrustedCatalog gates the observed marker, not publisher identity.
// Source is 0 on failure, EMBEDDED/CATALOG on success. Stable file bytes and a
// handle valid in the caller's current process context are required during I/O.
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C EXTERN_C
NTSTATUS NTSTATUS
DomitoValidateFileLegacyModeEx(
_In_ HANDLE FileHandle,
_In_reads_bytes_(HashSize) PVOID Hash,
_In_ UINT32 HashSize,
_In_ ALG_ID HashAlgId,
_In_ const IMAGE_DATA_DIRECTORY* SecurityDirectory,
_Inout_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_ LARGE_INTEGER* SigningTime,
_Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
_In_ UINT32 AcceptedCatalogPolicyBits,
_In_ BOOLEAN RequireTrustedCatalog,
_Out_ PULONG ValidationSource
);
//
// Compatibility wrapper around DomitoValidateFileLegacyModeEx, with catalog
// mask DOMITO_CI_LEGACY_CATALOG_POLICY_BITS and RequireTrustedCatalog = FALSE.
// The same initialization, ownership, hash, handle, and stable-file rules apply.
//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
__declspec(deprecated(
"Use DomitoValidateFileLegacyModeEx with explicit catalog policy and validation-source output; do not change trust policy implicitly."
)) NTSTATUS
DomitoValidateFileLegacyMode( DomitoValidateFileLegacyMode(
_In_ HANDLE FileHandle, _In_ HANDLE FileHandle,
_In_ PVOID Hash, _In_ PVOID Hash,
@@ -229,7 +375,6 @@ DomitoValidateFileLegacyMode(
_Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo _Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo
); );
/* __ __ _ /* __ __ _
* | \/ (_)___ __ * | \/ (_)___ __
* | |\/| | (_-</ _|_ * | |\/| | (_-</ _|_
@@ -238,7 +383,7 @@ DomitoValidateFileLegacyMode(
*/ */
// //
// Finds the base address of a driver module. // Finds the base address of a driver module. Clears the optional output first.
// //
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
@@ -251,7 +396,10 @@ DomitoFindModuleBaseAddress(
); );
// //
// Finds the address of an exported function by name. // Finds an export by name (case-insensitive) in a valid loader-mapped module.
// ModuleBase must remain mapped; arbitrary file buffers are not supported.
// Export table spans are checked against SizeOfImage. Forwarded exports return
// STATUS_NOT_SUPPORTED; the optional output is cleared before lookup.
// //
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
@@ -282,7 +430,28 @@ DomitoMemorySearchPattern(
); );
// //
// Reads from the beginning of a file until the end or the buffer size is reached. // Reads exactly BufferSize bytes at a nonnegative absolute file offset.
// Same completion, buffer, handle, APC, and short-read contract as DomitoReadFile.
//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoReadFileAt(
_In_ HANDLE FileHandle,
_Out_writes_bytes_(BufferSize) PVOID Buffer,
_In_ ULONG BufferSize,
_In_ const LARGE_INTEGER* ByteOffset
);
//
// Reads exactly BufferSize bytes from file offset zero. Short reads return
// STATUS_END_OF_FILE; Buffer may then contain a partial read. Zero length succeeds.
// Waits for pending I/O on a private event before returning. Requires PASSIVE_LEVEL
// with special kernel APCs enabled, a readable handle valid in the current process
// (or a kernel handle), and a buffer that remains valid for the entire call.
// For synchronous handles, the explicit read also updates the file position.
// //
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
@@ -296,14 +465,23 @@ DomitoReadFile(
); );
// //
// Gets the name of the main image of the process identified by PID. // Gets the main image name after resolving the ULONG PID once to a process handle.
// Requires DomitoInit. Output is NULL on failure; successful result is one Domito
// allocation containing UNICODE_STRING and its buffer. Use DomitoFreeProcessImageName.
// Do not change memory callbacks with outstanding allocations. PID reuse can race
// the initial open; a later path reopen is not proof of original image identity.
// //
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C EXTERN_C
NTSTATUS __declspec(deprecated(
"Use DomitoGetProcessImageNameEx with a caller-referenced PEPROCESS to avoid a fresh PID lookup; free the result with DomitoFreeProcessImageName."
)) NTSTATUS
DomitoGetProcessImageName( DomitoGetProcessImageName(
_In_ ULONG ProcessId, _In_ ULONG ProcessId,
_Inout_ PUNICODE_STRING* ProcessImageName _Outptr_result_maybenull_ PUNICODE_STRING* ProcessImageName
); );
// Additive consumer helpers, included here for existing Domito.h clients.
#include "Domito.Consumer.h"
+396
View File
@@ -0,0 +1,396 @@
#include "Domito.Internal.h"
typedef struct _DOMITO_CERTIFICATE_DER_VIEW
{
const UCHAR* Data;
ULONG Size;
} DOMITO_CERTIFICATE_DER_VIEW;
static BOOLEAN
DomitoCertificateDerTake(
DOMITO_CERTIFICATE_DER_VIEW* Input,
UCHAR Tag,
DOMITO_CERTIFICATE_DER_VIEW* Content,
DOMITO_CERTIFICATE_DER_VIEW* Encoded = nullptr
)
{
if (Input->Size < 2 || Input->Data[0] != Tag)
{
return FALSE;
}
ULONG length = Input->Data[1];
ULONG header = 2;
if (length & 0x80)
{
const ULONG count = length & 0x7f;
if (!count || count > 4 || count > Input->Size - 2 || !Input->Data[2])
{
return FALSE;
}
length = 0;
for (ULONG i = 0; i < count; ++i)
{
length = (length << 8) | Input->Data[header++];
}
if (length < 128)
{
return FALSE;
}
}
if (length > Input->Size - header)
{
return FALSE;
}
*Content = {Input->Data + header, length};
if (Encoded)
{
*Encoded = {Input->Data, header + length};
}
Input->Data += header + length;
Input->Size -= header + length;
return TRUE;
}
_Use_decl_annotations_
NTSTATUS
DomitoGetCertificateSpki(
const UCHAR* CertData,
ULONG CertSize,
DOMITO_CERTIFICATE_SPKI* Spki
)
{
PAGED_CODE();
if (!Spki)
{
return STATUS_INVALID_PARAMETER;
}
*Spki = {};
if (!CertData || !CertSize)
{
return STATUS_INVALID_PARAMETER;
}
DOMITO_CERTIFICATE_DER_VIEW certificateInput = {CertData, CertSize};
DOMITO_CERTIFICATE_DER_VIEW certificate;
DOMITO_CERTIFICATE_DER_VIEW tbsCertificate;
DOMITO_CERTIFICATE_DER_VIEW fieldContent;
DOMITO_CERTIFICATE_DER_VIEW encodedSpki;
DOMITO_CERTIFICATE_DER_VIEW spkiContent;
// Validate complete certificate framing, without interpreting trust/algorithms.
if (!DomitoCertificateDerTake(&certificateInput, 0x30, &certificate) || certificateInput.Size ||
!DomitoCertificateDerTake(&certificate, 0x30, &tbsCertificate) ||
!DomitoCertificateDerTake(&certificate, 0x30, &fieldContent) ||
!DomitoCertificateDerTake(&certificate, 0x03, &fieldContent) || !fieldContent.Size ||
fieldContent.Data[0] > 7 || certificate.Size)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
if (tbsCertificate.Size && tbsCertificate.Data[0] == 0xa0)
{
DOMITO_CERTIFICATE_DER_VIEW certificateVersion;
if (!DomitoCertificateDerTake(&tbsCertificate, 0xa0, &fieldContent) ||
!DomitoCertificateDerTake(&fieldContent, 0x02, &certificateVersion) || fieldContent.Size ||
certificateVersion.Size != 1 || certificateVersion.Data[0] > 2)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
}
const UCHAR tbsFieldTags[] = {0x02, 0x30, 0x30, 0x30, 0x30};
for (ULONG i = 0; i < RTL_NUMBER_OF(tbsFieldTags); ++i)
{
if (!DomitoCertificateDerTake(&tbsCertificate, tbsFieldTags[i], &fieldContent) ||
(i == 0 && !fieldContent.Size))
{
return STATUS_INVALID_IMAGE_FORMAT;
}
}
if (!DomitoCertificateDerTake(&tbsCertificate, 0x30, &spkiContent, &encodedSpki))
{
return STATUS_INVALID_IMAGE_FORMAT;
}
DOMITO_CERTIFICATE_DER_VIEW spkiFields = spkiContent;
DOMITO_CERTIFICATE_DER_VIEW keyAlgorithm;
DOMITO_CERTIFICATE_DER_VIEW algorithmOid;
DOMITO_CERTIFICATE_DER_VIEW publicKeyBits;
if (!DomitoCertificateDerTake(&spkiFields, 0x30, &keyAlgorithm) ||
!DomitoCertificateDerTake(&keyAlgorithm, 0x06, &algorithmOid) || !algorithmOid.Size ||
!DomitoCertificateDerTake(&spkiFields, 0x03, &publicKeyBits) || publicKeyBits.Size < 2 ||
publicKeyBits.Data[0] > 7 || spkiFields.Size)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
Spki->Encoded = {encodedSpki.Data, encodedSpki.Size};
Spki->Content = {spkiContent.Data, spkiContent.Size};
return STATUS_SUCCESS;
}
_Use_decl_annotations_
NTSTATUS
DomitoHashBuffer(
const UCHAR* Bytes,
ULONG Size,
ALG_ID Algorithm,
UCHAR* Hash,
ULONG* HashSize
)
{
PAGED_CODE();
if (!HashSize)
{
return STATUS_INVALID_PARAMETER;
}
const ULONG capacity = *HashSize;
ULONG required;
switch (Algorithm)
{
case CALG_SHA1:
required = 20;
break;
case CALG_SHA_256:
required = 32;
break;
case CALG_SHA_384:
required = 48;
break;
case CALG_SHA_512:
required = 64;
break;
default:
*HashSize = 0;
return STATUS_NOT_SUPPORTED;
}
if (!Bytes && Size)
{
*HashSize = 0;
return STATUS_INVALID_PARAMETER;
}
if (!Hash || capacity < required)
{
*HashSize = required;
return STATUS_BUFFER_TOO_SMALL;
}
RtlZeroMemory(Hash, required);
BCRYPT_ALG_HANDLE algorithm = nullptr;
BCRYPT_HASH_HANDLE hash = nullptr;
NTSTATUS status =
BCryptOpenAlgorithmProvider(&algorithm, DOMITO_CALG_TO_BCRYPT_ALGORITHM(Algorithm), MS_PRIMITIVE_PROVIDER, 0);
if (NT_SUCCESS(status))
{
status = BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0);
}
if (NT_SUCCESS(status) && Size)
{
status = BCryptHashData(hash, const_cast<PUCHAR>(Bytes), Size, 0);
}
if (NT_SUCCESS(status))
{
status = BCryptFinishHash(hash, Hash, required, 0);
}
if (hash)
{
BCryptDestroyHash(hash);
}
if (algorithm)
{
BCryptCloseAlgorithmProvider(algorithm, 0);
}
if (NT_SUCCESS(status))
{
*HashSize = required;
}
else
{
RtlZeroMemory(Hash, required);
*HashSize = 0;
}
return status;
}
_Use_decl_annotations_
NTSTATUS
DomitoHashCertificate(
const UCHAR* CertData,
ULONG CertSize,
DOMITO_CERTIFICATE_HASH_KIND Kind,
ALG_ID Algorithm,
UCHAR* Hash,
ULONG* HashSize
)
{
PAGED_CODE();
if (!HashSize)
{
return STATUS_INVALID_PARAMETER;
}
if (!CertData || !CertSize)
{
*HashSize = 0;
return STATUS_INVALID_PARAMETER;
}
DOMITO_CERTIFICATE_SPKI spki = {};
if (Kind == DomitoCertificateHashSpkiDer || Kind == DomitoCertificateHashSpkiContentLegacy)
{
NTSTATUS status = DomitoGetCertificateSpki(CertData, CertSize, &spki);
if (!NT_SUCCESS(status))
{
*HashSize = 0;
return status;
}
const DOMITO_BYTE_VIEW selected = Kind == DomitoCertificateHashSpkiDer ? spki.Encoded : spki.Content;
return DomitoHashBuffer(selected.Data, selected.Size, Algorithm, Hash, HashSize);
}
if (Kind != DomitoCertificateHashDer)
{
*HashSize = 0;
return STATUS_INVALID_PARAMETER;
}
// Whole-certificate fingerprint: hashes exact input bytes, no trust claim.
return DomitoHashBuffer(CertData, CertSize, Algorithm, Hash, HashSize);
}
static int
DomitoHexDigit(
WCHAR Value
)
{
if (Value >= L'0' && Value <= L'9')
{
return Value - L'0';
}
if (Value >= L'A' && Value <= L'F')
{
return Value - L'A' + 10;
}
if (Value >= L'a' && Value <= L'f')
{
return Value - L'a' + 10;
}
return -1;
}
_Use_decl_annotations_
NTSTATUS
DomitoDecodeHex(
const UNICODE_STRING* Hex,
UCHAR* Bytes,
ULONG ByteCount
)
{
PAGED_CODE();
if (!Bytes && ByteCount)
{
return STATUS_INVALID_PARAMETER;
}
if (Bytes && ByteCount)
{
RtlZeroMemory(Bytes, ByteCount);
}
if (!Hex || Hex->Length > Hex->MaximumLength || (Hex->Length && !Hex->Buffer) ||
ByteCount > MAXUSHORT / (2 * sizeof(WCHAR)) || Hex->Length != ByteCount * 2 * sizeof(WCHAR))
{
return STATUS_INVALID_PARAMETER;
}
for (ULONG i = 0; i < ByteCount * 2; ++i)
{
if (DomitoHexDigit(Hex->Buffer[i]) < 0)
{
return STATUS_INVALID_PARAMETER;
}
}
for (ULONG i = 0; i < ByteCount; ++i)
{
Bytes[i] =
static_cast<UCHAR>((DomitoHexDigit(Hex->Buffer[2 * i]) << 4) | DomitoHexDigit(Hex->Buffer[2 * i + 1]));
}
return STATUS_SUCCESS;
}
_Use_decl_annotations_
NTSTATUS
DomitoEncodeHex(
const UCHAR* Bytes,
ULONG ByteCount,
WCHAR* Hex,
ULONG HexChars
)
{
PAGED_CODE();
if (Hex && HexChars)
{
Hex[0] = 0;
}
if ((!Bytes && ByteCount) || !Hex)
{
return STATUS_INVALID_PARAMETER;
}
if (!HexChars || ByteCount > (HexChars - 1) / 2)
{
return STATUS_BUFFER_TOO_SMALL;
}
static const WCHAR digits[] = L"0123456789ABCDEF";
WCHAR* destination = Hex;
for (ULONG i = 0; i < ByteCount; ++i)
{
*destination++ = digits[Bytes[i] >> 4];
*destination++ = digits[Bytes[i] & 15];
}
*destination = 0;
return STATUS_SUCCESS;
}
+216
View File
@@ -0,0 +1,216 @@
#include "Domito.Internal.h"
#if (NTDDI_VERSION >= NTDDI_WIN10)
static UINT32
DomitoCiDigestSize(
ALG_ID Algorithm
)
{
switch (Algorithm)
{
case CALG_SHA1:
return MINCRYPT_SHA1_LENGTH;
case CALG_SHA_256:
return MINCRYPT_SHA256_LENGTH;
case CALG_SHA_384:
return MINCRYPT_SHA384_LENGTH;
case CALG_SHA_512:
return MINCRYPT_SHA512_LENGTH;
default:
return 0;
}
}
static void
DomitoClearCiDigest(
DOMITO_CI_VALIDATION_RESULT* Result
)
{
RtlZeroMemory(Result->Hash, sizeof(Result->Hash));
Result->HashSize = 0;
Result->HashAlgId = 0;
Result->SigningTime.QuadPart = 0;
}
_Use_decl_annotations_
void
DomitoFreeCiValidationResult(
DOMITO_CI_VALIDATION_RESULT* Result
)
{
PAGED_CODE();
if (!Result)
{
return;
}
CiFreePolicyInfo(&Result->PolicyInfo);
CiFreePolicyInfo(&Result->TimeStampPolicyInfo);
RtlZeroMemory(Result, sizeof(*Result));
}
_Use_decl_annotations_
NTSTATUS
DomitoValidateFileObjectEx(
PFILE_OBJECT FileObject,
UINT32 ValidationFlags,
UINT8 RequestedSigningLevel,
DOMITO_CI_SIGNATURE_SELECTION Selection,
DOMITO_CI_VALIDATION_RESULT* Result
)
{
PAGED_CODE();
if (!Result)
{
return STATUS_INVALID_PARAMETER;
}
DomitoFreeCiValidationResult(Result);
if (!FileObject)
{
return STATUS_INVALID_PARAMETER;
}
if (Selection != DomitoCiSignatureSelectionNative &&
Selection != DomitoCiSignatureSelectionFirstSuccessfulExperimental)
{
return STATUS_INVALID_PARAMETER;
}
if (Selection == DomitoCiSignatureSelectionFirstSuccessfulExperimental)
{
#if defined(_M_AMD64) || defined(_AMD64_)
ValidationFlags |= DOMITO_CI_VALIDATE_FLAG_FIRST_SUCCESS_EXPERIMENTAL;
#else
return STATUS_NOT_SUPPORTED;
#endif
}
Result->FileObjectValidationFlags = ValidationFlags;
Result->RequestedSigningLevel = RequestedSigningLevel;
Result->HashSize = sizeof(Result->Hash);
NTSTATUS status = CiValidateFileObject(
FileObject,
ValidationFlags,
RequestedSigningLevel,
&Result->PolicyInfo,
&Result->TimeStampPolicyInfo,
&Result->SigningTime,
Result->Hash,
&Result->HashSize,
&Result->HashAlgId
);
if (NT_SUCCESS(status))
{
const UINT32 expected = DomitoCiDigestSize(Result->HashAlgId);
if (!expected)
{
status = STATUS_NOT_SUPPORTED;
}
else if (Result->HashSize != expected || Result->HashSize > sizeof(Result->Hash))
{
status = STATUS_INVALID_BUFFER_SIZE;
}
}
if (!NT_SUCCESS(status))
{
DomitoClearCiDigest(Result);
}
else
{
RtlZeroMemory(Result->Hash + Result->HashSize, sizeof(Result->Hash) - Result->HashSize);
}
return status;
}
_Use_decl_annotations_
NTSTATUS
DomitoValidatePortableExecutableEmbeddedSignature(
const UCHAR* PeBytes,
ULONG PeSize,
ALG_ID HashAlgId,
DOMITO_CI_VALIDATION_RESULT* Result
)
{
PAGED_CODE();
if (!Result)
{
return STATUS_INVALID_PARAMETER;
}
DomitoFreeCiValidationResult(Result);
if (!PeBytes)
{
return STATUS_INVALID_PARAMETER;
}
const UINT32 expected = DomitoCiDigestSize(HashAlgId);
if (!expected)
{
return STATUS_NOT_SUPPORTED;
}
PVOID digest = nullptr;
ULONG digestSize = 0;
ULONG securitySize = 0;
LPWIN_CERTIFICATE certificate = nullptr;
NTSTATUS status = DomitoCalculatePortableExecutableDigestEx(
PeBytes, PeSize, HashAlgId, &digestSize, &digest, &certificate, &securitySize
);
if (NT_SUCCESS(status))
{
if (!certificate || !securitySize)
{
status = STATUS_INVALID_IMAGE_HASH;
}
else if (!digest || digestSize != expected || digestSize > sizeof(Result->Hash))
{
status = STATUS_INVALID_BUFFER_SIZE;
}
else
{
status = CiCheckSignedFile(
digest,
digestSize,
HashAlgId,
certificate,
securitySize,
&Result->PolicyInfo,
&Result->SigningTime,
&Result->TimeStampPolicyInfo
);
if (NT_SUCCESS(status))
{
RtlCopyMemory(Result->Hash, digest, digestSize);
Result->HashSize = digestSize;
Result->HashAlgId = HashAlgId;
}
}
}
if (digest)
{
DomitoFreePortableExecutableDigest(digest);
}
if (!NT_SUCCESS(status))
{
DomitoClearCiDigest(Result);
}
return status;
}
#endif // NTDDI_VERSION >= NTDDI_WIN10
File diff suppressed because it is too large. Load diff
+395
View File
@@ -0,0 +1,395 @@
#include "Domito.Internal.h"
_Use_decl_annotations_
NTSTATUS
DomitoFindModuleExportedFunctionAddress(
PANSI_STRING ModuleName,
PANSI_STRING FunctionName,
PVOID* FunctionAddress
)
{
PAGED_CODE();
if (!FunctionAddress)
{
return STATUS_INVALID_PARAMETER;
}
*FunctionAddress = nullptr;
if (!ModuleName || !ModuleName->Buffer || !ModuleName->Length || ModuleName->Length > ModuleName->MaximumLength ||
!FunctionName || !FunctionName->Buffer || !FunctionName->Length ||
FunctionName->Length > FunctionName->MaximumLength)
{
return STATUS_INVALID_PARAMETER;
}
PVOID moduleBase = nullptr;
NTSTATUS status = DomitoFindModuleBaseAddress(ModuleName, &moduleBase);
if (!NT_SUCCESS(status))
{
return status;
}
return DomitoFindExportedFunctionAddress(moduleBase, FunctionName, FunctionAddress);
}
_Use_decl_annotations_
void
DomitoFreeProcessImageName(
PUNICODE_STRING ImageName
)
{
PAGED_CODE();
if (ImageName)
{
G_Memory.Free(ImageName);
}
}
_Use_decl_annotations_
NTSTATUS
DomitoGetProcessImageNameEx(
PEPROCESS Process,
PUNICODE_STRING* ImageName
)
{
PAGED_CODE();
if (!ImageName)
{
return STATUS_INVALID_PARAMETER;
}
*ImageName = nullptr;
if (!Process)
{
return STATUS_INVALID_PARAMETER;
}
PUNICODE_STRING processImageName = nullptr;
NTSTATUS status = SeLocateProcessImageName(Process, &processImageName);
if (NT_SUCCESS(status))
{
if (!processImageName || !processImageName->Buffer || !processImageName->Length ||
(processImageName->Length & 1) || processImageName->Length > processImageName->MaximumLength ||
processImageName->Length > MAXUSHORT - sizeof(WCHAR))
{
status = STATUS_INVALID_BUFFER_SIZE;
}
else
{
const SIZE_T size = sizeof(UNICODE_STRING) + processImageName->Length + sizeof(WCHAR);
PUNICODE_STRING imageName = static_cast<PUNICODE_STRING>(G_Memory.Allocate(size));
if (!imageName)
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
else
{
imageName->Buffer = reinterpret_cast<PWCH>(imageName + 1);
imageName->Length = processImageName->Length;
imageName->MaximumLength = processImageName->Length + sizeof(WCHAR);
RtlCopyMemory(imageName->Buffer, processImageName->Buffer, processImageName->Length);
imageName->Buffer[imageName->Length / sizeof(WCHAR)] = 0;
*ImageName = imageName;
}
}
}
if (processImageName)
{
ExFreePool(processImageName);
}
return status;
}
#if (NTDDI_VERSION >= NTDDI_WIN10)
_Use_decl_annotations_
NTSTATUS
DomitoValidateFileHandleEx(
HANDLE FileHandle,
UINT32 ValidationFlags,
UINT8 RequestedSigningLevel,
DOMITO_CI_SIGNATURE_SELECTION Selection,
DOMITO_CI_VALIDATION_RESULT* Result
)
{
PAGED_CODE();
if (!Result)
{
return STATUS_INVALID_PARAMETER;
}
DomitoFreeCiValidationResult(Result);
if (!FileHandle)
{
return STATUS_INVALID_PARAMETER;
}
PFILE_OBJECT fileObject = nullptr;
NTSTATUS status = ObReferenceObjectByHandle(
FileHandle, FILE_READ_DATA, *IoFileObjectType, KernelMode, reinterpret_cast<PVOID*>(&fileObject), nullptr
);
if (!NT_SUCCESS(status))
{
return status;
}
status = DomitoValidateFileObjectEx(fileObject, ValidationFlags, RequestedSigningLevel, Selection, Result);
ObDereferenceObject(fileObject);
return status;
}
_Use_decl_annotations_
NTSTATUS
DomitoValidateFilePathEx(
const UNICODE_STRING* Path,
UINT32 ValidationFlags,
UINT8 RequestedSigningLevel,
DOMITO_CI_SIGNATURE_SELECTION Selection,
DOMITO_CI_VALIDATION_RESULT* Result
)
{
PAGED_CODE();
if (!Result)
{
return STATUS_INVALID_PARAMETER;
}
DomitoFreeCiValidationResult(Result);
if (!Path || !Path->Buffer || Path->Length < sizeof(WCHAR) || (Path->Length & 1) ||
Path->Length > Path->MaximumLength || Path->Buffer[0] != L'\\')
{
return STATUS_INVALID_PARAMETER;
}
for (USHORT i = 0; i < Path->Length / sizeof(WCHAR); ++i)
{
if (!Path->Buffer[i])
{
return STATUS_INVALID_PARAMETER;
}
}
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(
&attributes, const_cast<PUNICODE_STRING>(Path), OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, nullptr, nullptr
);
HANDLE fileHandle = nullptr;
IO_STATUS_BLOCK iosb = {};
NTSTATUS status = ZwOpenFile(
&fileHandle,
FILE_READ_DATA | SYNCHRONIZE,
&attributes,
&iosb,
FILE_SHARE_READ,
FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT
);
if (!NT_SUCCESS(status))
{
return status;
}
status = DomitoValidateFileHandleEx(fileHandle, ValidationFlags, RequestedSigningLevel, Selection, Result);
ZwClose(fileHandle);
return status;
}
#if defined(_M_AMD64) || defined(_AMD64_)
static BOOLEAN
DomitoCiOwnedSpan(
const MINCRYPT_CHAIN_INFO* Chain,
const void* Data,
SIZE_T Size
)
{
const ULONG_PTR chainBaseAddress = reinterpret_cast<ULONG_PTR>(Chain);
const ULONG_PTR blobAddress = reinterpret_cast<ULONG_PTR>(Data);
// Empty blobs may be NULL; they are never dereferenced by this accessor.
if (!Size)
{
return TRUE;
}
return blobAddress >= chainBaseAddress && blobAddress - chainBaseAddress >= 56 &&
blobAddress - chainBaseAddress <= Chain->Size && Size <= Chain->Size - (blobAddress - chainBaseAddress);
}
#endif
_Use_decl_annotations_
NTSTATUS
DomitoGetCiChainView(
const MINCRYPT_POLICY_INFO* Policy,
DOMITO_CI_CHAIN_LAYOUT Layout,
DOMITO_CI_CHAIN_VIEW* View
)
{
PAGED_CODE();
if (!View)
{
return STATUS_INVALID_PARAMETER;
}
*View = {};
if (!Policy || Layout != DomitoCiChainLayoutObservedX64)
{
return STATUS_INVALID_PARAMETER;
}
#if defined(_M_AMD64) || defined(_AMD64_)
C_ASSERT(sizeof(MINCRYPT_CHAIN_ELEMENT) == 120);
C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, ChainElements) == 40);
C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, NumberOfChainElement) == 48);
if (Policy->Size != sizeof(*Policy))
{
return STATUS_NOT_SUPPORTED;
}
const MINCRYPT_CHAIN_INFO* chain = Policy->ChainInfo;
if (!chain)
{
return STATUS_NOT_FOUND;
}
if (chain->Size < 56)
{
return STATUS_INVALID_BUFFER_SIZE;
}
const ULONG count = chain->NumberOfChainElement;
if (!count)
{
return STATUS_NOT_FOUND;
}
if (count > (chain->Size - 56) / sizeof(MINCRYPT_CHAIN_ELEMENT) ||
!DomitoCiOwnedSpan(chain, chain->ChainElements, static_cast<SIZE_T>(count) * sizeof(MINCRYPT_CHAIN_ELEMENT)))
{
return STATUS_INVALID_BUFFER_SIZE;
}
for (ULONG i = 0; i < count; ++i)
{
const MINCRYPT_CHAIN_ELEMENT& element = chain->ChainElements[i];
if (element.HashSize > sizeof(element.Hash) || !element.Certificate.Size ||
!DomitoCiOwnedSpan(chain, element.Certificate.Data, element.Certificate.Size) ||
!DomitoCiOwnedSpan(chain, element.Subject.Data, element.Subject.Size) ||
!DomitoCiOwnedSpan(chain, element.Issuer.Data, element.Issuer.Size))
{
return STATUS_INVALID_BUFFER_SIZE;
}
}
ULONG rootId;
RtlCopyMemory(&rootId, reinterpret_cast<const UCHAR*>(chain) + 52, sizeof(rootId));
*View = {chain->ChainElements, count, rootId};
return STATUS_SUCCESS;
#else
return STATUS_NOT_SUPPORTED;
#endif
}
static MINCRYPT_ALLOCATE_ROUTINE DomitoAllocatePublisher;
static PVOID NTAPI
DomitoAllocatePublisher(
SIZE_T Size
)
{
#pragma warning(suppress : 4996)
return ExAllocatePoolWithTag(PagedPool, Size, DOMITO_POOL_TAG);
}
_Use_decl_annotations_
void
DomitoFreeCertificatePublisherName(
PUNICODE_STRING PublisherName
)
{
PAGED_CODE();
if (!PublisherName)
{
return;
}
if (PublisherName->Buffer)
{
ExFreePoolWithTag(PublisherName->Buffer, DOMITO_POOL_TAG);
}
*PublisherName = {};
}
_Use_decl_annotations_
NTSTATUS
DomitoGetCertificatePublisherName(
const MINCERT_BLOB* Certificate,
PUNICODE_STRING PublisherName
)
{
PAGED_CODE();
if (!PublisherName)
{
return STATUS_INVALID_PARAMETER;
}
*PublisherName = {};
if (!Certificate || !Certificate->Data || !Certificate->Size)
{
return STATUS_INVALID_PARAMETER;
}
ANSI_STRING moduleName = RTL_CONSTANT_STRING("\\SystemRoot\\system32\\CI.dll");
ANSI_STRING exportName = RTL_CONSTANT_STRING("CiGetCertPublisherName");
PVOID publisherRoutine = nullptr;
NTSTATUS status = DomitoFindModuleExportedFunctionAddress(&moduleName, &exportName, &publisherRoutine);
if (!NT_SUCCESS(status))
{
return status;
}
if (!publisherRoutine)
{
return STATUS_PROCEDURE_NOT_FOUND;
}
typedef NTSTATUS(NTAPI * GET_PUBLISHER)(MINCERT_BLOB*, PMINCRYPT_ALLOCATE_ROUTINE, PUNICODE_STRING);
const GET_PUBLISHER getPublisher = reinterpret_cast<GET_PUBLISHER>(publisherRoutine);
status = getPublisher(const_cast<MINCERT_BLOB*>(Certificate), DomitoAllocatePublisher, PublisherName);
if (NT_SUCCESS(status) && (PublisherName->Length > PublisherName->MaximumLength || (PublisherName->Length & 1) ||
!PublisherName->Length || !PublisherName->Buffer))
{
status = STATUS_INVALID_BUFFER_SIZE;
}
if (!NT_SUCCESS(status))
{
DomitoFreeCertificatePublisherName(PublisherName);
}
return status;
}
#endif
+293 -145
View File
@@ -7,18 +7,26 @@
#include "Domito.Internal.h" #include "Domito.Internal.h"
DOMITO_COMMON G_Common = {}; DOMITO_COMMON G_Common = {};
DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QipRoutineName, L"ZwQueryInformationProcess"); DECLARE_GLOBAL_CONST_UNICODE_STRING(
DECLARE_GLOBAL_CONST_UNICODE_STRING(G_IdetdRoutineName, L"RtlImageDirectoryEntryToData"); G_QipRoutineName,
DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QsiRoutineName, L"ZwQuerySystemInformation"); L"ZwQueryInformationProcess"
);
DECLARE_GLOBAL_CONST_UNICODE_STRING(
G_IdetdRoutineName,
L"RtlImageDirectoryEntryToData"
);
DECLARE_GLOBAL_CONST_UNICODE_STRING(
G_QsiRoutineName,
L"ZwQuerySystemInformation"
);
#ifndef LOG #ifndef LOG
#define LOG(Format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[Domito][" __FUNCTION__ "] " Format " \n", __VA_ARGS__) #define LOG(Format, ...) \
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[Domito][" __FUNCTION__ "] " Format " \n", __VA_ARGS__)
#endif #endif
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
@@ -43,10 +51,8 @@ _IRQL_requires_max_(PASSIVE_LEVEL)
void void
DomitoShutdown() DomitoShutdown()
{ {
} }
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
@@ -56,6 +62,16 @@ DomitoFindModuleBaseAddress(
_Inout_opt_ PVOID* ModuleBase _Inout_opt_ PVOID* ModuleBase
) )
{ {
if (ModuleBase)
{
*ModuleBase = nullptr;
}
if (!ModuleName || !ModuleName->Buffer || !ModuleName->Length || ModuleName->Length > ModuleName->MaximumLength)
{
return STATUS_INVALID_PARAMETER;
}
ULONG bufferSize = 0; ULONG bufferSize = 0;
PSYSTEM_MODULE_INFORMATION moduleInfo = NULL; PSYSTEM_MODULE_INFORMATION moduleInfo = NULL;
@@ -67,12 +83,7 @@ DomitoFindModuleBaseAddress(
const ULONG SystemModuleInformation = 11; const ULONG SystemModuleInformation = 11;
// Query the required buffer size for module information // Query the required buffer size for module information
NTSTATUS status = G_Common.ZwQuerySystemInformation( NTSTATUS status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, &bufferSize, 0, &bufferSize);
SystemModuleInformation,
&bufferSize,
0,
&bufferSize
);
if (status != STATUS_INFO_LENGTH_MISMATCH) if (status != STATUS_INFO_LENGTH_MISMATCH)
{ {
@@ -80,9 +91,7 @@ DomitoFindModuleBaseAddress(
} }
// Allocate memory for the module information // Allocate memory for the module information
moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate( moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate(bufferSize);
bufferSize
);
if (moduleInfo == NULL) if (moduleInfo == NULL)
{ {
@@ -90,12 +99,7 @@ DomitoFindModuleBaseAddress(
} }
// Retrieve the module information // Retrieve the module information
status = G_Common.ZwQuerySystemInformation( status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, moduleInfo, bufferSize, NULL);
SystemModuleInformation,
moduleInfo,
bufferSize,
NULL
);
if (!NT_SUCCESS(status)) if (!NT_SUCCESS(status))
{ {
@@ -120,6 +124,7 @@ DomitoFindModuleBaseAddress(
{ {
*ModuleBase = moduleInfo->Module[i].Base; *ModuleBase = moduleInfo->Module[i].Base;
} }
break; break;
} }
} }
@@ -129,6 +134,19 @@ DomitoFindModuleBaseAddress(
return status; return status;
} }
// This helper handles directory RVAs after the loader-mapped header establishes
// SizeOfImage. ModuleBase is not an arbitrary file buffer (there is no size input).
static BOOLEAN
DomitoImageRvaSpan(
ULONG ImageSize,
ULONG Rva,
ULONG Count,
ULONG ElementSize
)
{
return Rva <= ImageSize && ElementSize != 0 && Count <= (ImageSize - Rva) / ElementSize;
}
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
@@ -139,56 +157,143 @@ DomitoFindExportedFunctionAddress(
_Inout_opt_ PVOID* FunctionAddress _Inout_opt_ PVOID* FunctionAddress
) )
{ {
NTSTATUS status = STATUS_NOT_FOUND; if (FunctionAddress)
ULONG exportSize; {
*FunctionAddress = nullptr;
}
if (G_Common.RtlImageDirectoryEntryToData == NULL) if (!ModuleBase || !FunctionName || !FunctionName->Buffer || !FunctionName->Length)
{
return STATUS_INVALID_PARAMETER;
}
if (!G_Common.RtlImageDirectoryEntryToData)
{ {
return STATUS_NOT_IMPLEMENTED; return STATUS_NOT_IMPLEMENTED;
} }
// Retrieve the export directory information // Initial headers belong to a valid loaded module, as required by this API.
const PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)G_Common.RtlImageDirectoryEntryToData( const PUCHAR base = (PUCHAR)ModuleBase;
ModuleBase, IMAGE_DOS_HEADER dos;
TRUE, memcpy(&dos, base, sizeof(dos));
IMAGE_DIRECTORY_ENTRY_EXPORT,
&exportSize
);
if (exportDirectory == NULL) if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < (LONG)sizeof(dos))
{ {
return STATUS_INVALID_IMAGE_FORMAT; return STATUS_INVALID_IMAGE_FORMAT;
} }
STRING currentFunctionName; const PUCHAR nt = base + dos.e_lfanew;
ULONG signature;
memcpy(&signature, nt, sizeof(signature));
USHORT magic;
memcpy(&magic, nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER), sizeof(magic));
const PULONG functionAddresses = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfFunctions); if (signature != IMAGE_NT_SIGNATURE ||
const PULONG functionNames = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNames); (magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC && magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC))
const PUSHORT functionOrdinals = (PUSHORT)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNameOrdinals);
for (ULONG i = 0; i < exportDirectory->NumberOfNames; i++)
{ {
const char* functionName = (const char*)((ULONG_PTR)ModuleBase + functionNames[i]); return STATUS_INVALID_IMAGE_FORMAT;
const USHORT functionOrdinal = functionOrdinals[i]; }
UNREFERENCED_PARAMETER(functionOrdinal);
const ULONG functionRva = functionAddresses[i]; // SizeOfImage is at the same offset in PE32 and PE32+ optional headers.
const PVOID functionAddress = (PVOID)((ULONG_PTR)ModuleBase + functionRva); ULONG imageSize;
memcpy(
&imageSize,
nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage),
sizeof(imageSize)
);
RtlInitAnsiString(&currentFunctionName, functionName); if (!DomitoImageRvaSpan(
imageSize,
if (0 == RtlCompareString(FunctionName, &currentFunctionName, TRUE)) (ULONG)dos.e_lfanew,
sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage) +
sizeof(imageSize),
1
))
{ {
return STATUS_INVALID_IMAGE_FORMAT;
}
ULONG exportSize = 0;
const PVOID directory =
G_Common.RtlImageDirectoryEntryToData(ModuleBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize);
const ULONG_PTR directoryOffset = (ULONG_PTR)directory - (ULONG_PTR)ModuleBase;
if (!directory || directoryOffset > imageSize || exportSize < sizeof(IMAGE_EXPORT_DIRECTORY) ||
exportSize > imageSize - directoryOffset)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
IMAGE_EXPORT_DIRECTORY exports;
memcpy(&exports, directory, sizeof(exports));
if (!DomitoImageRvaSpan(imageSize, exports.AddressOfFunctions, exports.NumberOfFunctions, sizeof(ULONG)) ||
!DomitoImageRvaSpan(imageSize, exports.AddressOfNames, exports.NumberOfNames, sizeof(ULONG)) ||
!DomitoImageRvaSpan(imageSize, exports.AddressOfNameOrdinals, exports.NumberOfNames, sizeof(USHORT)))
{
return STATUS_INVALID_IMAGE_FORMAT;
}
for (ULONG i = 0; i < exports.NumberOfNames; ++i)
{
ULONG nameRva;
USHORT ordinal;
memcpy(&nameRva, base + exports.AddressOfNames + i * sizeof(ULONG), sizeof(nameRva));
memcpy(&ordinal, base + exports.AddressOfNameOrdinals + i * sizeof(USHORT), sizeof(ordinal));
if (nameRva >= imageSize || ordinal >= exports.NumberOfFunctions)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
ULONG length = 0;
while (length < imageSize - nameRva && base[nameRva + length] != 0)
{
++length;
}
if (length == imageSize - nameRva || length > MAXUSHORT)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
ANSI_STRING current;
current.Buffer = (PCHAR)(base + nameRva);
current.Length = (USHORT)length;
current.MaximumLength = (USHORT)length;
if (RtlCompareString(FunctionName, &current, TRUE) != 0)
{
continue;
}
ULONG functionRva;
memcpy(&functionRva, base + exports.AddressOfFunctions + ordinal * sizeof(ULONG), sizeof(functionRva));
if (!functionRva)
{
return STATUS_NOT_FOUND; // Empty EAT slot.
}
if (functionRva >= imageSize)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
if (functionRva >= directoryOffset && functionRva - directoryOffset < exportSize)
{
return STATUS_NOT_SUPPORTED; // Forwarder string, not a callable address.
}
if (FunctionAddress) if (FunctionAddress)
{ {
status = STATUS_SUCCESS; *FunctionAddress = base + functionRva;
*FunctionAddress = functionAddress;
}
break;
}
} }
return status; return STATUS_SUCCESS;
}
return STATUS_NOT_FOUND;
} }
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
@@ -221,6 +326,7 @@ DomitoMemorySearchPattern(
for (SIZE_T i = 0; i + puLen <= puSize; i++) for (SIZE_T i = 0; i + puLen <= puSize; i++)
{ {
BOOLEAN found = TRUE; BOOLEAN found = TRUE;
for (SIZE_T j = 0; j < puLen; j++) for (SIZE_T j = 0; j < puLen; j++)
{ {
if (pcPattern[j] != uWildcard && pcPattern[j] != ((PCUCHAR)pcBase)[i + j]) if (pcPattern[j] != uWildcard && pcPattern[j] != ((PCUCHAR)pcBase)[i + j])
@@ -240,6 +346,74 @@ DomitoMemorySearchPattern(
return STATUS_NOT_FOUND; return STATUS_NOT_FOUND;
} }
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
NTSTATUS
DomitoReadFileAt(
_In_ HANDLE FileHandle,
_Out_writes_bytes_(BufferSize) PVOID Buffer,
_In_ ULONG BufferSize,
_In_ const LARGE_INTEGER* ByteOffset
)
{
if (!FileHandle || !ByteOffset || ByteOffset->QuadPart < 0 || (!Buffer && BufferSize))
{
return STATUS_INVALID_PARAMETER;
}
if (!BufferSize)
{
return STATUS_SUCCESS;
}
HANDLE eventHandle = nullptr;
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr);
NTSTATUS status = ZwCreateEvent(&eventHandle, EVENT_ALL_ACCESS, &attributes, NotificationEvent, FALSE);
if (!NT_SUCCESS(status))
{
return status;
}
PVOID eventObject = nullptr;
status = ObReferenceObjectByHandle(eventHandle, SYNCHRONIZE, *ExEventObjectType, KernelMode, &eventObject, nullptr);
if (!NT_SUCCESS(status))
{
ZwClose(eventHandle);
return status;
}
IO_STATUS_BLOCK ioStatusBlock = {};
LARGE_INTEGER offset = *ByteOffset;
status =
ZwReadFile(FileHandle, eventHandle, nullptr, nullptr, &ioStatusBlock, Buffer, BufferSize, &offset, nullptr);
if (status == STATUS_PENDING)
{
// A private event identifies this request even if the file has other I/O.
// KernelMode + nonalertable + no timeout cannot return before completion.
KeWaitForSingleObject(eventObject, Executive, KernelMode, FALSE, nullptr);
status = ioStatusBlock.Status;
}
if (NT_SUCCESS(status))
{
status = ioStatusBlock.Status;
if (NT_SUCCESS(status) && ioStatusBlock.Information != BufferSize)
{
status = STATUS_END_OF_FILE;
}
}
ObDereferenceObject(eventObject);
ZwClose(eventHandle);
return status;
}
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
@@ -250,33 +424,8 @@ DomitoReadFile(
_In_ ULONG BufferSize _In_ ULONG BufferSize
) )
{ {
NTSTATUS status = STATUS_SUCCESS; const LARGE_INTEGER offset = {};
IO_STATUS_BLOCK ioStatusBlock; return DomitoReadFileAt(FileHandle, Buffer, BufferSize, &offset);
// Read the file into memory using ZwReadFile
if (!NT_SUCCESS(status = ZwReadFile(
FileHandle,
NULL,
NULL,
NULL,
&ioStatusBlock,
Buffer,
BufferSize,
NULL,
NULL
)))
{
return status;
}
// Check if the file was read successfully
if (!NT_SUCCESS(ioStatusBlock.Status))
{
return ioStatusBlock.Status;
}
// File read successfully
return status;
} }
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
@@ -286,95 +435,94 @@ _IRQL_requires_max_(PASSIVE_LEVEL)
NTSTATUS NTSTATUS
DomitoGetProcessImageName( DomitoGetProcessImageName(
_In_ ULONG ProcessId, _In_ ULONG ProcessId,
_Inout_ PUNICODE_STRING * ProcessImageName _Outptr_result_maybenull_ PUNICODE_STRING* ProcessImageName
) )
{ {
NTSTATUS status = STATUS_UNSUCCESSFUL; PAGED_CODE();
ULONG returnedLength;
HANDLE hProcess = NULL;
PAGED_CODE(); // this eliminates the possibility of the IDLE Thread/Process if (!ProcessImageName)
if (ProcessId == 0 || ProcessId == 4)
{
return STATUS_INVALID_PARAMETER_1;
}
if (ProcessImageName == NULL)
{ {
return STATUS_INVALID_PARAMETER_2; return STATUS_INVALID_PARAMETER_2;
} }
CLIENT_ID cid; *ProcessImageName = nullptr;
cid.UniqueProcess = (HANDLE)ProcessId;
cid.UniqueThread = NULL;
OBJECT_ATTRIBUTES objAttr; if (!ProcessId || ProcessId == 4)
InitializeObjectAttributes( {
&objAttr, return STATUS_INVALID_PARAMETER_1;
NULL, }
OBJ_KERNEL_HANDLE,
NULL,
NULL
);
if (!NT_SUCCESS(status = ZwOpenProcess( if (!G_Common.ZwQueryInformationProcess)
&hProcess, {
PROCESS_ALL_ACCESS, return STATUS_NOT_IMPLEMENTED;
&objAttr, }
&cid
))) CLIENT_ID cid = {};
cid.UniqueProcess = ULongToHandle(ProcessId);
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr);
HANDLE process = nullptr;
// PROCESS_QUERY_LIMITED_INFORMATION (WinNT.h); not named by the WDK headers.
const ACCESS_MASK queryLimitedInformation = 0x1000;
NTSTATUS status = ZwOpenProcess(&process, queryLimitedInformation, &attributes, &cid);
if (!NT_SUCCESS(status))
{ {
return status; return status;
} }
if (G_Common.ZwQueryInformationProcess == NULL) ULONG size = 0;
status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, nullptr, 0, &size);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{ {
status = STATUS_NOT_IMPLEMENTED; if (size < sizeof(UNICODE_STRING))
goto cleanUp;
}
/* Query the actual size of the process path */
status = G_Common.ZwQueryInformationProcess(
hProcess,
ProcessImageFileName,
NULL, // buffer
0, // buffer size
&returnedLength
);
if (STATUS_INFO_LENGTH_MISMATCH != status)
{ {
goto cleanUp; status = STATUS_INVALID_BUFFER_SIZE;
} }
else
{
auto image = static_cast<PUNICODE_STRING>(G_Memory.Allocate(size));
*ProcessImageName = (PUNICODE_STRING)G_Memory.Allocate(returnedLength); if (!image)
if (*ProcessImageName == NULL)
{ {
status = STATUS_INSUFFICIENT_RESOURCES; status = STATUS_INSUFFICIENT_RESOURCES;
goto cleanUp;
} }
else
/* Retrieve the process path from the handle to the process */
if (!NT_SUCCESS(status = G_Common.ZwQueryInformationProcess(
hProcess,
ProcessImageFileName,
*ProcessImageName,
returnedLength,
&returnedLength
)))
{ {
G_Memory.Free(*ProcessImageName); ULONG returned = 0;
} status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, image, size, &returned);
cleanUp: if (NT_SUCCESS(status))
if (hProcess)
{ {
ZwClose(hProcess); const ULONG_PTR base = reinterpret_cast<ULONG_PTR>(image);
const ULONG_PTR buffer = reinterpret_cast<ULONG_PTR>(image->Buffer);
if (!image->Length || (image->Length & 1) || image->Length > image->MaximumLength ||
buffer < base || buffer - base < sizeof(UNICODE_STRING) || buffer - base > size ||
image->MaximumLength > size - (buffer - base))
{
status = STATUS_INVALID_BUFFER_SIZE;
}
} }
if (NT_SUCCESS(status))
{
*ProcessImageName = image;
}
else
{
G_Memory.Free(image);
}
}
}
}
else if (NT_SUCCESS(status))
{
status = STATUS_INVALID_BUFFER_SIZE;
}
ZwClose(process);
return status; return status;
} }
#pragma code_seg() #pragma code_seg()
+6
View File
@@ -121,12 +121,18 @@
<FilesToPackage Include="$(TargetPath)" /> <FilesToPackage Include="$(TargetPath)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="Domito.Certificate.cpp" />
<ClCompile Include="Domito.Consumer.cpp" />
<ClCompile Include="Domito.CodeIntegrity.cpp" /> <ClCompile Include="Domito.CodeIntegrity.cpp" />
<ClCompile Include="Domito.CodeIntegrity.Validation.cpp" />
<ClCompile Include="Domito.cpp" /> <ClCompile Include="Domito.cpp" />
<ClCompile Include="Domito.Memory.cpp" /> <ClCompile Include="Domito.Memory.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\include\Domito.MinCrypt.h" /> <ClInclude Include="..\include\Domito.MinCrypt.h" />
<ClInclude Include="..\include\Domito.MinCrypt.26100.h" />
<ClInclude Include="..\include\Domito.MinCrypt.Historical.h" />
<ClInclude Include="..\include\Domito.Consumer.h" />
<ClInclude Include="..\include\Domito.h" /> <ClInclude Include="..\include\Domito.h" />
<ClInclude Include="Domito.Internal.h" /> <ClInclude Include="Domito.Internal.h" />
</ItemGroup> </ItemGroup>
+18
View File
@@ -28,6 +28,15 @@
<ClCompile Include="Domito.Memory.cpp"> <ClCompile Include="Domito.Memory.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="Domito.CodeIntegrity.Validation.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Domito.Certificate.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Domito.Consumer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Domito.CodeIntegrity.cpp"> <ClCompile Include="Domito.CodeIntegrity.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
@@ -36,9 +45,18 @@
<ClInclude Include="Domito.Internal.h"> <ClInclude Include="Domito.Internal.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\include\Domito.Consumer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\include\Domito.h"> <ClInclude Include="..\include\Domito.h">
<Filter>Header Files\Public</Filter> <Filter>Header Files\Public</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\include\Domito.MinCrypt.26100.h">
<Filter>Header Files\Public</Filter>
</ClInclude>
<ClInclude Include="..\include\Domito.MinCrypt.Historical.h">
<Filter>Header Files\Public</Filter>
</ClInclude>
<ClInclude Include="..\include\Domito.MinCrypt.h"> <ClInclude Include="..\include\Domito.MinCrypt.h">
<Filter>Header Files\Public</Filter> <Filter>Header Files\Public</Filter>
</ClInclude> </ClInclude>
+109
View File
@@ -0,0 +1,109 @@
# Regression tests
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
```powershell
python tests\run-digest-tests.py
python tests\run-common-tests.py
python tests\run-ci-probe-tests.py
python tests\run-ci-validation-tests.py
```
Build outputs are temporary. No driver is installed or loaded. Test runners
extract the actual production functions or include the probe body unchanged;
kernel calls, CI calls, and pool allocation are replaced with test doubles.
The digest harness uses real BCrypt, Crypt32 and the Windows PE SIP provider.
## Digest and legacy verification tests
Coverage includes PE32/PE32+ files, all four supported hash algorithms, checksum
and certificate exclusions, overlays, reverse section-header order, multiple
certificate entries, certificate padding, unsupported section layouts, bounded
DER algorithm discovery, SHA-1 pattern decoys, truncated ASN.1, explicit catalog
policy forwarding and cleanup between failed/successful CI attempts.
The test compares digests against `CryptSIPCreateIndirectData` and reads the
installed system `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` as a
real-file corpus. For embedded signatures, Crypt32 independently decodes the SPC
algorithm and expected file digest. These checks do not verify signature trust.
Guard pages cover truncated headers and invalid ranges in both compatibility
and strict paths. Fault injection covers allocation/hash failures, deterministic
outputs, short certificate reads and digest ownership. A DOS stub larger than
4 MiB exercises the removed scratch-buffer limit. Older entry points retain
legacy selection/layout behavior; strict Ex APIs enforce their documented subset.
## File and export helper tests
The common harness checks explicit and zero offsets, exact/short/failed reads,
pending completion, event/object cleanup, and error propagation. Export fixtures
cover nonidentity name-ordinal mappings, forwarders, empty slots, invalid spans,
unbounded strings, and deterministic failure outputs. The export API requires a
valid loader-mapped module; it is not an arbitrary file parser.
## CI validation result and selection helpers
`run-ci-validation-tests.py` compiles the production wrapper source unchanged
(apart from replacing its private include with test dependencies) and extracts
the public declarations/constants. It checks native flag/level forwarding,
64-byte capacity on every call, the opt-in x64 experimental bit, non-x64 mode
rejection, invalid inputs, returned hash invariants, deterministic failure
scalars, preserved diagnostic policies, and result cleanup/reuse. The embedded
path covers all four explicit algorithms, unsupported/unsigned/invalid layouts,
partial digest allocation cleanup, CI failure propagation and no catalog retry.
CI/hash-layout behavior is injected; real digest behavior is checked by the
existing Windows SIP/Crypt32 differential harness. A second host build exercises
the non-x64 conditional branch; real ARM64 compilation is a separate build check.
The first kernel trace used direct CI calls. Probe v2 in CodeIntegrity-RE now
consumes these library helpers; a returned v2 trace is needed to claim runtime
verification of the wrappers themselves. The earlier native result is not
silently relabelled as an API-wrapper test.
## File-object validation flag probe
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
for `CiValidateFileObject` flag `0x20`. The host runner tests probe plumbing with
mock CI. The kernel probe must still run in a suitable VM to verify actual
policy behavior.
## Kernel compile checks and remaining validation
`mincrypt-layout-tests.cpp` asserts default modern x64/ARM64 and pre-Windows-10
compile layouts. On x64 it also includes the opt-in observed 26100 header's 14
layout assertions. Compile it with the same WDK/kernel architecture flags and
include paths as the library. This checks structure definitions, not runtime
compatibility on those systems. `ci-file-object-probe.cpp` is separately compiled
as kernel code for x64/ARM64; its real CI behavior is still untested.
The host tests do not establish CI trust policy, real asynchronous kernel I/O,
real policy ownership under Driver Verifier, revocation/timestamp behavior, or
all Windows-version/layout combinations. See the
[implementation ledger](../docs/implementation-status.md) for the remaining work.
## Consumer helper regression tests
Run `python tests/run-consumer-helper-tests.py` in an x64 Visual Studio developer
shell. This fifth host runner compiles the actual certificate/common-consumer
implementations and the existing PID name function, with kernel operations
replaced by deterministic doubles. Hashing uses real Windows CNG; independent
Crypt32 decoding/re-encoding/hash calls verify SPKI representation and pins.
The synthetic v1/v3 certificates are framing fixtures, not trust fixtures.
Coverage includes truncated certificate prefixes at inaccessible guard pages,
malformed DER lengths and optional read-only comparisons with real system-root
certificate SPKIs,
four hash algorithms, short capacities and four injected CNG failures, strict
hex conversion across all byte values, path/handle ownership, failed references,
partial publisher allocation, process-name query failures, stale-output cleanup,
and malformed/overflowing x64 chain spans. A second host build exercises the
non-x64 rejection branch without claiming ARM64 ABI or runtime emulation.
No service is started, certificate trusted, or real CI policy evaluated.
## Deprecation compiler checks
Run `python tests/run-deprecation-tests.py --arch x64` or `--arch ARM64` from a
Visual Studio developer shell with the WDK installed. This compile-only runner
uses the actual public headers: modern API references must compile under /WX,
while each of four deprecated references must fail specifically with C4996 and
its migration message. Both C and C++ are checked. It does not link or load code.
+175
View File
@@ -0,0 +1,175 @@
// Host tests validate the probe plumbing, NOT CI trust behavior.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "ci-probe-constants.inc"
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define DOMITO_CI_PROBE_HOST_TEST
typedef void* PFILE_OBJECT;
struct MINCRYPT_POLICY_INFO
{
ULONG Size;
ULONG VerificationStatus;
ULONG PolicyBits;
void* ChainInfo;
};
static unsigned calls;
static unsigned frees;
static unsigned outstanding;
static unsigned checks;
static bool failureMode;
static int fileToken;
static int chainToken;
static void
Check(
bool value,
const char* label
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", label);
std::exit(1);
}
}
static NTSTATUS
CiValidateFileObject(
PFILE_OBJECT file,
ULONG flags,
ULONG level,
MINCRYPT_POLICY_INFO* policy,
MINCRYPT_POLICY_INFO* timestamp,
LARGE_INTEGER* time,
UCHAR* hash,
UINT32* size,
ALG_ID* algorithm
)
{
const ULONG expectedFlags[] = {0, 0x20, 0x20, 0, 0, 0x20};
Check(calls < 6 && flags == expectedFlags[calls], "paired flags and order");
Check(file == &fileToken && level == 0, "same file and level zero");
Check(outstanding == 0, "previous outputs freed before next call");
Check(policy->Size == sizeof(*policy) && timestamp->Size == sizeof(*timestamp), "policy sizes");
Check(
!policy->ChainInfo && !timestamp->ChainInfo && !policy->PolicyBits && !timestamp->PolicyBits,
"fresh policy objects"
);
Check(*size == (calls < 4 ? 64u : 63u), "capacity reset for every call");
Check(*algorithm == 0 && time->QuadPart == 0, "fresh scalar outputs");
for (unsigned i = 0; i < 64; ++i)
{
Check(hash[i] == 0, "fresh hash buffer");
}
++calls;
if (*size < 64)
{
*size = 64;
return STATUS_BUFFER_TOO_SMALL;
}
policy->ChainInfo = timestamp->ChainInfo = &chainToken;
outstanding = 2;
policy->PolicyBits = 0xA5;
timestamp->PolicyBits = 0x5A;
policy->VerificationStatus = failureMode ? STATUS_INVALID_IMAGE_HASH : STATUS_SUCCESS;
timestamp->VerificationStatus = STATUS_SUCCESS;
// Also allocate on simulated failure to exercise unconditional cleanup.
if (failureMode)
{
return STATUS_INVALID_IMAGE_HASH;
}
std::memset(hash, 0xCC, 32);
*size = 32;
*algorithm = 0x800C;
time->QuadPart = 123;
return STATUS_SUCCESS;
}
static void
CiFreePolicyInfo(
MINCRYPT_POLICY_INFO* policy
)
{
++frees;
if (policy->ChainInfo)
{
Check(outstanding > 0, "owned chain");
--outstanding;
}
*policy = {};
}
#include "ci-file-object-probe.cpp"
int
main()
{
for (unsigned pass = 0; pass < 2; ++pass)
{
failureMode = pass != 0;
calls = frees = outstanding = 0;
DOMITO_CI_PROBE_RESULT results[DOMITO_CI_PROBE_RESULT_COUNT];
std::memset(results, 0xFF, sizeof(results));
DomitoProbeCiValidateFileObject(&fileToken, results);
Check(calls == 6 && frees == 12 && outstanding == 0, "all calls and frees");
for (unsigned i = 0; i < 6; ++i)
{
const auto& r = results[i];
Check(r.HashGuardIntact, "buffer guard");
const NTSTATUS expected = i >= 4 ? STATUS_BUFFER_TOO_SMALL
: failureMode ? STATUS_INVALID_IMAGE_HASH
: STATUS_SUCCESS;
Check(r.Status == expected, "status preserved");
if (i < 4)
{
Check(r.PolicyBits == 0xA5 && r.TimeStampPolicyBits == 0x5A, "policy capture");
Check(r.HasChain && r.HasTimeStampChain, "chain presence captured before free");
}
if (NT_SUCCESS(r.Status))
{
Check(
r.HashSize == 32 && r.HashAlgId == 0x800C && r.SigningTime.QuadPart == 123,
"digest metadata and signing time"
);
for (unsigned j = 0; j < 64; ++j)
{
Check(r.Hash[j] == (j < 32 ? 0xCC : 0), "digest copied and tail cleared");
}
}
else
{
for (auto byte : r.Hash)
{
Check(byte == 0, "no digest from failed call");
}
}
if (i >= 4)
{
Check(r.HashSize == 64, "required capacity captured");
}
}
}
std::printf("PASS: %u probe checks (mock CI; no runtime trust validation)\n", checks);
}
+72
View File
@@ -0,0 +1,72 @@
// Compile into an existing signed test driver; this file has no DriverEntry.
#ifndef DOMITO_CI_PROBE_HOST_TEST
#include <ntifs.h>
#include "Domito.MinCrypt.h"
#endif
#include "ci-file-object-probe.h"
_IRQL_requires_max_(PASSIVE_LEVEL)
void
DomitoProbeCiValidateFileObject(
PFILE_OBJECT FileObject,
DOMITO_CI_PROBE_RESULT* Results
)
{
const ULONG flags[DOMITO_CI_PROBE_RESULT_COUNT] = {
0,
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
0,
0,
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE
};
for (ULONG i = 0; i < DOMITO_CI_PROBE_RESULT_COUNT; ++i)
{
MINCRYPT_POLICY_INFO policy = {};
MINCRYPT_POLICY_INFO timestamp = {};
policy.Size = sizeof(policy);
timestamp.Size = sizeof(timestamp);
struct
{
UCHAR Bytes[MINCRYPT_MAX_HASH_LENGTH];
ULONG Guard;
} hash = {};
hash.Guard = 0xD01170AB;
DOMITO_CI_PROBE_RESULT& result = Results[i];
result = {};
result.Flags = flags[i];
result.InputCapacity = i < 4 ? sizeof(hash.Bytes) : sizeof(hash.Bytes) - 1;
result.HashSize = result.InputCapacity;
result.Status = CiValidateFileObject(
FileObject,
result.Flags,
0,
&policy,
&timestamp,
&result.SigningTime,
hash.Bytes,
&result.HashSize,
&result.HashAlgId
);
result.HashGuardIntact = hash.Guard == 0xD01170AB;
// Keep failure outputs as raw observations; only successful calls have
// a usable digest. Do not dereference the incompletely mapped chains.
result.PolicyBits = policy.PolicyBits;
result.TimeStampPolicyBits = timestamp.PolicyBits;
result.VerificationStatus = (NTSTATUS)policy.VerificationStatus;
result.TimeStampVerificationStatus = (NTSTATUS)timestamp.VerificationStatus;
result.HasChain = policy.ChainInfo != nullptr;
result.HasTimeStampChain = timestamp.ChainInfo != nullptr;
if (NT_SUCCESS(result.Status) && result.HashSize <= sizeof(result.Hash))
{
RtlCopyMemory(result.Hash, hash.Bytes, result.HashSize);
}
CiFreePolicyInfo(&policy);
CiFreePolicyInfo(&timestamp);
}
}
+31
View File
@@ -0,0 +1,31 @@
#pragma once
// Include ntifs.h and Domito.MinCrypt.h first. Test support, not a library API.
typedef struct _DOMITO_CI_PROBE_RESULT
{
ULONG Flags;
ULONG InputCapacity;
NTSTATUS Status;
UINT32 HashSize;
ALG_ID HashAlgId;
UCHAR Hash[MINCRYPT_MAX_HASH_LENGTH];
LARGE_INTEGER SigningTime;
ULONG PolicyBits;
ULONG TimeStampPolicyBits;
NTSTATUS VerificationStatus;
NTSTATUS TimeStampVerificationStatus;
BOOLEAN HasChain;
BOOLEAN HasTimeStampChain;
BOOLEAN HashGuardIntact;
} DOMITO_CI_PROBE_RESULT;
#define DOMITO_CI_PROBE_RESULT_COUNT 6
// PASSIVE_LEVEL; referenced file object; file bytes must remain stable.
// Writes observations, not an overall trust verdict. Frees both policies per call.
_IRQL_requires_max_(PASSIVE_LEVEL)
void
DomitoProbeCiValidateFileObject(
_In_ PFILE_OBJECT FileObject,
_Out_writes_(DOMITO_CI_PROBE_RESULT_COUNT) DOMITO_CI_PROBE_RESULT* Results
);
+519
View File
@@ -0,0 +1,519 @@
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#ifndef _IRQL_requires_max_
#define _IRQL_requires_max_(x)
#endif
#define PASSIVE_LEVEL 0
#define PAGED_CODE() ((void)0)
typedef void* PFILE_OBJECT;
typedef void* LPWIN_CERTIFICATE;
struct MINCRYPT_POLICY_INFO
{
ULONG Size;
NTSTATUS VerificationStatus;
ULONG PolicyBits;
void* ChainInfo;
};
#include "ci-validation-api.inc"
static unsigned checks;
static unsigned live;
static unsigned validationCalls;
static unsigned embeddedCalls;
static unsigned digestCalls;
static unsigned digestFrees;
static NTSTATUS nativeStatus = STATUS_SUCCESS;
static NTSTATUS embeddedStatus = STATUS_SUCCESS;
static NTSTATUS digestStatus = STATUS_SUCCESS;
static UINT32 nativeAlgorithm = CALG_SHA_256;
static UINT32 nativeSize = 32;
static ULONG fakeDigestSize = 32;
static bool omitCertificate;
static bool omitDigest;
static bool partialDigestOnFailure;
static const UCHAR image[] = {1, 2, 3};
static ULONG observedFlags;
static ULONG observedLevel;
static ULONG observedCapacity;
static PFILE_OBJECT observedObject;
static ALG_ID observedEmbeddedAlgorithm;
static ULONG observedSecuritySize;
static void* observedCertificate;
static void
Check(
bool value,
const char* message
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", message);
std::exit(1);
}
}
static void*
Allocate(
SIZE_T size
)
{
void* value = std::malloc(size);
Check(value != nullptr, "host allocation");
++live;
return value;
}
static void
Release(
void* value
)
{
Check(value && live, "single allocation owner");
--live;
std::free(value);
}
static void
CiFreePolicyInfo(
MINCRYPT_POLICY_INFO* policy
)
{
if (policy->ChainInfo)
{
Release(policy->ChainInfo);
}
*policy = {};
}
static void
Populate(
MINCRYPT_POLICY_INFO* policy,
MINCRYPT_POLICY_INFO* timestamp
)
{
Check(!policy->ChainInfo && !timestamp->ChainInfo, "old result freed before reuse");
policy->Size = timestamp->Size = sizeof(*policy);
policy->ChainInfo = Allocate(1);
timestamp->ChainInfo = Allocate(1);
policy->PolicyBits = 0x123;
timestamp->PolicyBits = 0x456;
policy->VerificationStatus = STATUS_INVALID_IMAGE_HASH;
}
static NTSTATUS
CiValidateFileObject(
PFILE_OBJECT object,
UINT32 flags,
UINT32 level,
MINCRYPT_POLICY_INFO* policy,
MINCRYPT_POLICY_INFO* timestamp,
LARGE_INTEGER* time,
UINT8* hash,
UINT32* size,
ALG_ID* algorithm
)
{
++validationCalls;
observedObject = object;
observedFlags = flags;
observedLevel = level;
observedCapacity = *size;
Check(*size == 64, "fresh native hash capacity");
Check(!time->QuadPart && !*algorithm, "native scalar outputs initialized");
for (unsigned i = 0; i < 64; ++i)
{
Check(!hash[i], "native digest starts empty");
}
Populate(policy, timestamp);
time->QuadPart = 99;
std::memset(hash, 0x5a, 64);
*size = nativeSize;
*algorithm = nativeAlgorithm;
return nativeStatus;
}
static NTSTATUS
DomitoCalculatePortableExecutableDigestEx(
const UCHAR* bytes,
ULONG size,
UINT32 algorithm,
PULONG digestSize,
PVOID* digest,
LPWIN_CERTIFICATE* certificate,
PULONG securitySize
)
{
++digestCalls;
Check(bytes == image && size == sizeof(image), "same PE snapshot forwarded");
Check(
algorithm == CALG_SHA1 || algorithm == CALG_SHA_256 || algorithm == CALG_SHA_384 || algorithm == CALG_SHA_512,
"supported digest choice forwarded"
);
if (NT_SUCCESS(digestStatus) || partialDigestOnFailure)
{
*digest = omitDigest ? nullptr : Allocate(64);
if (*digest)
{
std::memset(*digest, 0xa5, 64);
}
*digestSize = fakeDigestSize;
*certificate = omitCertificate ? nullptr : const_cast<UCHAR*>(image);
*securitySize = omitCertificate ? 0 : 123;
}
return digestStatus;
}
static void
DomitoFreePortableExecutableDigest(
PVOID digest
)
{
++digestFrees;
Release(digest);
}
static NTSTATUS
CiCheckSignedFile(
PVOID hash,
UINT32 size,
ALG_ID algorithm,
LPWIN_CERTIFICATE certificate,
ULONG securitySize,
MINCRYPT_POLICY_INFO* policy,
LARGE_INTEGER* time,
MINCRYPT_POLICY_INFO* timestamp
)
{
++embeddedCalls;
observedEmbeddedAlgorithm = algorithm;
observedSecuritySize = securitySize;
observedCertificate = certificate;
for (UINT32 i = 0; i < size; ++i)
{
Check(static_cast<UCHAR*>(hash)[i] == 0xa5, "computed digest forwarded unchanged");
}
Populate(policy, timestamp);
time->QuadPart = 123;
return embeddedStatus;
}
#ifdef DOMITO_TEST_OTHER_ARCH
#undef _M_AMD64
#undef _AMD64_
#endif
#include "ci-validation-under-test.inc"
static void
CheckEmptyDigest(
const DOMITO_CI_VALIDATION_RESULT& result
)
{
Check(!result.HashSize && !result.HashAlgId && !result.SigningTime.QuadPart, "failure digest/time cleared");
for (auto value : result.Hash)
{
Check(!value, "failure hash bytes cleared");
}
}
static void
Clear(
DOMITO_CI_VALIDATION_RESULT* result
)
{
DomitoFreeCiValidationResult(result);
DOMITO_CI_VALIDATION_RESULT empty = {};
Check(std::memcmp(result, &empty, sizeof(empty)) == 0, "full result cleared by destructor");
Check(live == 0, "no leaked policy/digest allocation");
}
int
main()
{
DOMITO_CI_VALIDATION_RESULT result = {};
PFILE_OBJECT object = reinterpret_cast<PFILE_OBJECT>(static_cast<ULONG_PTR>(0x1234));
DomitoFreeCiValidationResult(nullptr);
Clear(&result);
Clear(&result);
Check(
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, nullptr) == STATUS_INVALID_PARAMETER,
"null native result"
);
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, nullptr) ==
STATUS_INVALID_PARAMETER,
"null embedded result"
);
Check(!validationCalls && !embeddedCalls && !digestCalls, "null result made no validation calls");
for (UINT32 flags : {0UL, 0x20UL, 0xa0UL, 0x80000024UL})
{
for (UINT8 level : {UINT8(0), UINT8(7), UINT8(255)})
{
result.HashSize = 1;
result.HashAlgId = 0xdead;
Check(
DomitoValidateFileObjectEx(object, flags, level, DomitoCiSignatureSelectionNative, &result) ==
STATUS_SUCCESS,
"native status"
);
Check(
observedObject == object && observedFlags == flags && observedLevel == level && observedCapacity == 64,
"native inputs unchanged"
);
Check(
result.FileObjectValidationFlags == flags && result.RequestedSigningLevel == level,
"export input controls recorded"
);
Check(
result.HashSize == 32 && result.HashAlgId == CALG_SHA_256 && result.Hash[0] == 0x5a &&
result.SigningTime.QuadPart == 99,
"native result"
);
// API return success does not silently reinterpret a policy status.
Check(result.PolicyInfo.VerificationStatus == STATUS_INVALID_IMAGE_HASH, "policy observation retained");
Clear(&result);
}
}
const unsigned beforeExperimental = validationCalls;
NTSTATUS experimental =
DomitoValidateFileObjectEx(object, 0x20, 9, DomitoCiSignatureSelectionFirstSuccessfulExperimental, &result);
#ifdef DOMITO_TEST_OTHER_ARCH
Check(
experimental == STATUS_NOT_SUPPORTED && validationCalls == beforeExperimental,
"experimental mode excluded on non-x64"
);
CheckEmptyDigest(result);
#else
Check(experimental == STATUS_SUCCESS && validationCalls == beforeExperimental + 1, "experimental native call");
Check(observedFlags == 0xa0 && observedLevel == 9, "only experimental bit added");
#endif
Clear(&result);
const unsigned beforeInvalid = validationCalls;
Check(
DomitoValidateFileObjectEx(nullptr, 0x20, 0, DomitoCiSignatureSelectionNative, &result) ==
STATUS_INVALID_PARAMETER,
"null file object"
);
Check(
DomitoValidateFileObjectEx(object, 0x20, 0, static_cast<DOMITO_CI_SIGNATURE_SELECTION>(-1), &result) ==
STATUS_INVALID_PARAMETER,
"invalid selection"
);
Check(validationCalls == beforeInvalid, "invalid requests never call validation");
Clear(&result);
for (NTSTATUS failure : {STATUS_INVALID_IMAGE_HASH, STATUS_BUFFER_TOO_SMALL, STATUS_INSUFFICIENT_RESOURCES})
{
nativeStatus = failure;
Check(
DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == failure,
"native failure preserved"
);
CheckEmptyDigest(result);
Check(live == 2 && result.PolicyInfo.PolicyBits == 0x123, "failure policy diagnostics remain owned");
Clear(&result);
}
nativeStatus = STATUS_SUCCESS;
nativeSize = 65;
Check(
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) ==
STATUS_INVALID_BUFFER_SIZE,
"oversized returned hash rejected"
);
CheckEmptyDigest(result);
Clear(&result);
nativeSize = 31;
Check(
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) ==
STATUS_INVALID_BUFFER_SIZE,
"algorithm length mismatch rejected"
);
Clear(&result);
nativeSize = 32;
nativeAlgorithm = 0xdead;
Check(
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_NOT_SUPPORTED,
"unknown returned algorithm rejected"
);
Clear(&result);
for (ALG_ID algorithm : {CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512})
{
nativeAlgorithm = algorithm;
nativeSize = DomitoCiDigestSize(algorithm);
Check(
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS,
"known native digest sizes"
);
for (UINT32 i = result.HashSize; i < sizeof(result.Hash); ++i)
{
Check(!result.Hash[i], "unused native digest bytes cleared");
}
Clear(&result);
fakeDigestSize = DomitoCiDigestSize(algorithm);
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), algorithm, &result) ==
STATUS_SUCCESS,
"embedded success"
);
Check(
observedEmbeddedAlgorithm == algorithm && observedSecuritySize == 123 && observedCertificate == image,
"embedded certificate/algorithm forwarded"
);
Check(
result.HashSize == fakeDigestSize && result.HashAlgId == algorithm && result.Hash[0] == 0xa5 &&
result.SigningTime.QuadPart == 123,
"embedded result owns copied digest"
);
Check(
!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
"embedded metadata and digest cleanup"
);
Clear(&result);
}
nativeAlgorithm = CALG_SHA_256;
nativeSize = 32;
fakeDigestSize = 32;
nativeStatus = STATUS_MORE_ENTRIES;
Check(
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_MORE_ENTRIES,
"nonnegative native status preserved"
);
Clear(&result);
nativeStatus = STATUS_SUCCESS;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(nullptr, 0, CALG_SHA_256, &result) ==
STATUS_INVALID_PARAMETER,
"null PE"
);
unsigned beforeDigest = digestCalls;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), 0xdead, &result) ==
STATUS_NOT_SUPPORTED,
"unsupported algorithm"
);
Check(digestCalls == beforeDigest, "unsupported algorithm does not hash");
for (NTSTATUS failure : {STATUS_NOT_SUPPORTED, STATUS_INVALID_IMAGE_FORMAT, STATUS_INSUFFICIENT_RESOURCES})
{
digestStatus = failure;
unsigned beforeEmbedded = embeddedCalls;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == failure,
"digest failure propagation"
);
Check(embeddedCalls == beforeEmbedded, "digest failure never calls CI");
CheckEmptyDigest(result);
Clear(&result);
}
partialDigestOnFailure = true;
unsigned beforeFree = digestFrees;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INSUFFICIENT_RESOURCES,
"partial digest failure"
);
Check(digestFrees == beforeFree + 1, "partial digest allocation freed");
Clear(&result);
partialDigestOnFailure = false;
digestStatus = STATUS_SUCCESS;
omitCertificate = true;
unsigned beforeEmbedded = embeddedCalls;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_IMAGE_HASH,
"unsigned PE rejected"
);
Check(embeddedCalls == beforeEmbedded, "unsigned PE does not call CI");
Clear(&result);
omitCertificate = false;
omitDigest = true;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_BUFFER_SIZE,
"missing digest rejected"
);
Clear(&result);
omitDigest = false;
fakeDigestSize = 31;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_BUFFER_SIZE,
"digest helper length mismatch"
);
Clear(&result);
fakeDigestSize = 32;
embeddedStatus = STATUS_INVALID_IMAGE_HASH;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_IMAGE_HASH,
"CI embedded failure propagated without fallback"
);
CheckEmptyDigest(result);
Check(live == 2 && result.TimeStampPolicyInfo.PolicyBits == 0x456, "embedded failure diagnostics owned");
Clear(&result);
embeddedStatus = STATUS_SUCCESS;
for (unsigned i = 0; i < 16; ++i)
{
Check(
DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS,
"repeated native"
);
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_SUCCESS,
"reuse native result as embedded result"
);
Check(
!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
"reuse resets metadata without leaking"
);
}
// Invalid calls must release a previously valid result, too.
Check(
DomitoValidateFileObjectEx(nullptr, 0, 0, DomitoCiSignatureSelectionNative, &result) ==
STATUS_INVALID_PARAMETER,
"invalid reuse cleans old result"
);
Clear(&result);
std::printf(
"PASS: %u CI validation checks (%s architecture branch), no live allocations\n",
checks,
#ifdef DOMITO_TEST_OTHER_ARCH
"non-x64"
#else
"x64"
#endif
);
}
+395
View File
@@ -0,0 +1,395 @@
// Tests extract the unchanged production bodies. Kernel I/O is simulated.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <cctype>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define MAXUSHORT 0xffff
#define Executive 0
#define KernelMode 0
#define NotificationEvent 0
#define OBJ_KERNEL_HANDLE 0x200
#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f))
struct OBJECT_ATTRIBUTES
{
ULONG Attributes;
};
struct ANSI_STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
};
typedef ANSI_STRING* PANSI_STRING;
struct IO_STATUS_BLOCK
{
NTSTATUS Status;
ULONG_PTR Information;
};
static int objectType;
static void* objectTypeValue = &objectType;
static void** ExEventObjectType = &objectTypeValue;
static unsigned checks;
static void
Check(
bool value,
const char* label
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", label);
std::exit(1);
}
}
static LONG
RtlCompareString(
PANSI_STRING a,
PANSI_STRING b,
BOOLEAN insensitive
)
{
if (a->Length != b->Length)
{
return 1;
}
for (USHORT i = 0; i < a->Length; ++i)
{
unsigned char x = (unsigned char)a->Buffer[i], y = (unsigned char)b->Buffer[i];
if (insensitive)
{
x = (unsigned char)std::tolower(x);
y = (unsigned char)std::tolower(y);
}
if (x != y)
{
return 1;
}
}
return 0;
}
static void* exportPointer;
static ULONG exportBytes;
static PVOID
Directory(
PVOID,
BOOLEAN mapped,
USHORT index,
PULONG bytes
)
{
Check(mapped && index == IMAGE_DIRECTORY_ENTRY_EXPORT, "mapped export lookup");
*bytes = exportBytes;
return exportPointer;
}
struct
{
PVOID (*RtlImageDirectoryEntryToData)(
PVOID,
BOOLEAN,
USHORT,
PULONG
);
} G_Common = {Directory};
static int eventToken;
static int fileToken;
static unsigned created;
static unsigned referenced;
static unsigned closed;
static unsigned dereferenced;
static unsigned reads;
static unsigned waits;
static int ioMode;
static bool outstanding;
static IO_STATUS_BLOCK* pendingIosb;
static ULONG requested;
static LONGLONG expectedOffset;
static NTSTATUS
ZwCreateEvent(
HANDLE* event,
ACCESS_MASK access,
OBJECT_ATTRIBUTES* attributes,
int type,
BOOLEAN signaled
)
{
Check(
access == EVENT_ALL_ACCESS && attributes->Attributes == OBJ_KERNEL_HANDLE && type == NotificationEvent &&
!signaled,
"private kernel event"
);
if (ioMode == 4)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
++created;
*event = &eventToken;
return STATUS_SUCCESS;
}
static NTSTATUS
ObReferenceObjectByHandle(
HANDLE event,
ACCESS_MASK access,
void* type,
int mode,
void** object,
void*
)
{
Check(
event == &eventToken && access == SYNCHRONIZE && type == *ExEventObjectType && mode == KernelMode,
"reference own event"
);
if (ioMode == 5)
{
return STATUS_ACCESS_DENIED;
}
++referenced;
*object = &eventToken;
return STATUS_SUCCESS;
}
static void
ZwClose(
HANDLE event
)
{
Check(event == &eventToken && !outstanding, "close only after completion");
++closed;
}
static void
ObDereferenceObject(
void* object
)
{
Check(object == &eventToken && !outstanding, "dereference only after completion");
++dereferenced;
}
static NTSTATUS
ZwReadFile(
HANDLE file,
HANDLE event,
void*,
void*,
IO_STATUS_BLOCK* iosb,
void*,
ULONG size,
LARGE_INTEGER* offset,
void*
)
{
++reads;
Check(
file == &fileToken && event == &eventToken && offset && offset->QuadPart == expectedOffset,
"file and explicit offset"
);
Check(iosb->Status == 0 && iosb->Information == 0, "initialized IOSB");
requested = size;
if (ioMode == 6)
{
return STATUS_ACCESS_DENIED;
}
if (ioMode == 1 || ioMode == 2)
{
outstanding = true;
pendingIosb = iosb;
return STATUS_PENDING;
}
iosb->Status = STATUS_SUCCESS;
iosb->Information = ioMode == 3 ? size - 1 : size;
return STATUS_SUCCESS;
}
static NTSTATUS
KeWaitForSingleObject(
void* object,
int reason,
int mode,
BOOLEAN alertable,
LARGE_INTEGER* timeout
)
{
++waits;
Check(
object == &eventToken && reason == Executive && mode == KernelMode && !alertable && !timeout && outstanding,
"noninterruptible per-request wait"
);
outstanding = false;
pendingIosb->Status = ioMode == 2 ? STATUS_END_OF_FILE : STATUS_SUCCESS;
pendingIosb->Information = ioMode == 2 ? 0 : requested;
return STATUS_SUCCESS;
}
#include "common-under-test.inc"
static std::vector<UCHAR>
Image()
{
std::vector<UCHAR> image(4096);
auto dos = (IMAGE_DOS_HEADER*)image.data();
dos->e_magic = IMAGE_DOS_SIGNATURE;
dos->e_lfanew = 128;
auto nt = (IMAGE_NT_HEADERS64*)(image.data() + 128);
nt->Signature = IMAGE_NT_SIGNATURE;
nt->OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC;
nt->OptionalHeader.SizeOfImage = (ULONG)image.size();
auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
exports->NumberOfFunctions = 3;
exports->NumberOfNames = 2;
exports->AddressOfFunctions = 801;
exports->AddressOfNames = 901;
exports->AddressOfNameOrdinals = 1001;
const ULONG functions[] = {2000, 2100, 2200};
const ULONG names[] = {1100, 1120};
const USHORT ordinals[] = {2, 0};
std::memcpy(image.data() + 801, functions, sizeof(functions));
std::memcpy(image.data() + 901, names, sizeof(names));
std::memcpy(image.data() + 1001, ordinals, sizeof(ordinals));
std::memcpy(image.data() + 1100, "Beta", 5);
std::memcpy(image.data() + 1120, "Alpha", 6);
return image;
}
static NTSTATUS
Lookup(
std::vector<UCHAR>& image,
const char* name,
PVOID* output
)
{
ANSI_STRING string = {(USHORT)std::strlen(name), (USHORT)std::strlen(name), (PCHAR)name};
exportPointer = image.data() + 512;
exportBytes = 256;
return DomitoFindExportedFunctionAddress(image.data(), &string, output);
}
int
main()
{
auto image = Image();
PVOID output = nullptr;
Check(Lookup(image, "Beta", &output) == STATUS_SUCCESS && output == image.data() + 2200, "ordinal-indexed EAT");
Check(Lookup(image, "aLpHa", &output) == STATUS_SUCCESS && output == image.data() + 2000, "case-insensitive name");
Check(Lookup(image, "Alpha", nullptr) == STATUS_SUCCESS, "optional output discovery");
Check(Lookup(image, "missing", &output) == STATUS_NOT_FOUND && !output, "notfound clears output");
auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
const USHORT invalidOrdinal = 3;
std::memcpy(image.data() + 1001, &invalidOrdinal, 2);
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT && !output, "ordinal bounded");
image = Image();
const ULONG forwarder = 600;
std::memcpy(image.data() + 801 + 8, &forwarder, 4);
Check(Lookup(image, "Beta", &output) == STATUS_NOT_SUPPORTED && !output, "forwarder rejected");
image = Image();
const ULONG zero = 0;
std::memcpy(image.data() + 801 + 8, &zero, 4);
Check(Lookup(image, "Beta", &output) == STATUS_NOT_FOUND, "empty EAT slot");
image = Image();
const ULONG outside = 4096;
std::memcpy(image.data() + 801 + 8, &outside, 4);
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "function RVA bounded");
image = Image();
exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
exports->NumberOfNames = 0xffffffff;
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "table multiplication bounded");
image = Image();
exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
exports->AddressOfFunctions = 4090;
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "EAT span bounded");
image = Image();
const ULONG lateName = 4094;
std::memcpy(image.data() + 901, &lateName, 4);
image[4094] = 'x';
image[4095] = 'x';
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "unterminated name bounded");
image = Image();
std::memcpy(image.data() + 901, &outside, 4);
Check(Lookup(image, "Beta", &output) == STATUS_INVALID_IMAGE_FORMAT, "name RVA bounded");
image = Image();
ANSI_STRING name = {4, 4, (PCHAR) "Beta"};
exportPointer = image.data() + 512;
exportBytes = 4096;
Check(
DomitoFindExportedFunctionAddress(image.data(), &name, &output) == STATUS_INVALID_IMAGE_FORMAT,
"export directory span bounded"
);
output = (void*)1;
Check(
DomitoFindExportedFunctionAddress(nullptr, &name, &output) == STATUS_INVALID_PARAMETER && !output,
"invalid arguments clear output"
);
char buffer[32];
for (ioMode = 0; ioMode <= 6; ++ioMode)
{
created = referenced = closed = dereferenced = reads = waits = 0;
outstanding = false;
expectedOffset = 123;
LARGE_INTEGER offset;
offset.QuadPart = expectedOffset;
NTSTATUS status = DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), &offset);
const NTSTATUS expected[] = {
STATUS_SUCCESS,
STATUS_SUCCESS,
STATUS_END_OF_FILE,
STATUS_END_OF_FILE,
STATUS_INSUFFICIENT_RESOURCES,
STATUS_ACCESS_DENIED,
STATUS_ACCESS_DENIED
};
Check(status == expected[ioMode], "read status and exact-byte contract");
Check(created == closed && referenced == dereferenced && !outstanding, "event lifetime balanced");
Check(waits == ((ioMode == 1 || ioMode == 2) ? 1u : 0u), "wait pending only");
}
ioMode = 0;
expectedOffset = 0;
Check(DomitoReadFile(&fileToken, buffer, sizeof(buffer)) == STATUS_SUCCESS, "zero-offset wrapper");
LARGE_INTEGER offset = {};
offset.QuadPart = -1;
Check(
DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), &offset) == STATUS_INVALID_PARAMETER,
"negative offset rejected"
);
Check(
DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), nullptr) == STATUS_INVALID_PARAMETER,
"null offset rejected"
);
offset.QuadPart = 0;
Check(DomitoReadFileAt(&fileToken, nullptr, 0, &offset) == STATUS_SUCCESS, "zero byte read");
Check(DomitoReadFileAt(&fileToken, nullptr, 1, &offset) == STATUS_INVALID_PARAMETER, "null buffer rejected");
std::printf("PASS: %u common-helper checks (mock kernel I/O)\n", checks);
}
+967
View File
@@ -0,0 +1,967 @@
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <bcrypt.h>
#include <wincrypt.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <initializer_list>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define PASSIVE_LEVEL 0
#define PAGED_CODE() ((void)0)
#undef NTDDI_VERSION
#define NTDDI_VERSION 0x0a000008
#undef NTDDI_WIN10
#define NTDDI_WIN10 0x0a000000
#define DOMITO_POOL_TAG 0x446f6d69
#define MAXUSHORT 0xffff
#define PagedPool 1
#define KernelMode 0
#define ProcessImageFileName 27
#define OBJ_KERNEL_HANDLE 0x200
#define OBJ_CASE_INSENSITIVE 0x40
#define FILE_NON_DIRECTORY_FILE 0x40
#define FILE_SYNCHRONOUS_IO_NONALERT 0x20
typedef void* PEPROCESS;
typedef void* PFILE_OBJECT;
typedef const UCHAR* PCUCHAR;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _ANSI_STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} ANSI_STRING, *PANSI_STRING;
struct OBJECT_ATTRIBUTES
{
ULONG Attributes;
PUNICODE_STRING ObjectName;
};
struct CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
};
struct IO_STATUS_BLOCK
{
NTSTATUS Status;
ULONG_PTR Information;
};
#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f), (a)->ObjectName = (n))
#define RTL_CONSTANT_STRING(s) {sizeof(s) - sizeof((s)[0]), sizeof(s), const_cast<char*>(s)}
typedef PVOID(NTAPI* PMINCRYPT_ALLOCATE_ROUTINE)(SIZE_T);
typedef PVOID NTAPI MINCRYPT_ALLOCATE_ROUTINE(SIZE_T);
#include "consumer-api.inc"
static unsigned checks;
static unsigned live;
static unsigned refs;
static unsigned derefs;
static unsigned closed;
static unsigned validated;
static unsigned opened;
static unsigned queried;
static int failure;
static int cryptoFailure;
static NTSTATUS moduleStatus = STATUS_SUCCESS;
static NTSTATUS exportStatus = STATUS_SUCCESS;
static NTSTATUS validateStatus = STATUS_SUCCESS;
static int fileToken;
static int processToken;
static void* fileTypeValue = &fileToken;
static void** IoFileObjectType = &fileTypeValue;
static void
Check(
bool value,
const char* msg
)
{
++checks;
if (!value)
{
std::fprintf(stderr, "FAIL: %s\n", msg);
std::exit(1);
}
}
static void*
Allocate(
SIZE_T size
)
{
if (failure == 1)
{
return nullptr;
}
void* p = std::malloc(size);
Check(p != nullptr, "allocation");
++live;
std::memset(p, 0xa5, size);
return p;
}
static void
Release(
void* p
)
{
Check(p && live, "single owner release");
--live;
std::free(p);
}
static struct
{
PVOID (*Allocate)(SIZE_T);
void (*Free)(PVOID);
} G_Memory = {Allocate, Release};
static PVOID
ExAllocatePoolWithTag(
int,
SIZE_T size,
ULONG
)
{
return Allocate(size);
}
static void
ExFreePoolWithTag(
void* p,
ULONG
)
{
Release(p);
}
static void
ExFreePool(
void* p
)
{
Release(p);
}
static NTSTATUS
FakePublisher(
MINCERT_BLOB*,
PMINCRYPT_ALLOCATE_ROUTINE allocator,
PUNICODE_STRING name
)
{
name->Buffer = static_cast<PWCHAR>(allocator(12));
if (!name->Buffer)
{
return STATUS_INSUFFICIENT_RESOURCES;
}
std::memcpy(name->Buffer, L"hello", 12);
name->Length = 10;
name->MaximumLength = 12;
if (failure == 8)
{
return STATUS_UNSUCCESSFUL;
}
if (failure == 9)
{
name->Length = 13;
}
return STATUS_SUCCESS;
}
static NTSTATUS
DomitoFindModuleBaseAddress(
PANSI_STRING name,
PVOID* module
)
{
Check(name && name->Length, "module name");
if (NT_SUCCESS(moduleStatus))
{
*module = &fileToken;
}
return moduleStatus;
}
static NTSTATUS
DomitoFindExportedFunctionAddress(
PVOID module,
PANSI_STRING name,
PVOID* routine
)
{
Check(module == &fileToken && name->Length, "export resolution");
if (NT_SUCCESS(exportStatus))
{
*routine = reinterpret_cast<PVOID>(FakePublisher);
}
return exportStatus;
}
void
DomitoFreeCiValidationResult(
DOMITO_CI_VALIDATION_RESULT* result
)
{
if (result->PolicyInfo.ChainInfo)
{
Release(result->PolicyInfo.ChainInfo);
}
*result = {};
}
NTSTATUS
DomitoValidateFileObjectEx(
PFILE_OBJECT object,
UINT32 flags,
UINT8 level,
DOMITO_CI_SIGNATURE_SELECTION selection,
DOMITO_CI_VALIDATION_RESULT* result
)
{
++validated;
Check(
object == &fileToken && flags == 0x20 && level == 7 && selection == DomitoCiSignatureSelectionNative,
"CI arguments unchanged"
);
Check(!result->PolicyInfo.ChainInfo, "result reset before native call");
result->PolicyInfo.ChainInfo = static_cast<MINCRYPT_CHAIN_INFO*>(Allocate(1));
return validateStatus;
}
static NTSTATUS
ObReferenceObjectByHandle(
HANDLE handle,
ACCESS_MASK access,
void* type,
int mode,
void** object,
void*
)
{
Check(
handle == &fileToken && access == FILE_READ_DATA && type == *IoFileObjectType && mode == KernelMode,
"trusted handle reference"
);
if (failure == 2)
{
return STATUS_ACCESS_DENIED;
}
++refs;
*object = &fileToken;
return STATUS_SUCCESS;
}
static void
ObDereferenceObject(
void* object
)
{
Check(object == &fileToken, "file dereference");
++derefs;
}
static NTSTATUS
ZwOpenFile(
HANDLE* handle,
ACCESS_MASK access,
OBJECT_ATTRIBUTES* a,
IO_STATUS_BLOCK*,
ULONG share,
ULONG options
)
{
++opened;
Check(
access == (FILE_READ_DATA | SYNCHRONIZE) && a->Attributes == (OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE) &&
share == FILE_SHARE_READ && options == (FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT),
"read-only stable path open"
);
if (failure == 3)
{
return STATUS_OBJECT_NAME_NOT_FOUND;
}
*handle = &fileToken;
return STATUS_SUCCESS;
}
static void
ZwClose(
HANDLE handle
)
{
Check(handle == &fileToken || handle == &processToken, "close owned handle");
++closed;
}
static void
FillName(
PUNICODE_STRING name
)
{
name->Length = 8;
name->MaximumLength = 10;
name->Buffer = reinterpret_cast<PWCHAR>(name + 1);
std::memcpy(name->Buffer, L"test", 10);
}
static NTSTATUS
SeLocateProcessImageName(
PEPROCESS process,
PUNICODE_STRING* image
)
{
Check(process == &processToken, "referenced process passed unchanged");
if (failure == 4)
{
return STATUS_NOT_FOUND;
}
// OS allocations are independent of configurable Domito allocator failures.
*image = static_cast<PUNICODE_STRING>(std::malloc(sizeof(UNICODE_STRING) + 10));
++live;
FillName(*image);
if (failure == 5)
{
(*image)->Length = 9;
}
return STATUS_SUCCESS;
}
static NTSTATUS
ZwOpenProcess(
HANDLE* process,
ACCESS_MASK access,
OBJECT_ATTRIBUTES* a,
CLIENT_ID* cid
)
{
Check(
access == 0x1000 && a->Attributes == OBJ_KERNEL_HANDLE && cid->UniqueProcess == ULongToHandle(123),
"PID least privilege kernel open"
);
if (failure == 6)
{
return STATUS_ACCESS_DENIED;
}
*process = &processToken;
return STATUS_SUCCESS;
}
static NTSTATUS
QueryProcess(
HANDLE process,
int info,
PVOID buffer,
ULONG size,
PULONG returned
)
{
++queried;
Check(process == &processToken && info == ProcessImageFileName, "PID query bound to one handle");
*returned = sizeof(UNICODE_STRING) + 10;
if (!buffer)
{
return failure == 10 ? STATUS_SUCCESS : STATUS_INFO_LENGTH_MISMATCH;
}
Check(size == *returned, "sized name allocation");
FillName(static_cast<PUNICODE_STRING>(buffer));
if (failure == 7)
{
return STATUS_UNSUCCESSFUL;
}
if (failure == 11)
{
static_cast<PUNICODE_STRING>(buffer)->Buffer = reinterpret_cast<PWCHAR>(1);
}
return STATUS_SUCCESS;
}
static struct
{
NTSTATUS (*ZwQueryInformationProcess)(
HANDLE,
int,
PVOID,
ULONG,
PULONG
);
} G_Common = {QueryProcess};
static NTSTATUS
OpenAlgorithm(
BCRYPT_ALG_HANDLE* a,
LPCWSTR n,
LPCWSTR p,
ULONG f
)
{
return cryptoFailure == 1 ? STATUS_UNSUCCESSFUL : BCryptOpenAlgorithmProvider(a, n, p, f);
}
static NTSTATUS
CreateHash(
BCRYPT_ALG_HANDLE a,
BCRYPT_HASH_HANDLE* h,
PUCHAR o,
ULONG os,
PUCHAR s,
ULONG ss,
ULONG f
)
{
return cryptoFailure == 2 ? STATUS_UNSUCCESSFUL : BCryptCreateHash(a, h, o, os, s, ss, f);
}
static NTSTATUS
HashData(
BCRYPT_HASH_HANDLE h,
PUCHAR p,
ULONG n,
ULONG f
)
{
return cryptoFailure == 3 ? STATUS_UNSUCCESSFUL : BCryptHashData(h, p, n, f);
}
static NTSTATUS
FinishHash(
BCRYPT_HASH_HANDLE h,
PUCHAR p,
ULONG n,
ULONG f
)
{
if (cryptoFailure == 4)
{
std::memset(p, 0xff, n);
return STATUS_UNSUCCESSFUL;
}
return BCryptFinishHash(h, p, n, f);
}
#define BCryptOpenAlgorithmProvider OpenAlgorithm
#define BCryptCreateHash CreateHash
#define BCryptHashData HashData
#define BCryptFinishHash FinishHash
#ifdef DOMITO_TEST_OTHER_ARCH
#undef _M_AMD64
#undef _AMD64_
#endif
#include "consumer-under-test.inc"
#undef BCryptOpenAlgorithmProvider
#undef BCryptCreateHash
#undef BCryptHashData
#undef BCryptFinishHash
using Bytes = std::vector<UCHAR>;
static Bytes
Der(
UCHAR tag,
const Bytes& contents
)
{
Bytes b = {tag};
if (contents.size() < 128)
{
b.push_back(static_cast<UCHAR>(contents.size()));
}
else
{
Check(contents.size() < 256, "fixture DER bound");
b.push_back(0x81);
b.push_back(static_cast<UCHAR>(contents.size()));
}
b.insert(b.end(), contents.begin(), contents.end());
return b;
}
static Bytes
Join(
std::initializer_list<Bytes> values
)
{
Bytes b;
for (const auto& v : values)
{
b.insert(b.end(), v.begin(), v.end());
}
return b;
}
static Bytes
Fixture(
bool version
)
{
Bytes sig = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 11}), Der(5, {})}));
Bytes rsa = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 1}), Der(5, {})}));
Bytes name = Der(0x30, Der(0x31, Der(0x30, Join({Der(6, {0x55, 4, 3}), Der(0x0c, {'t', 'e', 's', 't'})}))));
const std::string date = "260101000000Z";
Bytes time = Der(0x17, Bytes(date.begin(), date.end()));
Bytes key = Der(0x30, Join({Der(2, {0, 0xab, 0xcd}), Der(2, {1, 0, 1})}));
key.insert(key.begin(), 0);
Bytes spki = Der(0x30, Join({rsa, Der(3, key)}));
Bytes tbs = Join(
{version ? Der(0xa0, Der(2, {2})) : Bytes{}, Der(2, {1}), sig, name, Der(0x30, Join({time, time})), name, spki}
);
return Der(0x30, Join({Der(0x30, tbs), sig, Der(3, {0, 0xab, 0xcd})}));
}
static void
TestCertificates()
{
for (bool v : {false, true})
{
auto cert = Fixture(v);
DOMITO_CERTIFICATE_SPKI spki = {};
Check(
NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast<ULONG>(cert.size()), &spki)),
"v1/v3 extraction"
);
Check(
spki.Content.Data > spki.Encoded.Data && spki.Content.Size < spki.Encoded.Size,
"content and complete TLV differ"
);
auto context = CertCreateCertificateContext(X509_ASN_ENCODING, cert.data(), static_cast<DWORD>(cert.size()));
Check(context != nullptr, "Crypt32 independently decodes fixture certificate");
BYTE* encoded = nullptr;
DWORD size = 0;
Check(
CryptEncodeObjectEx(
X509_ASN_ENCODING,
X509_PUBLIC_KEY_INFO,
&context->pCertInfo->SubjectPublicKeyInfo,
CRYPT_ENCODE_ALLOC_FLAG,
nullptr,
&encoded,
&size
) != FALSE,
"Crypt32 independent SPKI encoding"
);
Check(
size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size),
"extracted complete SPKI equals Crypt32"
);
LocalFree(encoded);
CertFreeCertificateContext(context);
UCHAR full[64];
UCHAR legacy[64];
UCHAR whole[64];
UCHAR oracle[64];
ULONG a = 64;
ULONG b = 64;
ULONG c = 64;
DWORD d = 64;
Check(
NT_SUCCESS(DomitoHashCertificate(
cert.data(), static_cast<ULONG>(cert.size()), DomitoCertificateHashSpkiDer, CALG_SHA_256, full, &a
)),
"full SPKI hash"
);
Check(
NT_SUCCESS(DomitoHashCertificate(
cert.data(),
static_cast<ULONG>(cert.size()),
DomitoCertificateHashSpkiContentLegacy,
CALG_SHA_256,
legacy,
&b
)),
"legacy content SPKI hash"
);
Check(a == 32 && b == 32 && std::memcmp(full, legacy, 32) != 0, "explicit SPKI conventions differ");
Check(
CryptHashCertificate2(
BCRYPT_SHA256_ALGORITHM, 0, nullptr, spki.Content.Data, spki.Content.Size, oracle, &d
) != FALSE &&
d == b && !std::memcmp(legacy, oracle, d),
"legacy hash matches exact content-only oracle"
);
Check(
NT_SUCCESS(DomitoHashCertificate(
cert.data(), static_cast<ULONG>(cert.size()), DomitoCertificateHashDer, CALG_SHA_256, whole, &c
)),
"DER thumbprint"
);
d = 64;
Check(
CryptHashCertificate2(
BCRYPT_SHA256_ALGORITHM, 0, nullptr, cert.data(), static_cast<DWORD>(cert.size()), oracle, &d
) != FALSE &&
d == c && !std::memcmp(whole, oracle, d),
"thumbprint matches Crypt32"
);
for (ULONG i = 0; i < cert.size(); ++i)
{
spki.Encoded = {cert.data(), 99};
Check(!NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), i, &spki)), "every truncated certificate rejected");
Check(!spki.Encoded.Data && !spki.Content.Data, "failed extraction clears borrowed views");
}
cert.push_back(0);
Check(
!NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast<ULONG>(cert.size()), &spki)),
"trailing byte rejected"
);
}
for (auto malformed :
{Bytes{0x30, 0x80, 0, 0},
Bytes{0x30, 0x81, 1, 0},
Bytes{0x30, 0x85, 1, 2, 3, 4, 5},
Bytes{0x30, 0x84, 0xff, 0xff, 0xff, 0xff}})
{
DOMITO_CERTIFICATE_SPKI spki = {};
Check(
!NT_SUCCESS(DomitoGetCertificateSpki(malformed.data(), static_cast<ULONG>(malformed.size()), &spki)),
"bad DER lengths rejected"
);
}
const UCHAR abc[] = {'a', 'b', 'c'};
UCHAR hash[64];
for (ALG_ID alg : {CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512})
{
ULONG n = 0;
Check(DomitoHashBuffer(abc, 3, alg, nullptr, &n) == STATUS_BUFFER_TOO_SMALL, "hash sizing");
Check(n == 20 || n == 32 || n == 48 || n == 64, "known output size");
ULONG cap = n - 1;
Check(DomitoHashBuffer(abc, 3, alg, hash, &cap) == STATUS_BUFFER_TOO_SMALL && cap == n, "short hash capacity");
cap = 64;
Check(NT_SUCCESS(DomitoHashBuffer(nullptr, 0, alg, hash, &cap)), "empty hash");
}
for (int fail = 1; fail <= 4; ++fail)
{
cryptoFailure = fail;
ULONG n = 64;
std::memset(hash, 0xff, 64);
Check(!NT_SUCCESS(DomitoHashBuffer(abc, 3, CALG_SHA_256, hash, &n)) && !n, "CNG failure propagated");
for (int i = 0; i < 32; ++i)
{
Check(!hash[i], "CNG failure digest erased");
}
}
cryptoFailure = 0;
for (unsigned byte = 0; byte < 256; ++byte)
{
UCHAR in = static_cast<UCHAR>(byte);
UCHAR out = 0;
WCHAR hex[3];
Check(NT_SUCCESS(DomitoEncodeHex(&in, 1, hex, 3)), "hex encoding");
UNICODE_STRING str = {4, 6, hex};
Check(NT_SUCCESS(DomitoDecodeHex(&str, &out, 1)) && out == in, "all byte values hex roundtrip");
}
for (WCHAR ch : {L'g', L'/', L':', L' ', L'\0', L'\u1234'})
{
WCHAR text[] = {L'0', ch};
UNICODE_STRING str = {4, 4, text};
UCHAR out = 9;
Check(
DomitoDecodeHex(&str, &out, 1) == STATUS_INVALID_PARAMETER && !out,
"invalid hex digit rejected and output zeroed"
);
}
WCHAR shortHex[2];
Check(DomitoEncodeHex(abc, 3, shortHex, 2) == STATUS_BUFFER_TOO_SMALL && !shortHex[0], "short hex buffer cleared");
// Put every truncated prefix immediately before an inaccessible page.
SYSTEM_INFO system = {};
GetSystemInfo(&system);
const SIZE_T page = system.dwPageSize;
auto guarded = static_cast<PUCHAR>(VirtualAlloc(nullptr, 2 * page, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
Check(guarded != nullptr, "guard-page allocation");
DWORD old = 0;
Check(VirtualProtect(guarded + page, page, PAGE_NOACCESS, &old) != FALSE, "guard page installed");
const auto fixture = Fixture(true);
for (ULONG n = 0; n < fixture.size(); ++n)
{
UCHAR* input = guarded + page - n;
std::memcpy(input, fixture.data(), n);
DOMITO_CERTIFICATE_SPKI spki = {};
Check(!NT_SUCCESS(DomitoGetCertificateSpki(input, n, &spki)), "truncated DER at guard page rejected");
}
VirtualFree(guarded, 0, MEM_RELEASE);
// Read-only optional corpus: exercises real long-length RSA/ECC certificates.
HCERTSTORE store = CertOpenSystemStoreW(0, L"ROOT");
if (store)
{
PCCERT_CONTEXT context = nullptr;
unsigned count = 0;
while (count < 16 && (context = CertEnumCertificatesInStore(store, context)) != nullptr)
{
++count;
DOMITO_CERTIFICATE_SPKI spki = {};
Check(
NT_SUCCESS(DomitoGetCertificateSpki(context->pbCertEncoded, context->cbCertEncoded, &spki)),
"real certificate SPKI extraction"
);
BYTE* encoded = nullptr;
DWORD size = 0;
Check(
CryptEncodeObjectEx(
X509_ASN_ENCODING,
X509_PUBLIC_KEY_INFO,
&context->pCertInfo->SubjectPublicKeyInfo,
CRYPT_ENCODE_ALLOC_FLAG,
nullptr,
&encoded,
&size
) != FALSE,
"real Crypt32 SPKI encoding"
);
Check(
size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size),
"real certificate SPKI equals Crypt32"
);
LocalFree(encoded);
}
if (context)
{
CertFreeCertificateContext(context);
}
CertCloseStore(store, 0);
}
}
static void
TestOwnership()
{
DOMITO_CI_VALIDATION_RESULT result = {};
UNICODE_STRING path = {8, 10, const_cast<PWCHAR>(L"\\foo")};
for (int fail : {0, 2, 3})
{
failure = fail;
const auto r = refs, d = derefs, c = closed;
result.PolicyInfo.ChainInfo = static_cast<MINCRYPT_CHAIN_INFO*>(Allocate(1));
NTSTATUS s = DomitoValidateFilePathEx(&path, 0x20, 7, DomitoCiSignatureSelectionNative, &result);
Check(NT_SUCCESS(s) == (fail == 0), "path/reference failure returned");
Check(
refs - r == derefs - d && closed - c == (fail == 3 ? 0u : 1u),
"all references and owned path handles released"
);
DomitoFreeCiValidationResult(&result);
Check(!live, "path no allocations leaked");
}
failure = 0;
validateStatus = STATUS_INVALID_IMAGE_HASH;
const auto c = closed;
Check(
DomitoValidateFileHandleEx(&fileToken, 0x20, 7, DomitoCiSignatureSelectionNative, &result) == validateStatus,
"CI failure retained"
);
Check(closed == c && result.PolicyInfo.ChainInfo, "borrowed handle open and diagnostics retained");
DomitoFreeCiValidationResult(&result);
validateStatus = STATUS_SUCCESS;
for (int fail : {0, 1, 4, 5})
{
failure = fail;
PUNICODE_STRING name = reinterpret_cast<PUNICODE_STRING>(1);
NTSTATUS s = DomitoGetProcessImageNameEx(&processToken, &name);
Check(NT_SUCCESS(s) == (fail == 0), "PEPROCESS lookup failure");
if (NT_SUCCESS(s))
{
Check(
name->Buffer == reinterpret_cast<PWCHAR>(name + 1) && name->Length == 8,
"one contiguous process-name allocation"
);
}
else
{
Check(!name, "failed process name null");
}
DomitoFreeProcessImageName(name);
Check(!live, "process-name allocations released");
}
for (int fail : {0, 1, 6, 7, 10, 11})
{
failure = fail;
PUNICODE_STRING name = reinterpret_cast<PUNICODE_STRING>(1);
NTSTATUS s = DomitoGetProcessImageName(123, &name);
Check(NT_SUCCESS(s) == (fail == 0), "PID query failure");
if (!NT_SUCCESS(s))
{
Check(!name, "PID failure never leaves freed pointer");
}
DomitoFreeProcessImageName(name);
Check(!live, "PID query cleanup");
}
failure = 0;
MINCERT_BLOB blob = {1, reinterpret_cast<PUCHAR>(&fileToken)};
for (int fail : {0, 1, 8, 9})
{
failure = fail;
UNICODE_STRING name = {};
NTSTATUS s = DomitoGetCertificatePublisherName(&blob, &name);
Check(NT_SUCCESS(s) == (fail == 0), "publisher error propagation");
DomitoFreeCertificatePublisherName(&name);
DomitoFreeCertificatePublisherName(&name);
Check(!live && !name.Buffer, "publisher matching free including partial failure");
}
failure = 0;
ANSI_STRING module = RTL_CONSTANT_STRING("module");
ANSI_STRING name = RTL_CONSTANT_STRING("export");
PVOID address = reinterpret_cast<PVOID>(1);
moduleStatus = STATUS_NOT_FOUND;
Check(
DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_FOUND && !address,
"missing module nulls export"
);
moduleStatus = STATUS_SUCCESS;
exportStatus = STATUS_NOT_SUPPORTED;
address = reinterpret_cast<PVOID>(1);
Check(
DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_SUPPORTED && !address,
"unsupported forwarder status preserved"
);
exportStatus = STATUS_SUCCESS;
}
static void
TestChains()
{
alignas(8) UCHAR storage[512] = {};
auto chain = reinterpret_cast<MINCRYPT_CHAIN_INFO*>(storage);
chain->Size = sizeof(storage);
chain->ChainElements = reinterpret_cast<MINCRYPT_CHAIN_ELEMENT*>(storage + 56);
chain->NumberOfChainElement = 2;
for (ULONG i = 0; i < 2; ++i)
{
chain->ChainElements[i].Certificate = {1, storage + 400 + i};
chain->ChainElements[i].HashSize = 32;
}
MINCRYPT_POLICY_INFO policy = {};
policy.Size = sizeof(policy);
policy.ChainInfo = chain;
DOMITO_CI_CHAIN_VIEW view = {};
#ifdef DOMITO_TEST_OTHER_ARCH
Check(
DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view) == STATUS_NOT_SUPPORTED && !view.Count,
"non-x64 rejects unverified layout"
);
#else
Check(
NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && view.Count == 2,
"minimal historical 56-byte chain prefix"
);
// The last certificate is an arbitrary byte, not a proven self-signed root.
chain->ChainElements[1].Certificate.Data = storage + 512;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && !view.Count,
"child span past allocation rejected"
);
chain->ChainElements[1].Certificate.Data = storage + 401;
chain->ChainElements[0].Subject = {reinterpret_cast<PCHAR>(storage + 500), 13};
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"malformed name span rejected"
);
chain->ChainElements[0].Subject = {};
chain->NumberOfChainElement = 0xffffffff;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"element multiplication overflow rejected"
);
chain->NumberOfChainElement = 2;
chain->ChainElements[0].HashSize = 65;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"oversize CI element hash rejected"
);
chain->ChainElements[0].HashSize = 32;
for (ULONG n = 0; n < 56; ++n)
{
chain->Size = n;
Check(
!NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)),
"short historical header rejected"
);
}
#endif
}
int
main()
{
TestCertificates();
TestOwnership();
TestChains();
Check(!live, "all allocations released");
std::printf("PASS: %u consumer-helper checks, no live allocations\n", checks);
}
File diff suppressed because it is too large. Load diff
+55
View File
@@ -0,0 +1,55 @@
// Compile with the WDK for supported 64-bit targets. No driver is linked/loaded.
#include <ntifs.h>
#include "Domito.MinCrypt.h"
#if NTDDI_VERSION >= NTDDI_WIN10
C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 48);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_POLICY_INFO,
VerificationStatus
) == 4
);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_POLICY_INFO,
ChainInfo
) == 16
);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_POLICY_INFO,
NotBefore
) == 32
);
C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 72);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_CHAIN_INFO,
ChainElements
) == 40
);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_CHAIN_INFO,
AuthenticodeAttributes
) == 56
);
#else
C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 32);
C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 40);
#endif
C_ASSERT(sizeof(MINCRYPT_NAME_BLOB) == 16);
C_ASSERT(sizeof(MINCRYPT_CHAIN_ELEMENT) == 120);
C_ASSERT(
FIELD_OFFSET(
MINCRYPT_CHAIN_ELEMENT,
Certificate
) == 104
);
#if defined(_M_AMD64)
// These opt-in declarations establish observations only for the named x64 build.
#include "Domito.MinCrypt.26100.h"
#include "Domito.MinCrypt.Historical.h"
#endif
+34
View File
@@ -0,0 +1,34 @@
"""Test the actual kernel probe body with mock CI; run in an x64 VS dev shell."""
from pathlib import Path
import re
import subprocess
import tempfile
tests = Path(__file__).resolve().parent
header = (tests.parent / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
names = ["MINCRYPT_MAX_HASH_LENGTH", "DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE"]
constants = "\n".join(
re.search(r"^#define\s+" + name + r"\s+[^\r\n]+", header, re.M)[0] for name in names
)
with tempfile.TemporaryDirectory(prefix="domito-ci-probe-") as temp:
build = Path(temp)
(build / "ci-probe-constants.inc").write_text(constants + "\n", encoding="utf-8")
exe = build / "ci-probe-tests.exe"
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
str(tests / "ci-file-object-probe-tests.cpp"),
"/Fe" + str(exe),
"/Fo" + str(build / "ci-probe-tests.obj"),
],
cwd=build,
check=True,
)
subprocess.run([str(exe)], cwd=build, check=True)
+53
View File
@@ -0,0 +1,53 @@
"""Compile the actual production CI wrapper body against deterministic CI doubles."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
header = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
start = header.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards")
end = header.index("#define DOMITO_CI_SOURCE_EMBEDDED", start)
constants_header = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
constants = "\n".join(
line
for line in constants_header.splitlines()
if line.startswith(
(
"#define DOMITO_CI_VALIDATE_FLAG_",
"#define MINCRYPT_MAX_HASH_LENGTH",
"#define MINCRYPT_SHA",
)
)
)
source = (root / "src/Domito.CodeIntegrity.Validation.cpp").read_text(encoding="utf-8-sig")
source = source.replace('#include "Domito.Internal.h"', "")
with tempfile.TemporaryDirectory(prefix="domito-ci-validation-") as temporary:
directory = Path(temporary)
(directory / "ci-validation-api.inc").write_text(
constants + "\n" + header[start:end], encoding="utf-8"
)
(directory / "ci-validation-under-test.inc").write_text(source, encoding="utf-8")
for branch in ["x64", "other"]:
program = directory / (branch + ".exe")
flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else []
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/DNTDDI_VERSION=0x0A000008",
"/DNTDDI_WIN10=0x0A000000",
"/I" + str(directory),
*flags,
str(Path(__file__).with_name("ci-validation-tests.cpp")),
"/Fe" + str(program),
"/Fo" + str(directory / (branch + ".obj")),
],
cwd=directory,
check=True,
)
subprocess.run([str(program)], check=True)
+35
View File
@@ -0,0 +1,35 @@
"""Run production export and file-read helpers with deterministic user-mode doubles."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
source = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
export_start = source.index("// This helper handles directory RVAs")
export_end = source.index("_Success_", source.index("DomitoFindExportedFunctionAddress(") + 1)
read_start = source.rfind("_Success_", 0, source.index("DomitoReadFileAt("))
read_end = source.index("_Success_", source.index("DomitoReadFile(") + 1)
with tempfile.TemporaryDirectory(prefix="domito-common-tests-") as temp:
build = Path(temp)
(build / "common-under-test.inc").write_text(
source[export_start:export_end] + "\n" + source[read_start:read_end], encoding="utf-8"
)
exe = build / "common-tests.exe"
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
str(root / "tests/common-tests.cpp"),
"/Fe" + str(exe),
"/Fo" + str(build / "common-tests.obj"),
],
cwd=build,
check=True,
)
subprocess.run([str(exe)], cwd=build, check=True)
+71
View File
@@ -0,0 +1,71 @@
"""Production consumer helpers with real CNG/Crypt32 and deterministic kernel doubles."""
from pathlib import Path
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
public = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
mincrypt = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig")
structures = mincrypt[
mincrypt.index("#include <pshpack8.h>") : mincrypt.index("#include <poppack.h>")
+ len("#include <poppack.h>")
]
validation = public[
public.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards") : public.index(
"#define DOMITO_CI_SOURCE_EMBEDDED"
)
]
helpers = (
(root / "include/Domito.Consumer.h")
.read_text(encoding="utf-8-sig")
.replace('#include "Domito.h"', "")
)
algorithm = public[
public.index("#if !defined(CALG_SHA1)") : public.index(
"/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM(")
)
]
common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
process_start = common.rfind("_Success_", 0, common.index("DomitoGetProcessImageName("))
process = (
common[process_start:]
.replace('#pragma code_seg("PAGED")', "")
.replace("#pragma code_seg()", "")
)
with tempfile.TemporaryDirectory(prefix="domito-consumer-tests-") as temp:
build = Path(temp)
(build / "consumer-api.inc").write_text(
algorithm + "\n" + structures + "\n" + validation + "\n" + helpers
)
bodies = "\n".join(
(root / ("src/" + name))
.read_text(encoding="utf-8-sig")
.replace('#include "Domito.Internal.h"', "")
for name in ["Domito.Certificate.cpp", "Domito.Consumer.cpp"]
)
(build / "consumer-under-test.inc").write_text(bodies + "\n" + process)
for branch in ["x64", "other"]:
exe = build / (branch + ".exe")
flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else []
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
*flags,
str(root / "tests/consumer-helper-tests.cpp"),
"/Fe" + str(exe),
"/Fo" + str(build / (branch + ".obj")),
"/link",
"bcrypt.lib",
"crypt32.lib",
],
cwd=build,
check=True,
)
subprocess.run([str(exe)], check=True)
+94
View File
@@ -0,0 +1,94 @@
"""Compile actual public headers and verify MSVC C4996 migration diagnostics."""
import argparse
import os
from pathlib import Path
import subprocess
import tempfile
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--arch", choices=["x64", "ARM64"], default="x64")
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
vc = Path(os.environ["VCToolsInstallDir"])
kit = Path(os.environ["WindowsSdkDir"]) / "Include" / os.environ["WindowsSDKVersion"].strip("\\/")
compiler = vc / "bin/Hostx64" / args.arch.lower() / "cl.exe"
architecture = ["/D_AMD64_", "/D_AMD64", "/Gz"] if args.arch == "x64" else ["/D_ARM64_", "/DARM64"]
common = [
"/nologo",
"/c",
"/kernel",
"/W4",
"/WX",
"/we4996",
"/D_WIN64",
"/DWINNT=1",
"/DNTDDI_VERSION=0x0A000008",
"/D_WIN32_WINNT=0x0A00",
*architecture,
"/I" + str(vc / "include"),
"/I" + str(kit / "km"),
"/I" + str(kit / "shared"),
"/I" + str(kit / "ucrt"),
"/I" + str(root / "include"),
]
old = {
"DomitoGetPortableExecutableDigestKind": "DomitoGetPortableExecutableDigestKindEx",
"DomitoCalculatePortableExecutableDigest": "DomitoCalculatePortableExecutableDigestEx",
"DomitoValidateFileLegacyMode": "DomitoValidateFileLegacyModeEx",
"DomitoGetProcessImageName": "DomitoGetProcessImageNameEx",
}
modern = [
*old.values(),
"DomitoFreePortableExecutableDigest",
"DomitoFreeProcessImageName",
"DomitoValidateFileObjectEx",
"DomitoValidateFileHandleEx",
"DomitoValidateFilePathEx",
"DomitoGetCiChainView",
"DomitoGetCertificatePublisherName",
"DomitoFindModuleBaseAddress",
"DomitoFindExportedFunctionAddress",
"DomitoReadFile",
"DomitoReadFileAt",
"CiValidateFileObject",
]
prefix = "#include <ntifs.h>\n#include <ntintsafe.h>\n#include <ntimage.h>\n#include <bcrypt.h>\n#include <Domito.h>\n"
checks = 0
with tempfile.TemporaryDirectory(prefix="domito-deprecation-") as temp:
build = Path(temp)
for language in ["c", "cpp"]:
for index, (name, replacement) in enumerate([("modern", None), *old.items()]):
source = build / (f"check-{index}." + language)
refs = modern if replacement is None else [name]
source.write_text(
prefix
+ "void Check(void) {\n"
+ "".join(f" (void)&{ref};\n" for ref in refs)
+ "}\n"
)
result = subprocess.run(
[
str(compiler),
*common,
"/TC" if language == "c" else "/TP",
"/Fo" + str(build / "check.obj"),
str(source),
],
text=True,
capture_output=True,
)
output = result.stdout + result.stderr
if replacement is None:
assert result.returncode == 0, output
else:
assert (
result.returncode != 0
and "C4996" in output
and name in output
and replacement in output
), output
errors = [line for line in output.splitlines() if "error C" in line]
assert errors and all("C4996" in line for line in errors), output
checks += 1
print(f"PASS: {checks} {args.arch} C/C++ public-header deprecation checks")
+57
View File
@@ -0,0 +1,57 @@
"""Run in an x64 VS developer shell with Windows SDK and Python 3 installed.
Extracts the actual production functions without changing their bodies, replacing
only kernel dependencies with the test harness. No driver is loaded. This tests
parsing, hashing and cleanup, not real CI policy or kernel I/O synchronization.
"""
import argparse
from pathlib import Path
import subprocess
import tempfile
parser = argparse.ArgumentParser()
parser.add_argument("--source-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--digest-source", type=Path)
args = parser.parse_args()
root = args.source_root
digest = (args.digest_source or root / "src/Domito.CodeIntegrity.cpp").read_text(
encoding="utf-8-sig"
)
digest = digest.replace('#include "Domito.Internal.h"', "").replace(
'#include "Domito.MinCrypt.h"', ""
)
common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig")
start = common.rfind("_Success_", 0, common.index("DomitoMemorySearchPattern("))
end = common.index("_Success_", common.index("DomitoMemorySearchPattern("))
search = common[start:end]
public = (root / "include/Domito.h").read_text(encoding="utf-8-sig")
start = public.index("#if !defined(CALG_SHA1)")
end = public.index("/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM("))
algorithm = public[start:end]
with tempfile.TemporaryDirectory(prefix="domito-digest-tests-") as temp:
build = Path(temp)
(build / "digest-under-test.inc").write_text(
algorithm + "\n" + search + "\n" + digest, encoding="utf-8"
)
test_source = Path(__file__).with_name("digest-tests.cpp")
subprocess.run(
[
"cl",
"/nologo",
"/EHsc",
"/W4",
"/WX",
"/std:c++17",
"/I" + str(build),
str(test_source),
"/Fe" + str(build / "digest-tests.exe"),
"/Fo" + str(build / "digest-tests.obj"),
"/link",
"bcrypt.lib",
"crypt32.lib",
],
cwd=build,
check=True,
)
subprocess.run([str(build / "digest-tests.exe")], cwd=build, check=True)