diff --git a/Domito.props b/Domito.props index 40705ee..251f70f 100644 --- a/Domito.props +++ b/Domito.props @@ -4,14 +4,15 @@ <_PropertySheetDisplayName>Domito Kernel Library - $(SolutionDir)..\Domito + $(MSBuildThisFileDirectory) + $(Platform) $(DomitoRootPath)\include;$(IntDir);%(AdditionalIncludeDirectories) - $(DomitoRootPath)\lib\$(DDKPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DDKPlatform)\ci.lib;$(DDK_LIB_PATH)cng.lib;%(AdditionalDependencies); + $(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\ci.lib;cng.lib;%(AdditionalDependencies); diff --git a/NOTES.md b/NOTES.md index cc4d759..c4461c5 100644 --- a/NOTES.md +++ b/NOTES.md @@ -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 -- CiFindPageHashesInCatalog -- CiFindPageHashesInSignedFile -- CiFreePolicyInfo -- CiGetPEInformation -- CiInitialize -- CiValidateFileObject -- CiVerifyHashInCatalog +These are export-table observations from six extracted x64 `ci.dll` files, not +claims about every servicing revision or a proof of the original introduction +build. The companion CodeIntegrity-RE `corpus-index.json` records identities, +extraction aliases, manifest checks, and metadata. VM directory names are labels; +versions below come from the PE version resources. No target was executed. -## 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--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. diff --git a/README.md b/README.md index 7123638..bf5e369 100644 --- a/README.md +++ b/README.md @@ -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 😉 - 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 🤞 - - **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 - 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. - 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 - 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 -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 @@ -71,6 +91,14 @@ Copy the provided `Domito.props` file into the directory of your `.vcxproj` 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 This library benefits from these awesome projects ❤ (appearance in no special order): diff --git a/docs/authenticode-digests.md b/docs/authenticode-digests.md new file mode 100644 index 0000000..1525f32 --- /dev/null +++ b/docs/authenticode-digests.md @@ -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. diff --git a/docs/ci-api-compatibility.json b/docs/ci-api-compatibility.json new file mode 100644 index 0000000..86c39e7 --- /dev/null +++ b/docs/ci-api-compatibility.json @@ -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 + } + } + ] +} diff --git a/docs/ci-api-compatibility.md b/docs/ci-api-compatibility.md new file mode 100644 index 0000000..ad5ae16 --- /dev/null +++ b/docs/ci-api-compatibility.md @@ -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 `. +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. diff --git a/docs/ci-validate-file-object.md b/docs/ci-validate-file-object.md new file mode 100644 index 0000000..dedfd6e --- /dev/null +++ b/docs/ci-validate-file-object.md @@ -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. diff --git a/docs/ci-validation-helpers.md b/docs/ci-validation-helpers.md new file mode 100644 index 0000000..9727193 --- /dev/null +++ b/docs/ci-validation-helpers.md @@ -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. diff --git a/docs/consumer-helpers.md b/docs/consumer-helpers.md new file mode 100644 index 0000000..4fa4333 --- /dev/null +++ b/docs/consumer-helpers.md @@ -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. diff --git a/docs/implementation-status.md b/docs/implementation-status.md new file mode 100644 index 0000000..30d865f --- /dev/null +++ b/docs/implementation-status.md @@ -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. diff --git a/docs/mincrypt-contracts.md b/docs/mincrypt-contracts.md new file mode 100644 index 0000000..e9da9a9 --- /dev/null +++ b/docs/mincrypt-contracts.md @@ -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. diff --git a/include/Domito.Consumer.h b/include/Domito.Consumer.h new file mode 100644 index 0000000..f2d9b79 --- /dev/null +++ b/include/Domito.Consumer.h @@ -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 diff --git a/include/Domito.MinCrypt.26100.h b/include/Domito.MinCrypt.26100.h new file mode 100644 index 0000000..ec5c7cf --- /dev/null +++ b/include/Domito.MinCrypt.26100.h @@ -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 diff --git a/include/Domito.MinCrypt.Historical.h b/include/Domito.MinCrypt.Historical.h new file mode 100644 index 0000000..3939172 --- /dev/null +++ b/include/Domito.MinCrypt.Historical.h @@ -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 +); diff --git a/include/Domito.MinCrypt.h b/include/Domito.MinCrypt.h index 63d1596..48a5dbb 100644 --- a/include/Domito.MinCrypt.h +++ b/include/Domito.MinCrypt.h @@ -22,16 +22,16 @@ #pragma once - // Warnings which disabled for compiling +// Warnings which disabled for compiling #if _MSC_VER >= 1200 #pragma warning(push) // nonstandard extension used : nameless struct/union -#pragma warning(disable:4201) +#pragma warning(disable : 4201) // 'struct_name' : structure was padded due to __declspec(align()) -#pragma warning(disable:4324) +#pragma warning(disable : 4324) // 'enumeration': a forward declaration of an unscoped enumeration must have an // underlying type (int assumed) -#pragma warning(disable:4471) +#pragma warning(disable : 4471) #endif EXTERN_C_START @@ -47,144 +47,144 @@ EXTERN_C_START // // ALG_ID crackers -#define GET_ALG_CLASS(x) (x & (7 << 13)) -#define GET_ALG_TYPE(x) (x & (15 << 9)) -#define GET_ALG_SID(x) (x & (511)) +#define GET_ALG_CLASS(x) (x & (7 << 13)) +#define GET_ALG_TYPE(x) (x & (15 << 9)) +#define GET_ALG_SID(x) (x & (511)) // Algorithm classes // certenrolld_begin -- ALG_CLASS_* -#define ALG_CLASS_ANY (0) -#define ALG_CLASS_SIGNATURE (1 << 13) -#define ALG_CLASS_MSG_ENCRYPT (2 << 13) -#define ALG_CLASS_DATA_ENCRYPT (3 << 13) -#define ALG_CLASS_HASH (4 << 13) -#define ALG_CLASS_KEY_EXCHANGE (5 << 13) -#define ALG_CLASS_ALL (7 << 13) +#define ALG_CLASS_ANY (0) +#define ALG_CLASS_SIGNATURE (1 << 13) +#define ALG_CLASS_MSG_ENCRYPT (2 << 13) +#define ALG_CLASS_DATA_ENCRYPT (3 << 13) +#define ALG_CLASS_HASH (4 << 13) +#define ALG_CLASS_KEY_EXCHANGE (5 << 13) +#define ALG_CLASS_ALL (7 << 13) // certenrolld_end // Algorithm types -#define ALG_TYPE_ANY (0) -#define ALG_TYPE_DSS (1 << 9) -#define ALG_TYPE_RSA (2 << 9) -#define ALG_TYPE_BLOCK (3 << 9) -#define ALG_TYPE_STREAM (4 << 9) -#define ALG_TYPE_DH (5 << 9) -#define ALG_TYPE_SECURECHANNEL (6 << 9) +#define ALG_TYPE_ANY (0) +#define ALG_TYPE_DSS (1 << 9) +#define ALG_TYPE_RSA (2 << 9) +#define ALG_TYPE_BLOCK (3 << 9) +#define ALG_TYPE_STREAM (4 << 9) +#define ALG_TYPE_DH (5 << 9) +#define ALG_TYPE_SECURECHANNEL (6 << 9) #if (NTDDI_VERSION >= NTDDI_VISTA) -#define ALG_TYPE_ECDH (7 << 9) +#define ALG_TYPE_ECDH (7 << 9) #endif //(NTDDI_VERSION >= NTDDI_VISTA) #if (NTDDI_VERSION >= NTDDI_WIN10_RS1) -#define ALG_TYPE_THIRDPARTY (8 << 9) +#define ALG_TYPE_THIRDPARTY (8 << 9) #endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1) // Generic sub-ids -#define ALG_SID_ANY (0) +#define ALG_SID_ANY (0) // Generic ThirdParty sub-ids #if (NTDDI_VERSION >= NTDDI_WIN10_RS1) -#define ALG_SID_THIRDPARTY_ANY (0) +#define ALG_SID_THIRDPARTY_ANY (0) #endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1) // Some RSA sub-ids -#define ALG_SID_RSA_ANY 0 -#define ALG_SID_RSA_PKCS 1 -#define ALG_SID_RSA_MSATWORK 2 -#define ALG_SID_RSA_ENTRUST 3 -#define ALG_SID_RSA_PGP 4 +#define ALG_SID_RSA_ANY 0 +#define ALG_SID_RSA_PKCS 1 +#define ALG_SID_RSA_MSATWORK 2 +#define ALG_SID_RSA_ENTRUST 3 +#define ALG_SID_RSA_PGP 4 // Some DSS sub-ids // -#define ALG_SID_DSS_ANY 0 -#define ALG_SID_DSS_PKCS 1 -#define ALG_SID_DSS_DMS 2 +#define ALG_SID_DSS_ANY 0 +#define ALG_SID_DSS_PKCS 1 +#define ALG_SID_DSS_DMS 2 #if (NTDDI_VERSION >= NTDDI_VISTA) -#define ALG_SID_ECDSA 3 +#define ALG_SID_ECDSA 3 #endif //(NTDDI_VERSION >= NTDDI_VISTA) // Block cipher sub ids // DES sub_ids -#define ALG_SID_DES 1 -#define ALG_SID_3DES 3 -#define ALG_SID_DESX 4 -#define ALG_SID_IDEA 5 -#define ALG_SID_CAST 6 -#define ALG_SID_SAFERSK64 7 -#define ALG_SID_SAFERSK128 8 -#define ALG_SID_3DES_112 9 -#define ALG_SID_CYLINK_MEK 12 -#define ALG_SID_RC5 13 +#define ALG_SID_DES 1 +#define ALG_SID_3DES 3 +#define ALG_SID_DESX 4 +#define ALG_SID_IDEA 5 +#define ALG_SID_CAST 6 +#define ALG_SID_SAFERSK64 7 +#define ALG_SID_SAFERSK128 8 +#define ALG_SID_3DES_112 9 +#define ALG_SID_CYLINK_MEK 12 +#define ALG_SID_RC5 13 #if (NTDDI_VERSION >= NTDDI_WINXP) -#define ALG_SID_AES_128 14 -#define ALG_SID_AES_192 15 -#define ALG_SID_AES_256 16 -#define ALG_SID_AES 17 +#define ALG_SID_AES_128 14 +#define ALG_SID_AES_192 15 +#define ALG_SID_AES_256 16 +#define ALG_SID_AES 17 #endif //(NTDDI_VERSION >= NTDDI_WINXP) // Fortezza sub-ids -#define ALG_SID_SKIPJACK 10 -#define ALG_SID_TEK 11 +#define ALG_SID_SKIPJACK 10 +#define ALG_SID_TEK 11 // KP_MODE -#define CRYPT_MODE_CBCI 6 // ANSI CBC Interleaved -#define CRYPT_MODE_CFBP 7 // ANSI CFB Pipelined -#define CRYPT_MODE_OFBP 8 // ANSI OFB Pipelined -#define CRYPT_MODE_CBCOFM 9 // ANSI CBC + OF Masking -#define CRYPT_MODE_CBCOFMI 10 // ANSI CBC + OFM Interleaved +#define CRYPT_MODE_CBCI 6 // ANSI CBC Interleaved +#define CRYPT_MODE_CFBP 7 // ANSI CFB Pipelined +#define CRYPT_MODE_OFBP 8 // ANSI OFB Pipelined +#define CRYPT_MODE_CBCOFM 9 // ANSI CBC + OF Masking +#define CRYPT_MODE_CBCOFMI 10 // ANSI CBC + OFM Interleaved // RC2 sub-ids -#define ALG_SID_RC2 2 +#define ALG_SID_RC2 2 // Stream cipher sub-ids -#define ALG_SID_RC4 1 -#define ALG_SID_SEAL 2 +#define ALG_SID_RC4 1 +#define ALG_SID_SEAL 2 // Diffie-Hellman sub-ids -#define ALG_SID_DH_SANDF 1 -#define ALG_SID_DH_EPHEM 2 -#define ALG_SID_AGREED_KEY_ANY 3 -#define ALG_SID_KEA 4 +#define ALG_SID_DH_SANDF 1 +#define ALG_SID_DH_EPHEM 2 +#define ALG_SID_AGREED_KEY_ANY 3 +#define ALG_SID_KEA 4 #if (NTDDI_VERSION >= NTDDI_VISTA) -#define ALG_SID_ECDH 5 -#define ALG_SID_ECDH_EPHEM 6 +#define ALG_SID_ECDH 5 +#define ALG_SID_ECDH_EPHEM 6 #endif //(NTDDI_VERSION >= NTDDI_VISTA) // Hash sub ids -#define ALG_SID_MD2 1 -#define ALG_SID_MD4 2 -#define ALG_SID_MD5 3 -#define ALG_SID_SHA 4 -#define ALG_SID_SHA1 4 -#define ALG_SID_MAC 5 -#define ALG_SID_RIPEMD 6 -#define ALG_SID_RIPEMD160 7 -#define ALG_SID_SSL3SHAMD5 8 -#define ALG_SID_HMAC 9 -#define ALG_SID_TLS1PRF 10 +#define ALG_SID_MD2 1 +#define ALG_SID_MD4 2 +#define ALG_SID_MD5 3 +#define ALG_SID_SHA 4 +#define ALG_SID_SHA1 4 +#define ALG_SID_MAC 5 +#define ALG_SID_RIPEMD 6 +#define ALG_SID_RIPEMD160 7 +#define ALG_SID_SSL3SHAMD5 8 +#define ALG_SID_HMAC 9 +#define ALG_SID_TLS1PRF 10 #if (NTDDI_VERSION >= NTDDI_WINXP) -#define ALG_SID_HASH_REPLACE_OWF 11 +#define ALG_SID_HASH_REPLACE_OWF 11 #endif //(NTDDI_VERSION >= NTDDI_WINXP) #if (NTDDI_VERSION > NTDDI_WINXPSP2) -#define ALG_SID_SHA_256 12 -#define ALG_SID_SHA_384 13 -#define ALG_SID_SHA_512 14 +#define ALG_SID_SHA_256 12 +#define ALG_SID_SHA_384 13 +#define ALG_SID_SHA_512 14 #endif //(NTDDI_VERSION > NTDDI_WINXPSP2) // secure channel sub ids -#define ALG_SID_SSL3_MASTER 1 -#define ALG_SID_SCHANNEL_MASTER_HASH 2 -#define ALG_SID_SCHANNEL_MAC_KEY 3 -#define ALG_SID_PCT1_MASTER 4 -#define ALG_SID_SSL2_MASTER 5 -#define ALG_SID_TLS1_MASTER 6 -#define ALG_SID_SCHANNEL_ENC_KEY 7 +#define ALG_SID_SSL3_MASTER 1 +#define ALG_SID_SCHANNEL_MASTER_HASH 2 +#define ALG_SID_SCHANNEL_MAC_KEY 3 +#define ALG_SID_PCT1_MASTER 4 +#define ALG_SID_SSL2_MASTER 5 +#define ALG_SID_TLS1_MASTER 6 +#define ALG_SID_SCHANNEL_ENC_KEY 7 #if (NTDDI_VERSION >= NTDDI_VISTA) // misc ECC sub ids -#define ALG_SID_ECMQV 1 +#define ALG_SID_ECMQV 1 #endif //(NTDDI_VERSION >= NTDDI_VISTA) // Our silly example sub-id -#define ALG_SID_EXAMPLE 80 +#define ALG_SID_EXAMPLE 80 // certenrolls_begin -- PROV_ENUMALGS_EX #ifndef ALGIDDEF @@ -194,132 +194,139 @@ typedef unsigned int ALG_ID; // certenrolls_end // algorithm identifier definitions -#define CALG_MD2 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD2) -#define CALG_MD4 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD4) -#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5) -#define CALG_SHA (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA) -#define CALG_SHA1 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA1) -#define CALG_MAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MAC) // Deprecated. Don't use. -#define CALG_RSA_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_RSA | ALG_SID_RSA_ANY) -#define CALG_DSS_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_DSS_ANY) +#define CALG_MD2 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD2) +#define CALG_MD4 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD4) +#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5) +#define CALG_SHA (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA) +#define CALG_SHA1 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA1) +#define CALG_MAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MAC) // Deprecated. Don't use. +#define CALG_RSA_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_RSA | ALG_SID_RSA_ANY) +#define CALG_DSS_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_DSS_ANY) #if (NTDDI_VERSION >= NTDDI_WINXP) -#define CALG_NO_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_ANY | ALG_SID_ANY) +#define CALG_NO_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_ANY | ALG_SID_ANY) #endif //(NTDDI_VERSION >= NTDDI_WINXP) -#define CALG_RSA_KEYX (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_RSA|ALG_SID_RSA_ANY) -#define CALG_DES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_DES) -#define CALG_3DES_112 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_3DES_112) -#define CALG_3DES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_3DES) -#define CALG_DESX (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_DESX) -#define CALG_RC2 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_RC2) -#define CALG_RC4 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_STREAM|ALG_SID_RC4) -#define CALG_SEAL (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_STREAM|ALG_SID_SEAL) -#define CALG_DH_SF (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_DH_SANDF) -#define CALG_DH_EPHEM (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_DH_EPHEM) -#define CALG_AGREEDKEY_ANY (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_AGREED_KEY_ANY) -#define CALG_KEA_KEYX (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_KEA) -#define CALG_HUGHES_MD5 (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_ANY|ALG_SID_MD5) -#define CALG_SKIPJACK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_SKIPJACK) -#define CALG_TEK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_TEK) -#define CALG_CYLINK_MEK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_CYLINK_MEK) // Deprecated. Do not use -#define CALG_SSL3_SHAMD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SSL3SHAMD5) -#define CALG_SSL3_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SSL3_MASTER) -#define CALG_SCHANNEL_MASTER_HASH (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_MASTER_HASH) -#define CALG_SCHANNEL_MAC_KEY (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_MAC_KEY) -#define CALG_SCHANNEL_ENC_KEY (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_ENC_KEY) -#define CALG_PCT1_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_PCT1_MASTER) -#define CALG_SSL2_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SSL2_MASTER) -#define CALG_TLS1_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_TLS1_MASTER) -#define CALG_RC5 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_RC5) -#define CALG_HMAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HMAC) -#define CALG_TLS1PRF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_TLS1PRF) +#define CALG_RSA_KEYX (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_RSA | ALG_SID_RSA_ANY) +#define CALG_DES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_DES) +#define CALG_3DES_112 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_3DES_112) +#define CALG_3DES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_3DES) +#define CALG_DESX (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_DESX) +#define CALG_RC2 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_RC2) +#define CALG_RC4 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_STREAM | ALG_SID_RC4) +#define CALG_SEAL (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_STREAM | ALG_SID_SEAL) +#define CALG_DH_SF (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_DH_SANDF) +#define CALG_DH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_DH_EPHEM) +#define CALG_AGREEDKEY_ANY (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_AGREED_KEY_ANY) +#define CALG_KEA_KEYX (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_KEA) +#define CALG_HUGHES_MD5 (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_MD5) +#define CALG_SKIPJACK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_SKIPJACK) +#define CALG_TEK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_TEK) +#define CALG_CYLINK_MEK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_CYLINK_MEK) // Deprecated. Do not use +#define CALG_SSL3_SHAMD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SSL3SHAMD5) +#define CALG_SSL3_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SSL3_MASTER) +#define CALG_SCHANNEL_MASTER_HASH (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_MASTER_HASH) +#define CALG_SCHANNEL_MAC_KEY (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_MAC_KEY) +#define CALG_SCHANNEL_ENC_KEY (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_ENC_KEY) +#define CALG_PCT1_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_PCT1_MASTER) +#define CALG_SSL2_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SSL2_MASTER) +#define CALG_TLS1_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_TLS1_MASTER) +#define CALG_RC5 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_RC5) +#define CALG_HMAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HMAC) +#define CALG_TLS1PRF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_TLS1PRF) #if (NTDDI_VERSION >= NTDDI_WINXP) -#define CALG_HASH_REPLACE_OWF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HASH_REPLACE_OWF) -#define CALG_AES_128 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_128) -#define CALG_AES_192 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_192) -#define CALG_AES_256 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_256) -#define CALG_AES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES) +#define CALG_HASH_REPLACE_OWF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HASH_REPLACE_OWF) +#define CALG_AES_128 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_128) +#define CALG_AES_192 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_192) +#define CALG_AES_256 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_256) +#define CALG_AES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES) #endif //(NTDDI_VERSION >= NTDDI_WINXP) #if (NTDDI_VERSION > NTDDI_WINXPSP2) -#define CALG_SHA_256 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_256) -#define CALG_SHA_384 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_384) -#define CALG_SHA_512 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_512) +#define CALG_SHA_256 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_256) +#define CALG_SHA_384 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_384) +#define CALG_SHA_512 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_512) #endif //(NTDDI_VERSION > NTDDI_WINXPSP2) #if (NTDDI_VERSION >= NTDDI_VISTA) -#define CALG_ECDH (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_ECDH) -#define CALG_ECDH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ECDH | ALG_SID_ECDH_EPHEM) -#define CALG_ECMQV (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_ECMQV) -#define CALG_ECDSA (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_ECDSA) -#define CALG_NULLCIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_ANY | 0) +#define CALG_ECDH (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_ECDH) +#define CALG_ECDH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ECDH | ALG_SID_ECDH_EPHEM) +#define CALG_ECMQV (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_ECMQV) +#define CALG_ECDSA (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_ECDSA) +#define CALG_NULLCIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_ANY | 0) #endif //(NTDDI_VERSION >= NTDDI_VISTA) #if (NTDDI_VERSION >= NTDDI_WIN10_RS1) -#define CALG_THIRDPARTY_KEY_EXCHANGE (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) -#define CALG_THIRDPARTY_SIGNATURE (ALG_CLASS_SIGNATURE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) -#define CALG_THIRDPARTY_CIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) -#define CALG_THIRDPARTY_HASH (ALG_CLASS_HASH | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_KEY_EXCHANGE (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_SIGNATURE (ALG_CLASS_SIGNATURE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_CIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_HASH (ALG_CLASS_HASH | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) #endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1) #include + typedef struct _MINCRYPTOAPI_BLOB { UINT32 Size; - _Field_size_bytes_(Size) UINT8* Data; + _Field_size_bytes_(Size) UINT8* Data; -} MINCRYPT_INTEGER_BLOB, * PMINCRYPT_INTEGER_BLOB, -MINCRYPT_UINT_BLOB, * PMINCRYPT_UINT_BLOB, -MINCRYPT_OBJID_BLOB, * PMINCRYPT_OBJID_BLOB, -MINCERT_NAME_BLOB, * PMINCERT_NAME_BLOB, -MINCERT_RDN_VALUE_BLOB, * PMINCERT_RDN_VALUE_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; +} MINCRYPT_INTEGER_BLOB, *PMINCRYPT_INTEGER_BLOB, MINCRYPT_UINT_BLOB, *PMINCRYPT_UINT_BLOB, MINCRYPT_OBJID_BLOB, + *PMINCRYPT_OBJID_BLOB, MINCERT_NAME_BLOB, *PMINCERT_NAME_BLOB, MINCERT_RDN_VALUE_BLOB, *PMINCERT_RDN_VALUE_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 { - _Field_size_bytes_(Size) PCHAR Data; + _Field_size_bytes_(Size) PCHAR Data; USHORT Size; -}MINCRYPT_NAME_BLOB, * PMINCRYPT_NAME_BLOB; +} MINCRYPT_NAME_BLOB, *PMINCRYPT_NAME_BLOB; -#define MINCRYPT_MAX_HASH_LENGTH (64) -#define MINCRYPT_SHA1_LENGTH (160/8) -#define MINCRYPT_SHA256_LENGTH (256/8) +#define MINCRYPT_MAX_HASH_LENGTH (64) +#define MINCRYPT_SHA1_LENGTH (160 / 8) +#define MINCRYPT_SHA256_LENGTH (256 / 8) +#define MINCRYPT_SHA384_LENGTH (384 / 8) +#define MINCRYPT_SHA512_LENGTH (512 / 8) typedef struct _MINCRYPT_CHAIN_ELEMENT { - ALG_ID HashAlgId; // CALG_SHA1, CALG_SHA_256 - UINT32 HashSize; // SHA1(160bit/8), SHA256(256bit/8) - UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH]; + ALG_ID HashAlgId; // CALG_SHA1, CALG_SHA_256 + UINT32 HashSize; // SHA1(160bit/8), SHA256(256bit/8) + UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH]; MINCRYPT_NAME_BLOB Subject; MINCRYPT_NAME_BLOB Issuer; - MINCERT_BLOB Certificate; + MINCERT_BLOB Certificate; -}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 { - UINT32 Size; + UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data. - MINCERT_BLOB* PublicKeys; - UINT32 NumberOfPublicKeys; + MINCERT_BLOB* PublicKeys; + UINT32 NumberOfPublicKeys; - MINCERT_BLOB* EKUs; - UINT32 NumberOfEKUs; + MINCERT_BLOB* EKUs; + UINT32 NumberOfEKUs; #if (NTDDI_VERSION >= NTDDI_WIN10) 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; - // 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 @@ -333,60 +340,66 @@ typedef struct _MINCRYPT_CHAIN_INFO // ChainElements[NumberOfChainElement] -}MINCRYPT_CHAIN_INFO, * PMINCRYPT_CHAIN_INFO; +} MINCRYPT_CHAIN_INFO, *PMINCRYPT_CHAIN_INFO; typedef struct _MINCRYPT_POLICY_INFO { - UINT32 Size; - UINT32 VerificationStatus; - UINT32 PolicyBits; + UINT32 Size; + NTSTATUS VerificationStatus; // Check API return status separately. + UINT32 PolicyBits; MINCRYPT_CHAIN_INFO* ChainInfo; - LARGE_INTEGER RevocationTime; // When was the certificate revoked (if applicable) + LARGE_INTEGER RevocationTime; // When was the certificate revoked (if applicable) #if (NTDDI_VERSION >= NTDDI_WIN10) - LARGE_INTEGER NotBefore; // The certificate is not valid before this time - LARGE_INTEGER NotAfter; // The certificate is not valid after this time + LARGE_INTEGER NotBefore; // Observed: signer certificate validity start. + LARGE_INTEGER NotAfter; // Observed: signer certificate validity end. #endif // NTDDI_VERSION >= NTDDI_WIN10 -}MINCRYPT_POLICY_INFO, PMINCRYPT_POLICY_INFO; +} MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO; + #include - - /** -* Resets a PolicyInfo struct - frees the dynamically allocated buffer in PolicyInfo (ChainInfo) if not null. -* Zeros the entire PolicyInfo struct. +* Release a known initialized policy's owned ChainInfo allocation and reset it. +* 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) MINCRYPTAPI -PVOID -NTAPI +PVOID NTAPI CiFreePolicyInfo( _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 * 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 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 * @@ -396,94 +409,109 @@ CiFreePolicyInfo( * * @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: -* 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_EXPIRED - the certificate used for signing the file has expired */ _IRQL_requires_max_(PASSIVE_LEVEL) MINCRYPTAPI -NTSTATUS -NTAPI +NTSTATUS NTAPI CiCheckSignedFile( - _In_ PVOID Hash, - _In_ UINT32 HashSize, - _In_ ALG_ID HashAlgId, - _In_ PVOID SecurityDirectory, - _In_ UINT32 SizeOfSecurityDirectory, + _In_reads_bytes_(HashSize) PVOID Hash, + _In_ UINT32 HashSize, + _In_ ALG_ID HashAlgId, + _In_reads_bytes_(SizeOfSecurityDirectory) PVOID SecurityDirectory, + _In_ UINT32 SizeOfSecurityDirectory, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo, - _Out_ LARGE_INTEGER* SigningTime, + _Out_ LARGE_INTEGER* SigningTime, _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! * -* @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 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 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 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: -* 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_EXPIRED - the certificate used for signing the file has expired */ _IRQL_requires_max_(PASSIVE_LEVEL) MINCRYPTAPI -NTSTATUS -NTAPI +NTSTATUS NTAPI CiVerifyHashInCatalog( - _In_ PVOID Hash, - _In_ UINT32 HashSize, - _In_ ALG_ID HashAlgId, - _In_ BOOLEAN IsReloadCatalogs, - _In_ UINT32 Always0, - _In_ UINT32 Always2007F, + _In_reads_bytes_(HashSize) PVOID Hash, + _In_ UINT32 HashSize, + _In_ ALG_ID HashAlgId, + _In_ BOOLEAN ReloadCatalogs, + _In_ UINT32 RequireTrustedCatalog, + _In_ UINT32 AcceptedCatalogPolicyBits, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo, - _Out_opt_ UNICODE_STRING* CatalogName, - _Out_ LARGE_INTEGER* SigningTime, + _Out_opt_ UNICODE_STRING* CatalogName, + _Out_ LARGE_INTEGER* SigningTime, _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo ); - #if (NTDDI_VERSION >= NTDDI_WIN10) -typedef -_IRQL_requires_same_ -_Function_class_(MINCRYPT_ALLOCATE_ROUTINE) -__drv_allocatesMem(Mem) +typedef _IRQL_requires_same_ _Function_class_(MINCRYPT_ALLOCATE_ROUTINE) +__drv_allocatesMem( + Mem +) PVOID NTAPI -MINCRYPT_ALLOCATE_ROUTINE( - _In_ SIZE_T ByteSize -); +MINCRYPT_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize); 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 * @@ -491,7 +519,10 @@ typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE; * * @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 NTSTATUS @@ -502,23 +533,40 @@ CiGetCertPublisherName( _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 -VOID -NTAPI +VOID NTAPI CiSetTrustedOriginClaimId( _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 * the certificates that was used for signing (the entire certificate chain) * * @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. * @@ -526,32 +574,37 @@ CiSetTrustedOriginClaimId( * * @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) * -* @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: -* 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_EXPIRED - the certificate used for signing the file has expired */ _IRQL_requires_max_(PASSIVE_LEVEL) MINCRYPTAPI -NTSTATUS -NTAPI +NTSTATUS NTAPI CiValidateFileObject( - _In_ FILE_OBJECT* FileObject, - _In_opt_ UINT32 Unkonwn1, - _In_opt_ UINT32 Unkonwn2, + _In_ FILE_OBJECT* FileObject, + _In_ UINT32 ValidationFlags, + _In_ UINT32 RequestedSigningLevel, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo, - _Out_ LARGE_INTEGER* SigningTime, - _Out_ UINT8* Hash, - _Inout_ UINT32* HashSize, - _Out_ ALG_ID* HashAlgId + _Out_ LARGE_INTEGER* SigningTime, + _Out_writes_bytes_(*HashSize) UINT8* Hash, + _Inout_ UINT32* HashSize, + _Out_ ALG_ID* HashAlgId ); #endif // NTDDI_VERSION >= NTDDI_WIN10 diff --git a/include/Domito.h b/include/Domito.h index 68d22f4..d3e0737 100644 --- a/include/Domito.h +++ b/include/Domito.h @@ -2,7 +2,6 @@ #include - /* ___ * / __|___ _ __ _ __ ___ _ _ * | (__/ _ \ ' \| ' \/ _ \ ' \ @@ -12,7 +11,7 @@ // // Library initialization tasks. Call once in your DriverEntry -// +// _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) @@ -25,7 +24,6 @@ EXTERN_C void DomitoShutdown(); - /* __ __ __ __ _ * | \/ |___ _ __ ___ _ _ _ _ | \/ |__ _ _ _ __ _ __ _ ___ _ __ ___ _ _| |_ * | |\/| / -_) ' \/ _ \ '_| || | | |\/| / _` | ' \/ _` / _` / -_) ' \/ -_) ' \ _| @@ -35,34 +33,26 @@ DomitoShutdown(); // // Allocator function the library uses. -// -typedef -_IRQL_requires_same_ -_Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE) -__drv_allocatesMem(mem) +// +typedef _IRQL_requires_same_ _Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE) +__drv_allocatesMem( + mem +) PVOID NTAPI -EVT_DOMITO_ALLOCATE_ROUTINE( - _In_ SIZE_T ByteSize -); +EVT_DOMITO_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize); typedef EVT_DOMITO_ALLOCATE_ROUTINE* PFN_DOMITO_ALLOCATE_ROUTINE; // // Freeing function the library uses. -// -typedef -_IRQL_requires_same_ -_Function_class_(EVT_DOMITO_FREE_ROUTINE) -void -NTAPI -EVT_DOMITO_FREE_ROUTINE( - _In_ __drv_freesMem(mem) PVOID Memory -); +// +typedef _IRQL_requires_same_ _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; // // Get the original set of Domito memory functions. -// +// EXTERN_C void DomitoGetOriginalMemoryFunctions( @@ -72,7 +62,7 @@ DomitoGetOriginalMemoryFunctions( // // Get the current set of Domito memory functions. -// +// EXTERN_C void DomitoGetMemoryFunctions( @@ -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 void DomitoSetMemoryFunctions( @@ -89,7 +81,6 @@ DomitoSetMemoryFunctions( _In_opt_ PFN_DOMITO_FREE_ROUTINE Free ); - /* ___ _ _ * / __|_ _ _ _ _ __| |_ ___ __ _ _ _ __ _ _ __| |_ _ _ * | (__| '_| || | '_ \ _/ _ \/ _` | '_/ _` | '_ \ ' \ || | @@ -97,9 +88,9 @@ DomitoSetMemoryFunctions( * |__/|_| |___/ |_| |__/ */ - // - // This structure encapsulates a signature used in verifying executable files. - // +// +// This structure encapsulates a signature used in verifying executable files. +// #if !defined(WIN_CERTIFICATE) typedef struct _WIN_CERTIFICATE { @@ -112,16 +103,16 @@ typedef struct _WIN_CERTIFICATE // // UM definitions of WinCrypt.h -// +// #if !defined(WIN_CERT_TYPE_X509) -#define WIN_CERT_TYPE_X509 (0x0001) // The bCertificate member contains an X.509 certificate. +#define WIN_CERT_TYPE_X509 (0x0001) // The bCertificate member contains an X.509 certificate. #endif #if !defined(WIN_CERT_TYPE_PKCS_SIGNED_DATA) -#define WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002) // The bCertificate member contains a PKCS SignedData structure. +#define WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002) // The bCertificate member contains a PKCS SignedData structure. #endif #if !defined(WIN_CERT_TYPE_PKCS1_SIGN) -#define WIN_CERT_TYPE_PKCS1_SIGN (0x0009) // The bCertificate member contains PKCS1_MODULE_SIGN fields. +#define WIN_CERT_TYPE_PKCS1_SIGN (0x0009) // The bCertificate member contains PKCS1_MODULE_SIGN fields. #endif #if !defined(CALG_SHA1) @@ -141,6 +132,7 @@ typedef struct _WIN_CERTIFICATE // Converts a WinCrypt CALG_ID to a BCRYPT_ALGORITHM identifier. // PCWSTR + FORCEINLINE DOMITO_CALG_TO_BCRYPT_ALGORITHM( _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 -// calculate the message digest when it was signed +// Heuristically detects the CALG_ID in the first WIN_CERTIFICATE using byte patterns. +// 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) EXTERN_C -UINT32 +__declspec(deprecated( + "Use DomitoGetPortableExecutableDigestKindEx; heuristic detection silently falls back to SHA-1. Check the returned NTSTATUS." +)) UINT32 DomitoGetPortableExecutableDigestKind( _In_ PUCHAR pPeBytes, _In_ ULONG PeSize, _In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory ); -// -// Extracts Authenticode signing information and calculates the file digest of a PE file. -// +// Reads the first PKCS#7 entry's SpcIndirectDataContent DigestInfo algorithm +// 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) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) EXTERN_C 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( _In_ PUCHAR pPeBytes, _In_ ULONG PeSize, @@ -202,7 +241,7 @@ DomitoCalculatePortableExecutableDigest( // // Frees the memory allocated by DomitoCalculatePortableExecutableDigest. -// +// _IRQL_requires_max_(DISPATCH_LEVEL) EXTERN_C void @@ -210,14 +249,121 @@ DomitoFreePortableExecutableDigest( _In_ PVOID pDigestOut ); -// -// Verifies if the Authenticode signature of a give PE file matches the provided (e.g. SHA1) file digest. -// +#if (NTDDI_VERSION >= NTDDI_WIN10) +// 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) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) EXTERN_C 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( _In_ HANDLE FileHandle, _In_ PVOID Hash, @@ -229,7 +375,6 @@ DomitoValidateFileLegacyMode( _Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo ); - /* __ __ _ * | \/ (_)___ __ * | |\/| | (_-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(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((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; +} diff --git a/src/Domito.CodeIntegrity.Validation.cpp b/src/Domito.CodeIntegrity.Validation.cpp new file mode 100644 index 0000000..aeee18c --- /dev/null +++ b/src/Domito.CodeIntegrity.Validation.cpp @@ -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 diff --git a/src/Domito.CodeIntegrity.cpp b/src/Domito.CodeIntegrity.cpp index 8a22b33..56ee962 100644 --- a/src/Domito.CodeIntegrity.cpp +++ b/src/Domito.CodeIntegrity.cpp @@ -8,7 +8,6 @@ #include "Domito.Internal.h" #include "Domito.MinCrypt.h" - /* _ _ _ _ _ _ _ ___ _ _ * | | | | |_(_) (_) |_ _ _ | __| _ _ _ __| |_(_)___ _ _ ___ * | |_| | _| | | | _| || | | _| || | ' \/ _| _| / _ \ ' \(_-< @@ -24,43 +23,53 @@ DomitoGetPortableExecutableDigestKind( _In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory ) { - const ULONG securityRva = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress; - - if (!securityRva) + if (!pPeBytes || !pImgDataDirectory) { return CALG_SHA1; } - if (securityRva > PeSize || (PeSize - securityRva) < sizeof(WIN_CERTIFICATE)) + const IMAGE_DATA_DIRECTORY security = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; + const ULONG securityRva = security.VirtualAddress; + const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate); + + if (!securityRva || securityRva > PeSize || security.Size > PeSize - securityRva || + security.Size < certificateHeaderSize) + { + return CALG_SHA1; + } + + ULONG certificateSize; + memcpy(&certificateSize, pPeBytes + securityRva, sizeof(certificateSize)); + + if (certificateSize < certificateHeaderSize || certificateSize > security.Size) { return CALG_SHA1; } const PVOID pBase = pPeBytes + securityRva; - const LPWIN_CERTIFICATE pCert = (WIN_CERTIFICATE*)pBase; - const ULONG searchLen = min(pCert->dwLength, PeSize - securityRva); + const ULONG searchLen = certificateSize; PUCHAR pMatch = NULL; if (NT_SUCCESS(DomitoMemorySearchPattern( - (const PUCHAR)"\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", - 0x00, - 15, - pBase, - searchLen, - (PVOID*)&pMatch - ))) + (const PUCHAR) "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", + 0x00, + 15, + pBase, + searchLen, + (PVOID*)&pMatch + ))) { return CALG_SHA1; } if (NT_SUCCESS(DomitoMemorySearchPattern( - (const PUCHAR)"\x30\xcc\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\xcc\x05\x00\x04\xcc", - 0xcc, - 19, - pBase, - searchLen, - (PVOID*)&pMatch - ))) + (const PUCHAR) "\x30\xcc\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\xcc\x05\x00\x04\xcc", + 0xcc, + 19, + pBase, + searchLen, + (PVOID*)&pMatch + ))) { if (pMatch == NULL) { @@ -84,308 +93,672 @@ DomitoGetPortableExecutableDigestKind( return CALG_SHA1; } +// Bounded DER navigation for the outer Authenticode content only. This does not +// validate certificates, signerInfos, nested signatures, or cryptographic trust. +typedef struct _DOMITO_DER_VIEW +{ + const UCHAR* Bytes; + ULONG Length; +} DOMITO_DER_VIEW; + +static BOOLEAN +DomitoDerTake( + DOMITO_DER_VIEW* Input, + UCHAR Tag, + DOMITO_DER_VIEW* Value +) +{ + if (Input->Length < 2 || Input->Bytes[0] != Tag) + { + return FALSE; + } + + ULONG length = Input->Bytes[1]; + ULONG header = 2; + + if (length & 0x80) + { + const ULONG count = length & 0x7F; + + if (!count || count > sizeof(ULONG) || count > Input->Length - header || Input->Bytes[header] == 0) + { + return FALSE; // No indefinite or nonminimal DER lengths. + } + + length = 0; + + for (ULONG i = 0; i < count; ++i) + { + length = (length << 8) | Input->Bytes[header++]; + } + + if (length < 0x80) + { + return FALSE; + } + } + + if (length > Input->Length - header) + { + return FALSE; + } + + Value->Bytes = Input->Bytes + header; + Value->Length = length; + Input->Bytes += header + length; + Input->Length -= header + length; + return TRUE; +} + +static BOOLEAN +DomitoDerOid( + DOMITO_DER_VIEW* Input, + const UCHAR* Oid, + ULONG Size +) +{ + DOMITO_DER_VIEW value; + return DomitoDerTake(Input, 0x06, &value) && value.Length == Size && memcmp(value.Bytes, Oid, Size) == 0; +} + +_IRQL_requires_max_(DISPATCH_LEVEL) +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 +) +{ + if (!DigestCalg) + { + return STATUS_INVALID_PARAMETER; + } + + *DigestCalg = 0; + + if (!PeBytes || !Directories) + { + return STATUS_INVALID_PARAMETER; + } + + const IMAGE_DATA_DIRECTORY security = Directories[IMAGE_DIRECTORY_ENTRY_SECURITY]; + + if (!security.VirtualAddress && !security.Size) + { + return STATUS_NOT_FOUND; + } + + if (!security.VirtualAddress || (security.VirtualAddress & 7) || security.VirtualAddress > PeSize || + security.Size > PeSize - security.VirtualAddress || security.Size < 8) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + ULONG length; + USHORT revision; + USHORT type; + const UCHAR* certificate = PeBytes + security.VirtualAddress; + memcpy(&length, certificate, 4); + memcpy(&revision, certificate + 4, 2); + memcpy(&type, certificate + 6, 2); + + if (length <= 8 || length > security.Size) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA) + { + return STATUS_NOT_SUPPORTED; + } + + const UCHAR signedDataOid[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02}; + const UCHAR indirectOid[] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x04}; + const UCHAR sha1Oid[] = {0x2B, 0x0E, 0x03, 0x02, 0x1A}; + const UCHAR sha2Prefix[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02}; + DOMITO_DER_VIEW payload = {certificate + 8, length - 8}; + DOMITO_DER_VIEW content; + DOMITO_DER_VIEW wrapped; + DOMITO_DER_VIEW signedData; + DOMITO_DER_VIEW ignored; + DOMITO_DER_VIEW contentInfo; + DOMITO_DER_VIEW indirect; + DOMITO_DER_VIEW digest; + DOMITO_DER_VIEW algorithm; + DOMITO_DER_VIEW oid; + + if (!DomitoDerTake(&payload, 0x30, &content) || !DomitoDerOid(&content, signedDataOid, sizeof(signedDataOid)) || + !DomitoDerTake(&content, 0xA0, &wrapped) || content.Length || !DomitoDerTake(&wrapped, 0x30, &signedData) || + wrapped.Length || !DomitoDerTake(&signedData, 0x02, &ignored) || !DomitoDerTake(&signedData, 0x31, &ignored) || + !DomitoDerTake(&signedData, 0x30, &contentInfo) || + !DomitoDerOid(&contentInfo, indirectOid, sizeof(indirectOid)) || !DomitoDerTake(&contentInfo, 0xA0, &wrapped) || + contentInfo.Length || !DomitoDerTake(&wrapped, 0x30, &indirect) || wrapped.Length || + !DomitoDerTake(&indirect, 0x30, &ignored) || !DomitoDerTake(&indirect, 0x30, &digest) || indirect.Length || + !DomitoDerTake(&digest, 0x30, &algorithm) || !DomitoDerTake(&algorithm, 0x06, &oid)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + // Windows-signed entries can include up to seven zero alignment bytes in + // dwLength itself. Accept only that bounded, aligned padding after the DER. + if (payload.Length > 7 || (payload.Length && (length & 7))) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + for (ULONG i = 0; i < payload.Length; ++i) + { + if (payload.Bytes[i]) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + } + + if (algorithm.Length && (!DomitoDerTake(&algorithm, 0x05, &ignored) || ignored.Length || algorithm.Length)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + UINT32 calg = 0; + ULONG expectedLength = 0; + + if (oid.Length == sizeof(sha1Oid) && !memcmp(oid.Bytes, sha1Oid, sizeof(sha1Oid))) + { + calg = CALG_SHA1; + expectedLength = 20; + } + else if (oid.Length == sizeof(sha2Prefix) + 1 && !memcmp(oid.Bytes, sha2Prefix, sizeof(sha2Prefix))) + { + switch (oid.Bytes[sizeof(sha2Prefix)]) + { + case 1: + calg = CALG_SHA256; + expectedLength = 32; + break; + case 2: + calg = CALG_SHA384; + expectedLength = 48; + break; + case 3: + calg = CALG_SHA512; + expectedLength = 64; + break; + } + } + + if (!calg) + { + return STATUS_NOT_SUPPORTED; + } + + if (!DomitoDerTake(&digest, 0x04, &ignored) || digest.Length || ignored.Length != expectedLength) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + *DigestCalg = calg; + return STATUS_SUCCESS; +} + +// Conservative section-layout boundary: contiguous raw sections (any header +// order), optional overlay before an EOF certificate table. Gaps, overlaps, and +// certificates in the middle are rejected rather than assigned an ambiguous hash. +static NTSTATUS +DomitoValidateAuthenticodeLayout( + const UCHAR* Bytes, + ULONG Size +) +{ + if (!Bytes || Size < sizeof(IMAGE_DOS_HEADER)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + IMAGE_DOS_HEADER dos; + memcpy(&dos, Bytes, sizeof(dos)); + + if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < 0) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + const ULONG nt = (ULONG)dos.e_lfanew; + const ULONG prefix = sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER); + + if (nt > Size || prefix > Size - nt) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + ULONG signature; + IMAGE_FILE_HEADER file; + memcpy(&signature, Bytes + nt, sizeof(signature)); + memcpy(&file, Bytes + nt + sizeof(signature), sizeof(file)); + + if (signature != IMAGE_NT_SIGNATURE || !file.NumberOfSections || file.NumberOfSections > 96) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + const ULONG optional = nt + prefix; + + if (file.SizeOfOptionalHeader < 2 || file.SizeOfOptionalHeader > Size - optional) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + USHORT magic; + memcpy(&magic, Bytes + optional, sizeof(magic)); + ULONG directory; + + if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) + { + directory = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory); + } + else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) + { + directory = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory); + } + else + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + if (file.SizeOfOptionalHeader < directory) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + ULONG count; + ULONG headers; + memcpy(&count, Bytes + optional + directory - 4, sizeof(count)); + memcpy(&headers, Bytes + optional + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfHeaders), sizeof(headers)); + + if (count > (file.SizeOfOptionalHeader - directory) / sizeof(IMAGE_DATA_DIRECTORY)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + if (count <= IMAGE_DIRECTORY_ENTRY_SECURITY) + { + return STATUS_NOT_SUPPORTED; // SIP exclusion behavior differs for absent slots. + } + + const ULONG table = optional + file.SizeOfOptionalHeader; + const ULONG tableSize = file.NumberOfSections * sizeof(IMAGE_SECTION_HEADER); + + if (tableSize > Size - table || headers < table + tableSize || headers > Size) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + struct + { + ULONG Offset; + ULONG Length; + } sections[96]; + + ULONG used = 0; + + for (ULONG i = 0; i < file.NumberOfSections; ++i) + { + IMAGE_SECTION_HEADER section; + memcpy(§ion, Bytes + table + i * sizeof(section), sizeof(section)); + + if (!section.SizeOfRawData) + { + continue; + } + + if (section.PointerToRawData < headers || section.PointerToRawData > Size || + section.SizeOfRawData > Size - section.PointerToRawData) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + ULONG at = used; + while (at && sections[at - 1].Offset > section.PointerToRawData) + { + sections[at] = sections[at - 1]; + --at; + } + + sections[at].Offset = section.PointerToRawData; + sections[at].Length = section.SizeOfRawData; + ++used; + } + + ULONG end = headers; + + for (ULONG i = 0; i < used; ++i) + { + if (sections[i].Offset != end) + { + return STATUS_NOT_SUPPORTED; + } + + end += sections[i].Length; + } + + if (count > IMAGE_DIRECTORY_ENTRY_SECURITY) + { + IMAGE_DATA_DIRECTORY security; + memcpy( + &security, + Bytes + optional + directory + IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(security), + sizeof(security) + ); + + if (security.VirtualAddress || security.Size) + { + if (!security.VirtualAddress || !security.Size || (security.VirtualAddress & 7) || + security.VirtualAddress < end || security.VirtualAddress > Size || + security.Size != Size - security.VirtualAddress) + { + return STATUS_NOT_SUPPORTED; + } + + // Validate every aligned certificate entry, not just the first. + ULONG offset = security.VirtualAddress; + while (offset < Size) + { + if (Size - offset < 8) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + ULONG length; + USHORT revision; + USHORT type; + memcpy(&length, Bytes + offset, 4); + memcpy(&revision, Bytes + offset + 4, 2); + memcpy(&type, Bytes + offset + 6, 2); + + if (length <= 8 || length > Size - offset) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA) + { + return STATUS_NOT_SUPPORTED; + } + + const ULONG padding = (8 - (length & 7)) & 7; + + if (padding > Size - offset - length) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + offset += length + padding; + } + } + } + + return STATUS_SUCCESS; +} + _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) -NTSTATUS -DomitoCalculatePortableExecutableDigest( +static NTSTATUS +DomitoCalculatePortableExecutableDigestInternal( + _In_ UINT32 ExplicitCalg, _In_ PUCHAR pPeBytes, _In_ ULONG PeSize, _Out_ PUINT32 pDigestCalgOut, _Out_ PULONG pDigestSizeOut, - _Out_ PVOID * pDigestOut, - _Outptr_result_maybenull_ LPWIN_CERTIFICATE * pCertOut, + _Out_ PVOID* pDigestOut, + _Outptr_result_maybenull_ LPWIN_CERTIFICATE* pCertOut, _Out_ PULONG pSizeOfSecurityDirectory ) { - PIMAGE_DATA_DIRECTORY pImgDataDirectory; - BOOLEAN is64Bit; - PUCHAR pHash = nullptr; - NTSTATUS status; - BCRYPT_ALG_HANDLE hbAlg = nullptr; - BCRYPT_HASH_HANDLE hbHash = nullptr; - UINT32 hashLength = 0; - ULONG resultLength; - ULONG fileOffset = 0; + if (!pDigestCalgOut || !pDigestSizeOut || !pDigestOut || !pCertOut || !pSizeOfSecurityDirectory) + { + return STATUS_INVALID_PARAMETER; + } + + *pDigestCalgOut = 0; + *pDigestSizeOut = 0; + *pDigestOut = nullptr; + *pCertOut = nullptr; + *pSizeOfSecurityDirectory = 0; + + if (!pPeBytes) + { + return STATUS_INVALID_PARAMETER; + } if (PeSize < sizeof(IMAGE_DOS_HEADER)) { return STATUS_INVALID_IMAGE_NOT_MZ; } - const PIMAGE_DOS_HEADER phDos = (IMAGE_DOS_HEADER*)pPeBytes; - if (phDos->e_magic != IMAGE_DOS_SIGNATURE) + // Copy fields rather than assuming the caller's buffer is aligned. + IMAGE_DOS_HEADER dos = {}; + memcpy(&dos, pPeBytes, sizeof(dos)); + + if (dos.e_magic != IMAGE_DOS_SIGNATURE) { - // Not an executable return STATUS_INVALID_IMAGE_NOT_MZ; } - if (phDos->e_lfanew > PeSize - sizeof(IMAGE_NT_HEADERS)) + if (dos.e_lfanew < 0) { return STATUS_INVALID_IMAGE_FORMAT; } - const PIMAGE_NT_HEADERS phNt = (PIMAGE_NT_HEADERS)(pPeBytes + phDos->e_lfanew); - if (phNt->Signature != IMAGE_NT_SIGNATURE) + const ULONG ntOffset = (ULONG)dos.e_lfanew; + const ULONG ntPrefixSize = sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER); + + if (ntOffset > PeSize || ntPrefixSize > PeSize - ntOffset) { - // Not a PE image return STATUS_INVALID_IMAGE_FORMAT; } - // - // Check if PE is 32 or 64 bits - // - switch (phNt->OptionalHeader.Magic) + ULONG signature; + IMAGE_FILE_HEADER fileHeader; + memcpy(&signature, pPeBytes + ntOffset, sizeof(signature)); + memcpy(&fileHeader, pPeBytes + ntOffset + sizeof(signature), sizeof(fileHeader)); + + if (signature != IMAGE_NT_SIGNATURE) { - case IMAGE_NT_OPTIONAL_HDR32_MAGIC: - is64Bit = FALSE; - break; - case IMAGE_NT_OPTIONAL_HDR64_MAGIC: - is64Bit = TRUE; - break; - default: - return STATUS_INVALID_IMAGE_FORMAT; // Unsupported architecture + return STATUS_INVALID_IMAGE_FORMAT; } - // - // TODO: Not sure if 16 * 512 * 512 is right. Do something better! - // - ULONG copySize = phDos->e_lfanew + sizeof(IMAGE_FILE_HEADER) + 4 + 0x40; -#pragma warning(disable:4996) - const PUCHAR pBuf = (PUCHAR)ExAllocatePoolWithTag( - NonPagedPoolNx, - 16 * 512 * 512, - DOMITO_POOL_TAG - ); -#pragma warning(default:4996) - if (!pBuf) + const ULONG optionalOffset = ntOffset + ntPrefixSize; + const ULONG optionalSize = fileHeader.SizeOfOptionalHeader; + + if (optionalSize < sizeof(USHORT) || optionalSize > PeSize - optionalOffset) { - return STATUS_INSUFFICIENT_RESOURCES; + return STATUS_INVALID_IMAGE_FORMAT; } - // - // Fetch the data directory from the PE. - // Note that we can also use RtlImageDirectoryEntryToData to fetch - // only the security directory entry but why not do it manually - // when we have the full PE image anyway - // - if (is64Bit == TRUE) + USHORT magic; + memcpy(&magic, pPeBytes + optionalOffset, sizeof(magic)); + ULONG directoryOffset; + ULONG checksumOffset; + ULONG directoryCountOffset; + + if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { - pImgDataDirectory = ((PIMAGE_NT_HEADERS64)phNt)->OptionalHeader.DataDirectory; + directoryOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory); + checksumOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, CheckSum); + directoryCountOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, NumberOfRvaAndSizes); + } + else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) + { + directoryOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory); + checksumOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, CheckSum); + directoryCountOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, NumberOfRvaAndSizes); } else { - pImgDataDirectory = ((PIMAGE_NT_HEADERS32)phNt)->OptionalHeader.DataDirectory; + return STATUS_INVALID_IMAGE_FORMAT; } - // - // Check if the PE file contains a signature - // - const BOOLEAN hasEmbeddedSig = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress != 0; + if (optionalSize < directoryOffset) + { + return STATUS_INVALID_IMAGE_FORMAT; + } - // - // Boring bcrypt boilerplate code - // - const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, pImgDataDirectory); + ULONG directoryCount; + memcpy(&directoryCount, pPeBytes + optionalOffset + directoryCountOffset, sizeof(directoryCount)); - if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider( - &hbAlg, - DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), // bcrypt doesn't understand standard WinCrypt CALG IDs - MS_PRIMITIVE_PROVIDER, - 0 - ))) + if (directoryCount > (optionalSize - directoryOffset) / sizeof(IMAGE_DATA_DIRECTORY)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + checksumOffset += optionalOffset; + const BOOLEAN hasSecurityEntry = directoryCount > IMAGE_DIRECTORY_ENTRY_SECURITY; + IMAGE_DATA_DIRECTORY directories[IMAGE_DIRECTORY_ENTRY_SECURITY + 1] = {}; + ULONG securityEntryOffset = PeSize; + ULONG dataOffset = PeSize; + + if (hasSecurityEntry) + { + securityEntryOffset = + optionalOffset + directoryOffset + IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(IMAGE_DATA_DIRECTORY); + memcpy( + &directories[IMAGE_DIRECTORY_ENTRY_SECURITY], pPeBytes + securityEntryOffset, sizeof(IMAGE_DATA_DIRECTORY) + ); + dataOffset = securityEntryOffset + sizeof(IMAGE_DATA_DIRECTORY); + } + + const IMAGE_DATA_DIRECTORY security = directories[IMAGE_DIRECTORY_ENTRY_SECURITY]; + const BOOLEAN hasEmbeddedSig = security.VirtualAddress != 0; + + if (hasEmbeddedSig != (security.Size != 0)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + if (hasEmbeddedSig) + { + // The security directory contains a FILE OFFSET, not an image RVA. + const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate); + + if (security.VirtualAddress < dataOffset || security.VirtualAddress > PeSize || + security.Size > PeSize - security.VirtualAddress || security.Size < certificateHeaderSize) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + ULONG certificateSize; + memcpy(&certificateSize, pPeBytes + security.VirtualAddress, sizeof(certificateSize)); + + if (certificateSize < certificateHeaderSize || certificateSize > security.Size) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + } + + PUCHAR pHash = nullptr; + BCRYPT_ALG_HANDLE hbAlg = nullptr; + BCRYPT_HASH_HANDLE hbHash = nullptr; + ULONG hashLength = 0; + ULONG resultLength = 0; + // Intentional compatibility branch; the explicit API never calls the heuristic. +#pragma warning(push) +#pragma warning(disable : 4996) + const UINT32 peDigestKind = + ExplicitCalg ? ExplicitCalg : DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories); +#pragma warning(pop) + NTSTATUS status = + BCryptOpenAlgorithmProvider(&hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0); + + if (!NT_SUCCESS(status)) { goto cleanup; } - if (!NT_SUCCESS(status = BCryptGetProperty( - hbAlg, - BCRYPT_HASH_LENGTH, - (PUCHAR)&hashLength, // put the length of the hash into hashLength - sizeof(hashLength), - &resultLength, - 0 - ))) + status = BCryptGetProperty(hbAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength, sizeof(hashLength), &resultLength, 0); + + if (!NT_SUCCESS(status)) { goto cleanup; } - // - // Allocate a buffer to store the resulting hash - // -#pragma warning(disable:4996) +#pragma warning(disable : 4996) pHash = (PUCHAR)ExAllocatePoolWithTag(NonPagedPoolNx, hashLength, DOMITO_POOL_TAG); -#pragma warning(default:4996) +#pragma warning(default : 4996) if (!pHash) { status = STATUS_INSUFFICIENT_RESOURCES; goto cleanup; } - RtlZeroMemory(pHash, hashLength); - // - // Create a handle to the resulting hash - // - if (!NT_SUCCESS(status = BCryptCreateHash( - hbAlg, - &hbHash, - NULL, - 0, - NULL, - 0, - 0 - ))) - { - goto cleanup; - } + status = BCryptCreateHash(hbAlg, &hbHash, nullptr, 0, nullptr, 0, 0); - // - // Ok, if we haven't BSODed yet, we're ready to continue - // parsing the PE/COFF and hash the needed values - // - memcpy(pBuf, pPeBytes, copySize); - fileOffset += copySize; - - // - // Hash everything up to the Checksum then skip it - // - status = BCryptHashData( - hbHash, - pBuf, - copySize, - 0 - ); if (!NT_SUCCESS(status)) { goto cleanup; } - fileOffset += 4; // Skipping the checksum field here + // Keep the existing linear hashing convention, but hash validated ranges + // directly: no fixed-size scratch buffer and no unchecked memcpy lengths. + // General section-ordered Authenticode hashing is a separate change. + status = BCryptHashData(hbHash, pPeBytes, checksumOffset, 0); - // - // Reach the security directory information. - // For x64 PEs it's 10 bytes further - // - copySize = 0x3C; - if (is64Bit == TRUE) - { - copySize += 0x10; - } - - memcpy(pBuf, pPeBytes + fileOffset, copySize); - fileOffset += copySize; - - // - // Again, hash everything up to here then skip the ignored field. - // - status = BCryptHashData( - hbHash, - pBuf, - copySize, - 0 - ); if (!NT_SUCCESS(status)) { goto cleanup; } - fileOffset += 8; // Skipping an other ignored field here + status = BCryptHashData( + hbHash, pPeBytes + checksumOffset + sizeof(ULONG), securityEntryOffset - checksumOffset - sizeof(ULONG), 0 + ); - // - // Now hash everything else in the file up to the certificate data. - // - ULONG remaining; - if (hasEmbeddedSig) + if (!NT_SUCCESS(status)) { - const ULONG secRva = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress; - if (secRva < fileOffset) - { - status = STATUS_INVALID_IMAGE_FORMAT; - goto cleanup; - } - remaining = secRva - fileOffset; + goto cleanup; } - else - { - remaining = (fileOffset <= PeSize) ? (PeSize - fileOffset) : 0; - } - while (remaining > 0) + if (hasSecurityEntry) { - const ULONG chunkSize = min(remaining, 4096); - ULONG readBytes; - memset(pBuf, 0, chunkSize); - - if (fileOffset + chunkSize > PeSize) - { - readBytes = PeSize - chunkSize; - memcpy(pBuf, pPeBytes + fileOffset, readBytes); - fileOffset += readBytes; - break; - } - else - { - memcpy(pBuf, pPeBytes + fileOffset, chunkSize); - readBytes = chunkSize; - fileOffset += readBytes; - } + status = BCryptHashData( + hbHash, pPeBytes + dataOffset, (hasEmbeddedSig ? security.VirtualAddress : PeSize) - dataOffset, 0 + ); - if (!NT_SUCCESS(status = BCryptHashData( - hbHash, - pBuf, - readBytes, - 0 - ))) + if (!NT_SUCCESS(status)) { goto cleanup; } + } - remaining -= readBytes; - } + status = BCryptFinishHash(hbHash, pHash, hashLength, 0); - // - // Finish up the hash here - // - if (!NT_SUCCESS(status = BCryptFinishHash( - hbHash, - pHash, - hashLength, - 0 - ))) + if (!NT_SUCCESS(status)) { goto cleanup; } - // - // Supply the results to the caller - // *pDigestCalgOut = peDigestKind; *pDigestSizeOut = hashLength; *pDigestOut = pHash; - if (hasEmbeddedSig == TRUE) - { - *pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress); - *pSizeOfSecurityDirectory = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size; - } - else - { - *pCertOut = NULL; - *pSizeOfSecurityDirectory = 0; - } + pHash = nullptr; // Transfer ownership only after all work has succeeded. - status = STATUS_SUCCESS; + if (hasEmbeddedSig) + { + *pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + security.VirtualAddress); + *pSizeOfSecurityDirectory = security.Size; + } cleanup: - if (pBuf) - { - ExFreePoolWithTag(pBuf, DOMITO_POOL_TAG); - } if (hbHash) { BCryptDestroyHash(hbHash); } + if (pHash) + { + ExFreePoolWithTag(pHash, DOMITO_POOL_TAG); + } + if (hbAlg) { BCryptCloseAlgorithmProvider(hbAlg, 0); @@ -394,6 +767,74 @@ cleanup: return status; } +_Success_(return == STATUS_SUCCESS) +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS +DomitoCalculatePortableExecutableDigest( + PUCHAR PeBytes, + ULONG PeSize, + PUINT32 DigestCalg, + PULONG DigestSize, + PVOID* Digest, + LPWIN_CERTIFICATE* Certificate, + PULONG SecuritySize +) +{ + return DomitoCalculatePortableExecutableDigestInternal( + 0, PeBytes, PeSize, DigestCalg, DigestSize, Digest, Certificate, SecuritySize + ); +} + +_Success_(return == STATUS_SUCCESS) +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS +DomitoCalculatePortableExecutableDigestEx( + const UCHAR* PeBytes, + ULONG PeSize, + UINT32 HashAlgId, + PULONG DigestSize, + PVOID* Digest, + LPWIN_CERTIFICATE* Certificate, + PULONG SecuritySize +) +{ + if (!DigestSize || !Digest || !Certificate || !SecuritySize) + { + return STATUS_INVALID_PARAMETER; + } + + *DigestSize = 0; + *Digest = nullptr; + *Certificate = nullptr; + *SecuritySize = 0; + + if (!PeBytes) + { + return STATUS_INVALID_PARAMETER; + } + + if (HashAlgId != CALG_SHA1 && HashAlgId != CALG_SHA256 && HashAlgId != CALG_SHA384 && HashAlgId != CALG_SHA512) + { + return STATUS_NOT_SUPPORTED; + } + + NTSTATUS status = DomitoValidateAuthenticodeLayout(PeBytes, PeSize); + + if (!NT_SUCCESS(status)) + { + return status; + } + + UINT32 actualCalg = 0; + // Contiguous sorted sections make the validated section sequence identical + // to linear byte order; overlay precedes an EOF certificate table. + return DomitoCalculatePortableExecutableDigestInternal( + HashAlgId, (PUCHAR)PeBytes, PeSize, &actualCalg, DigestSize, Digest, Certificate, SecuritySize + ); +} + _IRQL_requires_max_(DISPATCH_LEVEL) void DomitoFreePortableExecutableDigest( @@ -412,20 +853,36 @@ _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) #pragma code_seg("PAGED") -NTSTATUS -DomitoValidateFileLegacyMode( +static NTSTATUS +DomitoValidateFileLegacyModeInternal( _In_ HANDLE FileHandle, _In_ 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_ 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 ) { PAGED_CODE(); + if (!ValidationSource) + { + return STATUS_INVALID_PARAMETER; + } + + *ValidationSource = 0; + + if (!Hash || !SecurityDirectory || !PolicyInfo || !SigningTime || !TimeStampPolicyInfo || + PolicyInfo == TimeStampPolicyInfo) + { + return STATUS_INVALID_PARAMETER; + } + NTSTATUS status = STATUS_SUCCESS; PVOID certDirectory = nullptr; KAPC_STATE systemContext = {}; @@ -437,22 +894,61 @@ DomitoValidateFileLegacyMode( CiFreePolicyInfo(PolicyInfo); CiFreePolicyInfo(TimeStampPolicyInfo); - if (HashSize != MINCRYPT_SHA1_LENGTH && HashSize != MINCRYPT_SHA256_LENGTH) + if (HashAlgId != CALG_SHA1 && HashAlgId != CALG_SHA256) { - status = STATUS_INVALID_IMAGE_HASH; + status = STATUS_NOT_SUPPORTED; break; } - if (SecurityDirectory->Size != 0u && - SecurityDirectory->VirtualAddress != 0u) + if (HashSize != (UINT32)(HashAlgId == CALG_SHA1 ? MINCRYPT_SHA1_LENGTH : MINCRYPT_SHA256_LENGTH)) { -#pragma warning(disable:4996) - certDirectory = ExAllocatePoolWithTag( - PagedPool, - SecurityDirectory->Size, - DOMITO_POOL_TAG + status = STATUS_INVALID_PARAMETER; + break; + } + + if ((SecurityDirectory->Size != 0) != (SecurityDirectory->VirtualAddress != 0)) + { + status = STATUS_INVALID_IMAGE_FORMAT; + break; + } + + if (SecurityDirectory->Size != 0u && SecurityDirectory->VirtualAddress != 0u) + { + if ((SecurityDirectory->VirtualAddress & 7) || SecurityDirectory->Size < 12) + { + status = STATUS_INVALID_IMAGE_FORMAT; + break; + } + + // Bound the allocation/read against the file before trusting its table. + PFILE_OBJECT fileObject = nullptr; + status = ObReferenceObjectByHandle( + FileHandle, FILE_READ_DATA, *IoFileObjectType, KernelMode, (PVOID*)&fileObject, nullptr ); -#pragma warning(default:4996) + + if (!NT_SUCCESS(status)) + { + break; + } + + LARGE_INTEGER fileSize = {}; + status = FsRtlGetFileSize(fileObject, &fileSize); + ObDereferenceObject(fileObject); + + if (!NT_SUCCESS(status)) + { + break; + } + + if (fileSize.QuadPart < 0 || (ULONGLONG)SecurityDirectory->VirtualAddress > (ULONGLONG)fileSize.QuadPart || + SecurityDirectory->Size > (ULONGLONG)fileSize.QuadPart - SecurityDirectory->VirtualAddress) + { + status = STATUS_INVALID_IMAGE_FORMAT; + break; + } +#pragma warning(disable : 4996) + certDirectory = ExAllocatePoolWithTag(PagedPool, SecurityDirectory->Size, DOMITO_POOL_TAG); +#pragma warning(default : 4996) if (certDirectory == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; @@ -460,33 +956,8 @@ DomitoValidateFileLegacyMode( } LARGE_INTEGER offset = {}; - IO_STATUS_BLOCK ioStatusBlock = {}; - - offset.LowPart = SecurityDirectory->VirtualAddress; - - status = ZwReadFile( - FileHandle, - NULL, - NULL, - NULL, - &ioStatusBlock, - certDirectory, - SecurityDirectory->Size, - &offset, - NULL - ); - - if (status == STATUS_PENDING) - { - ZwWaitForSingleObject( - FileHandle, - FALSE, - NULL - ); - - MemoryBarrier(); - status = ioStatusBlock.Status; - } + offset.QuadPart = SecurityDirectory->VirtualAddress; + status = DomitoReadFileAt(FileHandle, certDirectory, SecurityDirectory->Size, &offset); if (!NT_SUCCESS(status)) { @@ -506,10 +977,12 @@ DomitoValidateFileLegacyMode( TimeStampPolicyInfo ); } + KeUnstackDetachProcess(&systemContext); if (NT_SUCCESS(status)) { + *ValidationSource = DOMITO_CI_SOURCE_EMBEDDED; break; } @@ -519,6 +992,9 @@ DomitoValidateFileLegacyMode( } } + CiFreePolicyInfo(PolicyInfo); + CiFreePolicyInfo(TimeStampPolicyInfo); + SigningTime->QuadPart = 0; KeStackAttachProcess(PsInitialSystemProcess, &systemContext); { status = CiVerifyHashInCatalog( @@ -526,8 +1002,8 @@ DomitoValidateFileLegacyMode( HashSize, HashAlgId, FALSE, - 0, - 0x2007F, + RequireTrustedCatalog, + AcceptedCatalogPolicyBits, PolicyInfo, NULL, SigningTime, @@ -536,13 +1012,16 @@ DomitoValidateFileLegacyMode( if (status == STATUS_INVALID_IMAGE_HASH) { + CiFreePolicyInfo(PolicyInfo); + CiFreePolicyInfo(TimeStampPolicyInfo); + SigningTime->QuadPart = 0; status = CiVerifyHashInCatalog( Hash, HashSize, HashAlgId, TRUE, - 0, - 0x2007F, + RequireTrustedCatalog, + AcceptedCatalogPolicyBits, PolicyInfo, NULL, SigningTime, @@ -550,8 +1029,14 @@ DomitoValidateFileLegacyMode( ); } } + KeUnstackDetachProcess(&systemContext); + if (NT_SUCCESS(status)) + { + *ValidationSource = DOMITO_CI_SOURCE_CATALOG; + } + } while (FALSE); if (certDirectory) @@ -561,4 +1046,69 @@ DomitoValidateFileLegacyMode( return status; } + +_Success_(return == STATUS_SUCCESS) +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS +DomitoValidateFileLegacyMode( + HANDLE FileHandle, + PVOID Hash, + UINT32 HashSize, + ALG_ID HashAlgId, + const IMAGE_DATA_DIRECTORY* SecurityDirectory, + MINCRYPT_POLICY_INFO* PolicyInfo, + LARGE_INTEGER* SigningTime, + MINCRYPT_POLICY_INFO* TimeStampPolicyInfo +) +{ + ULONG source; + return DomitoValidateFileLegacyModeInternal( + FileHandle, + Hash, + HashSize, + HashAlgId, + SecurityDirectory, + PolicyInfo, + SigningTime, + TimeStampPolicyInfo, + DOMITO_CI_LEGACY_CATALOG_POLICY_BITS, + FALSE, + &source + ); +} + +_Success_(return == STATUS_SUCCESS) +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +NTSTATUS +DomitoValidateFileLegacyModeEx( + HANDLE FileHandle, + PVOID Hash, + UINT32 HashSize, + ALG_ID HashAlgId, + const IMAGE_DATA_DIRECTORY* SecurityDirectory, + MINCRYPT_POLICY_INFO* PolicyInfo, + LARGE_INTEGER* SigningTime, + MINCRYPT_POLICY_INFO* TimeStampPolicyInfo, + UINT32 AcceptedCatalogPolicyBits, + BOOLEAN RequireTrustedCatalog, + PULONG ValidationSource +) +{ + return DomitoValidateFileLegacyModeInternal( + FileHandle, + Hash, + HashSize, + HashAlgId, + SecurityDirectory, + PolicyInfo, + SigningTime, + TimeStampPolicyInfo, + AcceptedCatalogPolicyBits, + RequireTrustedCatalog, + ValidationSource + ); +} + #pragma code_seg() diff --git a/src/Domito.Consumer.cpp b/src/Domito.Consumer.cpp new file mode 100644 index 0000000..c14404b --- /dev/null +++ b/src/Domito.Consumer.cpp @@ -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(G_Memory.Allocate(size)); + + if (!imageName) + { + status = STATUS_INSUFFICIENT_RESOURCES; + } + else + { + imageName->Buffer = reinterpret_cast(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(&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(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(Chain); + const ULONG_PTR blobAddress = reinterpret_cast(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(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(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(publisherRoutine); + status = getPublisher(const_cast(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 diff --git a/src/Domito.cpp b/src/Domito.cpp index 41c9dab..4753fdb 100644 --- a/src/Domito.cpp +++ b/src/Domito.cpp @@ -7,18 +7,26 @@ #include "Domito.Internal.h" - DOMITO_COMMON G_Common = {}; -DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QipRoutineName, L"ZwQueryInformationProcess"); -DECLARE_GLOBAL_CONST_UNICODE_STRING(G_IdetdRoutineName, L"RtlImageDirectoryEntryToData"); -DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QsiRoutineName, L"ZwQuerySystemInformation"); +DECLARE_GLOBAL_CONST_UNICODE_STRING( + G_QipRoutineName, + L"ZwQueryInformationProcess" +); +DECLARE_GLOBAL_CONST_UNICODE_STRING( + G_IdetdRoutineName, + L"RtlImageDirectoryEntryToData" +); +DECLARE_GLOBAL_CONST_UNICODE_STRING( + G_QsiRoutineName, + L"ZwQuerySystemInformation" +); #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 - _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) @@ -27,7 +35,7 @@ DomitoInit() { // // Do those first since the follow-up code depends on them - // + // G_Common.ZwQueryInformationProcess = (t_ZwQueryInformationProcess)MmGetSystemRoutineAddress((PUNICODE_STRING)&G_QipRoutineName); @@ -43,19 +51,27 @@ _IRQL_requires_max_(PASSIVE_LEVEL) void DomitoShutdown() { - } - _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS DomitoFindModuleBaseAddress( _In_ PANSI_STRING ModuleName, - _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; PSYSTEM_MODULE_INFORMATION moduleInfo = NULL; @@ -67,12 +83,7 @@ DomitoFindModuleBaseAddress( const ULONG SystemModuleInformation = 11; // Query the required buffer size for module information - NTSTATUS status = G_Common.ZwQuerySystemInformation( - SystemModuleInformation, - &bufferSize, - 0, - &bufferSize - ); + NTSTATUS status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, &bufferSize, 0, &bufferSize); if (status != STATUS_INFO_LENGTH_MISMATCH) { @@ -80,9 +91,7 @@ DomitoFindModuleBaseAddress( } // Allocate memory for the module information - moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate( - bufferSize - ); + moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate(bufferSize); if (moduleInfo == NULL) { @@ -90,12 +99,7 @@ DomitoFindModuleBaseAddress( } // Retrieve the module information - status = G_Common.ZwQuerySystemInformation( - SystemModuleInformation, - moduleInfo, - bufferSize, - NULL - ); + status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, moduleInfo, bufferSize, NULL); if (!NT_SUCCESS(status)) { @@ -120,6 +124,7 @@ DomitoFindModuleBaseAddress( { *ModuleBase = moduleInfo->Module[i].Base; } + break; } } @@ -129,6 +134,19 @@ DomitoFindModuleBaseAddress( 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) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) @@ -136,59 +154,146 @@ NTSTATUS DomitoFindExportedFunctionAddress( _In_ PVOID ModuleBase, _In_ PANSI_STRING FunctionName, - _Inout_opt_ PVOID * FunctionAddress + _Inout_opt_ PVOID* FunctionAddress ) { - NTSTATUS status = STATUS_NOT_FOUND; - ULONG exportSize; + if (FunctionAddress) + { + *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; } - // Retrieve the export directory information - const PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)G_Common.RtlImageDirectoryEntryToData( - ModuleBase, - TRUE, - IMAGE_DIRECTORY_ENTRY_EXPORT, - &exportSize - ); + // Initial headers belong to a valid loaded module, as required by this API. + const PUCHAR base = (PUCHAR)ModuleBase; + IMAGE_DOS_HEADER dos; + memcpy(&dos, base, sizeof(dos)); - if (exportDirectory == NULL) + if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < (LONG)sizeof(dos)) { 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); - const PULONG functionNames = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNames); - const PUSHORT functionOrdinals = (PUSHORT)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNameOrdinals); - - for (ULONG i = 0; i < exportDirectory->NumberOfNames; i++) + if (signature != IMAGE_NT_SIGNATURE || + (magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC && magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)) { - const char* functionName = (const char*)((ULONG_PTR)ModuleBase + functionNames[i]); - const USHORT functionOrdinal = functionOrdinals[i]; - UNREFERENCED_PARAMETER(functionOrdinal); - - const ULONG functionRva = functionAddresses[i]; - const PVOID functionAddress = (PVOID)((ULONG_PTR)ModuleBase + functionRva); - - RtlInitAnsiString(¤tFunctionName, functionName); - - if (0 == RtlCompareString(FunctionName, ¤tFunctionName, TRUE)) - { - if (FunctionAddress) - { - status = STATUS_SUCCESS; - *FunctionAddress = functionAddress; - } - break; - } + return STATUS_INVALID_IMAGE_FORMAT; } - return status; + // SizeOfImage is at the same offset in PE32 and PE32+ optional headers. + ULONG imageSize; + memcpy( + &imageSize, + nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage), + sizeof(imageSize) + ); + + if (!DomitoImageRvaSpan( + imageSize, + (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, ¤t, 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) + { + *FunctionAddress = base + functionRva; + } + + return STATUS_SUCCESS; + } + + return STATUS_NOT_FOUND; } _Success_(return == STATUS_SUCCESS) @@ -201,7 +306,7 @@ DomitoMemorySearchPattern( _In_ SIZE_T puLen, _In_ PVOID pcBase, _In_ SIZE_T puSize, - _Outptr_result_maybenull_ PVOID * ppMatch + _Outptr_result_maybenull_ PVOID* ppMatch ) { ASSERT(ppMatch != NULL && pcPattern != NULL && pcBase != NULL); @@ -221,6 +326,7 @@ DomitoMemorySearchPattern( for (SIZE_T i = 0; i + puLen <= puSize; i++) { BOOLEAN found = TRUE; + for (SIZE_T j = 0; j < puLen; j++) { if (pcPattern[j] != uWildcard && pcPattern[j] != ((PCUCHAR)pcBase)[i + j]) @@ -240,6 +346,74 @@ DomitoMemorySearchPattern( 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) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) @@ -250,33 +424,8 @@ DomitoReadFile( _In_ ULONG BufferSize ) { - NTSTATUS status = STATUS_SUCCESS; - IO_STATUS_BLOCK ioStatusBlock; - - // 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; + const LARGE_INTEGER offset = {}; + return DomitoReadFileAt(FileHandle, Buffer, BufferSize, &offset); } _Success_(return == STATUS_SUCCESS) @@ -286,95 +435,94 @@ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS DomitoGetProcessImageName( _In_ ULONG ProcessId, - _Inout_ PUNICODE_STRING * ProcessImageName + _Outptr_result_maybenull_ PUNICODE_STRING* ProcessImageName ) { - NTSTATUS status = STATUS_UNSUCCESSFUL; - ULONG returnedLength; - HANDLE hProcess = NULL; + PAGED_CODE(); - PAGED_CODE(); // this eliminates the possibility of the IDLE Thread/Process - - if (ProcessId == 0 || ProcessId == 4) - { - return STATUS_INVALID_PARAMETER_1; - } - - if (ProcessImageName == NULL) + if (!ProcessImageName) { return STATUS_INVALID_PARAMETER_2; } - CLIENT_ID cid; - cid.UniqueProcess = (HANDLE)ProcessId; - cid.UniqueThread = NULL; + *ProcessImageName = nullptr; - OBJECT_ATTRIBUTES objAttr; - InitializeObjectAttributes( - &objAttr, - NULL, - OBJ_KERNEL_HANDLE, - NULL, - NULL - ); + if (!ProcessId || ProcessId == 4) + { + return STATUS_INVALID_PARAMETER_1; + } - if (!NT_SUCCESS(status = ZwOpenProcess( - &hProcess, - PROCESS_ALL_ACCESS, - &objAttr, - &cid - ))) + if (!G_Common.ZwQueryInformationProcess) + { + return STATUS_NOT_IMPLEMENTED; + } + + 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; } - 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; - goto cleanUp; - } + if (size < sizeof(UNICODE_STRING)) + { + status = STATUS_INVALID_BUFFER_SIZE; + } + else + { + auto image = static_cast(G_Memory.Allocate(size)); - /* Query the actual size of the process path */ - status = G_Common.ZwQueryInformationProcess( - hProcess, - ProcessImageFileName, - NULL, // buffer - 0, // buffer size - &returnedLength - ); + if (!image) + { + status = STATUS_INSUFFICIENT_RESOURCES; + } + else + { + ULONG returned = 0; + status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, image, size, &returned); - if (STATUS_INFO_LENGTH_MISMATCH != status) - { - goto cleanUp; - } + if (NT_SUCCESS(status)) + { + const ULONG_PTR base = reinterpret_cast(image); + const ULONG_PTR buffer = reinterpret_cast(image->Buffer); - *ProcessImageName = (PUNICODE_STRING)G_Memory.Allocate(returnedLength); + 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 (*ProcessImageName == NULL) - { - status = STATUS_INSUFFICIENT_RESOURCES; - goto cleanUp; + if (NT_SUCCESS(status)) + { + *ProcessImageName = image; + } + else + { + G_Memory.Free(image); + } + } + } } - - /* 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); - } - -cleanUp: - - if (hProcess) + else if (NT_SUCCESS(status)) { - ZwClose(hProcess); + status = STATUS_INVALID_BUFFER_SIZE; } + ZwClose(process); return status; } #pragma code_seg() diff --git a/src/Domito.vcxproj b/src/Domito.vcxproj index 760de2e..173306a 100644 --- a/src/Domito.vcxproj +++ b/src/Domito.vcxproj @@ -121,12 +121,18 @@ + + + + + + diff --git a/src/Domito.vcxproj.filters b/src/Domito.vcxproj.filters index 1b7ac8a..bf45072 100644 --- a/src/Domito.vcxproj.filters +++ b/src/Domito.vcxproj.filters @@ -28,6 +28,15 @@ Source Files + + Source Files + + + Source Files + + + Source Files + Source Files @@ -36,9 +45,18 @@ Header Files + + Header Files + Header Files\Public + + Header Files\Public + + + Header Files\Public + Header Files\Public diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..29b3a13 --- /dev/null +++ b/tests/README.md @@ -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. diff --git a/tests/ci-file-object-probe-tests.cpp b/tests/ci-file-object-probe-tests.cpp new file mode 100644 index 0000000..d7924b4 --- /dev/null +++ b/tests/ci-file-object-probe-tests.cpp @@ -0,0 +1,175 @@ +// Host tests validate the probe plumbing, NOT CI trust behavior. +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS +#include +#include +#include +#include +#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); +} diff --git a/tests/ci-file-object-probe.cpp b/tests/ci-file-object-probe.cpp new file mode 100644 index 0000000..41e3b9d --- /dev/null +++ b/tests/ci-file-object-probe.cpp @@ -0,0 +1,72 @@ +// Compile into an existing signed test driver; this file has no DriverEntry. +#ifndef DOMITO_CI_PROBE_HOST_TEST +#include +#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, + ×tamp, + &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(×tamp); + } +} diff --git a/tests/ci-file-object-probe.h b/tests/ci-file-object-probe.h new file mode 100644 index 0000000..04e4f87 --- /dev/null +++ b/tests/ci-file-object-probe.h @@ -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 +); diff --git a/tests/ci-validation-tests.cpp b/tests/ci-validation-tests.cpp new file mode 100644 index 0000000..ff21972 --- /dev/null +++ b/tests/ci-validation-tests.cpp @@ -0,0 +1,519 @@ +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS +#include +#include +#include +#include +#include + +#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(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(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(static_cast(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(-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 + ); +} diff --git a/tests/common-tests.cpp b/tests/common-tests.cpp new file mode 100644 index 0000000..ade7b29 --- /dev/null +++ b/tests/common-tests.cpp @@ -0,0 +1,395 @@ +// Tests extract the unchanged production bodies. Kernel I/O is simulated. +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#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 +Image() +{ + std::vector 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& 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); +} diff --git a/tests/consumer-helper-tests.cpp b/tests/consumer-helper-tests.cpp new file mode 100644 index 0000000..4cdbb06 --- /dev/null +++ b/tests/consumer-helper-tests.cpp @@ -0,0 +1,967 @@ +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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(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(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(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(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(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(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(buffer)); + + if (failure == 7) + { + return STATUS_UNSUCCESSFUL; + } + + if (failure == 11) + { + static_cast(buffer)->Buffer = reinterpret_cast(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; + +static Bytes +Der( + UCHAR tag, + const Bytes& contents +) +{ + Bytes b = {tag}; + + if (contents.size() < 128) + { + b.push_back(static_cast(contents.size())); + } + else + { + Check(contents.size() < 256, "fixture DER bound"); + b.push_back(0x81); + b.push_back(static_cast(contents.size())); + } + + b.insert(b.end(), contents.begin(), contents.end()); + return b; +} + +static Bytes +Join( + std::initializer_list 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(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(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(cert.size()), DomitoCertificateHashSpkiDer, CALG_SHA_256, full, &a + )), + "full SPKI hash" + ); + Check( + NT_SUCCESS(DomitoHashCertificate( + cert.data(), + static_cast(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(cert.size()), DomitoCertificateHashDer, CALG_SHA_256, whole, &c + )), + "DER thumbprint" + ); + d = 64; + Check( + CryptHashCertificate2( + BCRYPT_SHA256_ALGORITHM, 0, nullptr, cert.data(), static_cast(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(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(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(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(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(L"\\foo")}; + + for (int fail : {0, 2, 3}) + { + failure = fail; + const auto r = refs, d = derefs, c = closed; + result.PolicyInfo.ChainInfo = static_cast(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(1); + NTSTATUS s = DomitoGetProcessImageNameEx(&processToken, &name); + Check(NT_SUCCESS(s) == (fail == 0), "PEPROCESS lookup failure"); + + if (NT_SUCCESS(s)) + { + Check( + name->Buffer == reinterpret_cast(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(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(&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(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(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(storage); + chain->Size = sizeof(storage); + chain->ChainElements = reinterpret_cast(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(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); +} diff --git a/tests/digest-tests.cpp b/tests/digest-tests.cpp new file mode 100644 index 0000000..b425464 --- /dev/null +++ b/tests/digest-tests.cpp @@ -0,0 +1,1170 @@ +// User-mode regression harness. The runner extracts the production functions +// unchanged; pool, file-I/O, and CI calls are test doubles. BCrypt is real CNG. +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0) +#define ASSERT(x) Check(!!(x), #x) +#define DOMITO_POOL_TAG 0x446f6d69 +#define NonPagedPoolNx 0 +#define PagedPool 1 +#define PAGED_CODE() ((void)0) +#define MINCRYPT_SHA1_LENGTH 20 +#define MINCRYPT_SHA256_LENGTH 32 +#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL +#define DOMITO_CI_SOURCE_EMBEDDED 1UL +#define DOMITO_CI_SOURCE_CATALOG 2UL +typedef const UCHAR* PCUCHAR; + +static unsigned checks; +static unsigned allocations; +static unsigned ciCalls; +static unsigned hashCalls; +static int failOperation; +static int failHashCall; +static ULONG_PTR bytesRead; +static bool allocatePolicies; +static NTSTATUS embeddedResult = STATUS_SUCCESS; +static NTSTATUS catalogResults[2] = {STATUS_SUCCESS, STATUS_SUCCESS}; +static unsigned catalogCalls; +static ULONG expectedCatalogMask = DOMITO_CI_LEGACY_CATALOG_POLICY_BITS; +static BOOLEAN expectedTrustedMarker; +static LONGLONG observedFileSize = 1056; + +static void +Check( + bool ok, + const char* message +) +{ + ++checks; + + if (!ok) + { + std::fprintf(stderr, "FAIL: %s\n", message); + std::exit(1); + } +} + +static PVOID +ExAllocatePoolWithTag( + int, + SIZE_T size, + ULONG +) +{ + if (failOperation == 4) + { + return nullptr; + } + + void* p = HeapAlloc(GetProcessHeap(), 0, size); + + if (p) + { + ++allocations; + } + + return p; +} + +static void +ExFreePoolWithTag( + PVOID p, + ULONG +) +{ + Check(p != nullptr && allocations != 0, "allocation ownership"); + --allocations; + Check(HeapFree(GetProcessHeap(), 0, p) != FALSE, "heap free"); +} + +static NTSTATUS +TestCreateHash( + BCRYPT_ALG_HANDLE a, + BCRYPT_HASH_HANDLE* h, + PUCHAR o, + ULONG os, + PUCHAR s, + ULONG ss, + ULONG f +) +{ + if (failOperation == 1) + { + return STATUS_UNSUCCESSFUL; + } + + return BCryptCreateHash(a, h, o, os, s, ss, f); +} + +static NTSTATUS +TestHashData( + BCRYPT_HASH_HANDLE h, + PUCHAR b, + ULONG n, + ULONG f +) +{ + ++hashCalls; + + if (failOperation == 2 && hashCalls == (unsigned)failHashCall) + { + return STATUS_UNSUCCESSFUL; + } + + return BCryptHashData(h, b, n, f); +} + +static NTSTATUS +TestFinishHash( + BCRYPT_HASH_HANDLE h, + PUCHAR b, + ULONG n, + ULONG f +) +{ + if (failOperation == 3) + { + return STATUS_UNSUCCESSFUL; + } + + return BCryptFinishHash(h, b, n, f); +} + +// Minimal dependencies for the legacy wrapper; no actual CI code is invoked. +struct MINCRYPT_POLICY_INFO +{ + ULONG Size; + PVOID ChainInfo; +}; + +struct KAPC_STATE +{ + ULONG unused; +}; + +struct IO_STATUS_BLOCK +{ + NTSTATUS Status; + ULONG_PTR Information; +}; + +static PVOID PsInitialSystemProcess = nullptr; + +static void +CiFreePolicyInfo( + MINCRYPT_POLICY_INFO* p +) +{ + if (p->ChainInfo) + { + ExFreePoolWithTag(p->ChainInfo, DOMITO_POOL_TAG); + } + + *p = {}; +} + +static void +PopulatePolicies( + MINCRYPT_POLICY_INFO* policy, + MINCRYPT_POLICY_INFO* timestamp +) +{ + Check(!policy->ChainInfo && !timestamp->ChainInfo, "previous CI attempt released before reuse"); + + if (allocatePolicies) + { + policy->Size = timestamp->Size = sizeof(*policy); + policy->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG); + timestamp->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG); + } +} + +static void +KeStackAttachProcess( + PVOID, + KAPC_STATE* +) +{ +} + +static void +KeUnstackDetachProcess( + KAPC_STATE* +) +{ +} + +typedef PVOID PFILE_OBJECT; +static PVOID fileObjectType; +static PVOID* IoFileObjectType = &fileObjectType; +#define KernelMode 0 + +static NTSTATUS +ObReferenceObjectByHandle( + HANDLE, + ULONG, + PVOID, + int, + PVOID* object, + PVOID +) +{ + *object = &fileObjectType; + return STATUS_SUCCESS; +} + +static void +ObDereferenceObject( + PVOID +) +{ +} + +static NTSTATUS +FsRtlGetFileSize( + PFILE_OBJECT, + LARGE_INTEGER* size +) +{ + size->QuadPart = observedFileSize; + return STATUS_SUCCESS; +} + +static NTSTATUS +DomitoReadFileAt( + HANDLE, + PVOID buffer, + ULONG size, + const LARGE_INTEGER* offset +) +{ + Check(offset->QuadPart == 1024, "explicit certificate read offset"); + std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size)); + return bytesRead == size ? STATUS_SUCCESS : STATUS_END_OF_FILE; +} + +static NTSTATUS +CiCheckSignedFile( + PVOID, + UINT32, + ALG_ID, + PVOID, + UINT32, + MINCRYPT_POLICY_INFO* policy, + LARGE_INTEGER* time, + MINCRYPT_POLICY_INFO* timestamp +) +{ + ++ciCalls; + Check(time->QuadPart == 0, "fresh signing time for embedded validation"); + PopulatePolicies(policy, timestamp); + time->QuadPart = 123; + return embeddedResult; +} + +static NTSTATUS +CiVerifyHashInCatalog( + PVOID, + UINT32, + ALG_ID, + BOOLEAN reload, + UINT32 trusted, + UINT32 mask, + MINCRYPT_POLICY_INFO* policy, + PVOID, + LARGE_INTEGER* time, + MINCRYPT_POLICY_INFO* timestamp +) +{ + ++ciCalls; + Check(mask == expectedCatalogMask && trusted == expectedTrustedMarker, "explicit catalog policy forwarded"); + Check(reload == (catalogCalls != 0), "catalog reload sequence"); + Check(time->QuadPart == 0, "fresh signing time for catalog attempt"); + PopulatePolicies(policy, timestamp); + time->QuadPart = 456; + Check(catalogCalls < 2, "bounded catalog retries"); + return catalogResults[catalogCalls++]; +} + +#define BCryptCreateHash TestCreateHash +#define BCryptHashData TestHashData +#define BCryptFinishHash TestFinishHash +#include "digest-under-test.inc" +#undef BCryptCreateHash +#undef BCryptHashData +#undef BCryptFinishHash + +static std::vector +Image( + bool pe64, + ULONG nt = 128, + bool certificate = false +) +{ + std::vector b((size_t)nt + 896 + (certificate ? 32 : 0), 0); + IMAGE_DOS_HEADER dos = {}; + dos.e_magic = IMAGE_DOS_SIGNATURE; + dos.e_lfanew = (LONG)nt; + std::memcpy(b.data(), &dos, sizeof(dos)); + ULONG signature = IMAGE_NT_SIGNATURE; + std::memcpy(b.data() + nt, &signature, sizeof(signature)); + IMAGE_FILE_HEADER f = {}; + f.NumberOfSections = 1; + f.Machine = pe64 ? IMAGE_FILE_MACHINE_AMD64 : IMAGE_FILE_MACHINE_I386; + f.SizeOfOptionalHeader = pe64 ? sizeof(IMAGE_OPTIONAL_HEADER64) : sizeof(IMAGE_OPTIONAL_HEADER32); + std::memcpy(b.data() + nt + 4, &f, sizeof(f)); + IMAGE_OPTIONAL_HEADER64 h64 = {}; + IMAGE_OPTIONAL_HEADER32 h32 = {}; + h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; + h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC; + h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16; + h64.SectionAlignment = h32.SectionAlignment = 4096; + h64.FileAlignment = h32.FileAlignment = 512; + h64.SizeOfImage = h32.SizeOfImage = 8192; + h64.Subsystem = h32.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI; + h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384; + + if (certificate) + { + h64.DataDirectory[4] = h32.DataDirectory[4] = {nt + 896, 32}; + ULONG length = 32; + std::memcpy(b.data() + nt + 896, &length, sizeof(length)); + USHORT revision = 0x0200; + USHORT type = WIN_CERT_TYPE_PKCS_SIGNED_DATA; + std::memcpy(b.data() + nt + 900, &revision, 2); + std::memcpy(b.data() + nt + 902, &type, 2); + } + + std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader); + IMAGE_SECTION_HEADER section = {}; + section.VirtualAddress = 4096; + section.Misc.VirtualSize = 512; + section.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ; + section.PointerToRawData = nt + 384; + section.SizeOfRawData = 512; + std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, §ion, sizeof(section)); + + for (ULONG i = nt + 384; i < nt + 896; ++i) + { + b[i] = (UCHAR)(i * 31); + } + + return b; +} + +static void +Set32( + std::vector& b, + size_t at, + ULONG value +) +{ + std::memcpy(b.data() + at, &value, sizeof(value)); +} + +static void +Set16( + std::vector& b, + size_t at, + USHORT value +) +{ + std::memcpy(b.data() + at, &value, sizeof(value)); +} + +static std::vector +Digest( + PUCHAR b, + ULONG size, + NTSTATUS expected = STATUS_SUCCESS +) +{ + UINT32 algorithm = 0xdead; + ULONG length = 0xdead; + ULONG certSize = 0xdead; + PVOID digest = (void*)1; + LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1; + hashCalls = 0; + NTSTATUS s = DomitoCalculatePortableExecutableDigest(b, size, &algorithm, &length, &digest, &cert, &certSize); + Check(s == expected, "digest status"); + std::vector result; + + if (NT_SUCCESS(s)) + { + Check(algorithm == CALG_SHA1 && length == 20 && digest != nullptr, "SHA1 output"); + Check((cert == nullptr) == (certSize == 0), "certificate outputs agree"); + + if (cert) + { + Check((PUCHAR)cert >= b && (PUCHAR)cert + certSize <= b + size, "borrowed certificate span"); + } + + result.assign((PUCHAR)digest, (PUCHAR)digest + length); + DomitoFreePortableExecutableDigest(digest); + } + else + { + Check(!algorithm && !length && !digest && !cert && !certSize, "failure outputs cleared"); + } + + Check(allocations == 0, "no leaked pool allocations"); + return result; +} + +static std::vector +Reference( + const std::vector& b, + ULONG checksum, + ULONG securitySlot, + ULONG end +) +{ + // Explicit fixture ranges, independent of the production parser. + std::vector bytes(b.begin(), b.begin() + checksum); + bytes.insert(bytes.end(), b.begin() + checksum + 4, b.begin() + securitySlot); + + if (securitySlot != end) + { + bytes.insert(bytes.end(), b.begin() + securitySlot + 8, b.begin() + end); + } + + std::vector digest(20); + BCRYPT_ALG_HANDLE algorithm = nullptr; + Check( + NT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA1_ALGORITHM, nullptr, 0)), "reference algorithm" + ); + Check( + NT_SUCCESS(BCryptHash(algorithm, nullptr, 0, bytes.data(), (ULONG)bytes.size(), digest.data(), 20)), + "reference digest" + ); + BCryptCloseAlgorithmProvider(algorithm, 0); + return digest; +} + +static void +GuardedInvalid( + const std::vector& b, + NTSTATUS expected +) +{ + SYSTEM_INFO si; + GetSystemInfo(&si); + const SIZE_T span = ((b.size() + si.dwPageSize - 1) / si.dwPageSize) * si.dwPageSize; + UCHAR* region = (UCHAR*)VirtualAlloc(nullptr, span + si.dwPageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + Check(region != nullptr, "guard allocation"); + DWORD old; + Check(VirtualProtect(region + span, si.dwPageSize, PAGE_NOACCESS, &old) != FALSE, "guard protection"); + UCHAR* input = region + span - b.size(); + std::memcpy(input, b.data(), b.size()); + Digest(input, (ULONG)b.size(), expected); + ULONG strictSize = 0; + ULONG securitySize = 0; + PVOID strictDigest = nullptr; + LPWIN_CERTIFICATE certificate = nullptr; + Check( + !NT_SUCCESS(DomitoCalculatePortableExecutableDigestEx( + input, (ULONG)b.size(), CALG_SHA256, &strictSize, &strictDigest, &certificate, &securitySize + )), + "strict guard-page malformed input rejected" + ); + Check(!strictSize && !strictDigest && !certificate && !securitySize, "strict malformed outputs cleared"); + VirtualFree(region, 0, MEM_RELEASE); +} + +static std::vector +ExplicitDigest( + const std::vector& b, + UINT32 calg, + NTSTATUS expected = STATUS_SUCCESS +) +{ + ULONG length = 0xdead; + ULONG certSize = 0xdead; + PVOID digest = (PVOID)1; + LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1; + NTSTATUS status = + DomitoCalculatePortableExecutableDigestEx(b.data(), (ULONG)b.size(), calg, &length, &digest, &cert, &certSize); + Check(status == expected, "explicit digest status"); + std::vector result; + + if (NT_SUCCESS(status)) + { + Check( + length == (calg == CALG_SHA1 ? 20u + : calg == CALG_SHA256 ? 32u + : calg == CALG_SHA384 ? 48u + : 64u), + "explicit algorithm digest length" + ); + result.assign((UCHAR*)digest, (UCHAR*)digest + length); + DomitoFreePortableExecutableDigest(digest); + } + else + { + Check(!length && !digest && !cert && !certSize, "explicit failure outputs cleared"); + } + + Check(allocations == 0, "explicit digest allocation cleanup"); + return result; +} + +static std::vector +SipDigest( + const std::vector& b, + UINT32 calg +) +{ + WCHAR tempPath[MAX_PATH]; + WCHAR tempName[MAX_PATH]; + Check(GetTempPathW(MAX_PATH, tempPath) != 0, "temp path"); + Check(GetTempFileNameW(tempPath, L"dci", 0, tempName) != 0, "temp name"); + HANDLE file = CreateFileW( + tempName, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, + nullptr + ); + Check(file != INVALID_HANDLE_VALUE, "SIP fixture file"); + DWORD written = 0; + Check(WriteFile(file, b.data(), (DWORD)b.size(), &written, nullptr) && written == b.size(), "write SIP fixture"); + GUID subject; + Check(CryptSIPRetrieveSubjectGuid(tempName, file, &subject) != FALSE, "PE SIP subject"); + SIP_SUBJECTINFO info = {}; + info.cbSize = sizeof(info); + info.pgSubjectType = &subject; + info.hFile = file; + info.pwsFileName = tempName; + info.dwEncodingType = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + info.DigestAlgorithm.pszObjId = const_cast( + calg == CALG_SHA1 ? szOID_OIWSEC_sha1 + : calg == CALG_SHA256 ? szOID_NIST_sha256 + : calg == CALG_SHA384 ? szOID_NIST_sha384 + : szOID_NIST_sha512 + ); + DWORD size = 0; + Check(CryptSIPCreateIndirectData(&info, &size, nullptr) != FALSE, "SIP size query"); + std::vector buffer(size); + auto indirect = (SIP_INDIRECT_DATA*)buffer.data(); + Check(CryptSIPCreateIndirectData(&info, &size, indirect) != FALSE, "Windows SIP digest"); + std::vector result(indirect->Digest.pbData, indirect->Digest.pbData + indirect->Digest.cbData); + CloseHandle(file); + return result; +} + +static std::vector +Der( + UCHAR tag, + const std::vector& value +) +{ + std::vector out = {tag}; + + if (value.size() < 128) + { + out.push_back((UCHAR)value.size()); + } + else if (value.size() < 256) + { + out.push_back(0x81); + out.push_back((UCHAR)value.size()); + } + else + { + out.push_back(0x82); + out.push_back((UCHAR)(value.size() >> 8)); + out.push_back((UCHAR)value.size()); + } + + out.insert(out.end(), value.begin(), value.end()); + return out; +} + +static std::vector +Join( + std::initializer_list> values +) +{ + std::vector out; + + for (auto& value : values) + { + out.insert(out.end(), value.begin(), value.end()); + } + + return out; +} + +static std::vector +SignedContent( + UINT32 calg, + bool nullParameters = true +) +{ + std::vector oid = calg == CALG_SHA1 ? std::vector{0x2B, 0x0E, 0x03, 0x02, 0x1A} + : std::vector{ + 0x60, + 0x86, + 0x48, + 0x01, + 0x65, + 0x03, + 0x04, + 0x02, + (UCHAR)(calg == CALG_SHA256 ? 1 + : calg == CALG_SHA384 ? 2 + : 3) + }; + auto algorithm = Der(0x30, Join({Der(0x06, oid), nullParameters ? Der(0x05, {}) : std::vector{}})); + auto digest = + Der(0x30, + Join( + {algorithm, + Der(0x04, + std::vector( + calg == CALG_SHA1 ? 20 + : calg == CALG_SHA256 ? 32 + : calg == CALG_SHA384 ? 48 + : 64, + 0xAA + ))} + )); + // A complete SHA-1 DigestInfo decoy in the ignored data attribute must not + // override the selected SpcIndirectDataContent digest AlgorithmIdentifier. + auto data = + Der(0x30, Der(0x04, {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x05, 0, 0x04, 0x14})); + auto indirect = Der(0x30, Join({data, digest})); + auto content = + Der(0x30, Join({Der(0x06, {0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x04}), Der(0xA0, indirect)})); + auto signedData = Der(0x30, Join({Der(0x02, {1}), Der(0x31, algorithm), content, Der(0x31, {})})); + return Der(0x30, Join({Der(0x06, {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02}), Der(0xA0, signedData)})); +} + +static void +ExtendedTests() +{ + for (bool pe64 : {false, true}) + { + for (bool cert : {false, true}) + { + for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512}) + { + auto b = Image(pe64, 128, cert); + Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP differential fixture"); + + if (!cert) + { + b.insert(b.end(), 48, 0x42); // Non-section overlay. + Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP overlay differential"); + } + } + } + } + + for (bool pe64 : {false, true}) + { + for (ULONG count : {0UL, 4UL}) + { + auto absent = Image(pe64); + Set32(absent, pe64 ? 260 : 244, count); + ExplicitDigest(absent, CALG_SHA256, STATUS_NOT_SUPPORTED); + } + } + + auto b = Image(true); + // Two contiguous sections whose headers are in reverse raw-offset order. + Set16(b, 134, 2); + IMAGE_SECTION_HEADER first = {}; + IMAGE_SECTION_HEADER second = {}; + first.PointerToRawData = 768; + first.SizeOfRawData = 256; + second.PointerToRawData = 512; + second.SizeOfRawData = 256; + std::memcpy(b.data() + 392, &first, sizeof(first)); + std::memcpy(b.data() + 432, &second, sizeof(second)); + Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "section header order differential"); + Set32(b, 392 + 20, 800); + ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT); // extends beyond EOF + Set32(b, 392 + 20, 640); + ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // overlap + b = Image(true); + Set32(b, 392 + 20, 528); + Set32(b, 392 + 16, 496); + ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // gap + b = Image(true, 128, true); + b.insert(b.end(), 8, 0); + ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // nonterminal certificate + b = Image(true); + Set32(b, 392 + 20, 0xFFFFFFFF); + ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT); + ExplicitDigest(Image(true), 0x1234, STATUS_NOT_SUPPORTED); + b = Image(true, 128, true); + b.resize(1088); + Set32(b, 300, 64); + Set32(b, 1056, 32); + Set16(b, 1060, 0x0200); + Set16(b, 1062, 2); + Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "multiple certificate entries excluded"); + Set32(b, 1056, 0); + ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT); + b = Image(true, 128, true); + Set32(b, 1024, 31); // Last entry's alignment padding is outside dwLength. + Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "certificate alignment padding"); + + for (int failure : {1, 2, 3, 4}) + { + failOperation = failure; + failHashCall = 1; + hashCalls = 0; + ExplicitDigest(Image(true), CALG_SHA256, failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL); + } + + failOperation = 0; + + for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512}) + { + auto content = SignedContent(calg); + auto signedImage = Image(true); + ULONG certLength = 8 + (ULONG)content.size(); + ULONG tableSize = (certLength + 7) & ~7u; + signedImage.resize(1024 + tableSize); + Set32(signedImage, 296, 1024); + Set32(signedImage, 300, tableSize); + Set32(signedImage, 1024, certLength); + Set16(signedImage, 1028, 0x0200); + Set16(signedImage, 1030, 2); + std::memcpy(signedImage.data() + 1032, content.data(), content.size()); + IMAGE_DATA_DIRECTORY directories[5] = {}; + directories[4] = {1024, tableSize}; + UINT32 actual = 0; + Check( + DomitoGetPortableExecutableDigestKindEx( + signedImage.data(), (ULONG)signedImage.size(), directories, &actual + ) == STATUS_SUCCESS && + actual == calg, + "bounded SPC digest algorithm, decoy ignored" + ); + + for (ULONG cut = 0; cut < content.size(); ++cut) + { + Set32(signedImage, 1024, cut + 8); + actual = 0xDEAD; + Check( + !NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx( + signedImage.data(), (ULONG)signedImage.size(), directories, &actual + )) && + actual == 0, + "truncated DER rejected" + ); + } + + Set32(signedImage, 1024, tableSize); + Check( + DomitoGetPortableExecutableDigestKindEx( + signedImage.data(), (ULONG)signedImage.size(), directories, &actual + ) == STATUS_SUCCESS && + actual == calg, + "bounded zero padding inside dwLength" + ); + + if (tableSize > certLength) + { + signedImage.back() = 0x7F; + Check( + DomitoGetPortableExecutableDigestKindEx( + signedImage.data(), (ULONG)signedImage.size(), directories, &actual + ) == STATUS_INVALID_IMAGE_FORMAT && + actual == 0, + "nonzero DER trailing bytes rejected" + ); + signedImage.back() = 0; + } + + Set32(signedImage, 1024, certLength); + signedImage[1033] = 0x80; // Indefinite length forbidden in this DER subset. + Check( + DomitoGetPortableExecutableDigestKindEx( + signedImage.data(), (ULONG)signedImage.size(), directories, &actual + ) == STATUS_INVALID_IMAGE_FORMAT, + "indefinite DER rejected" + ); + } + + for (bool nullParameters : {false, true}) + { + auto content = SignedContent(CALG_SHA256, nullParameters); + auto pe = Image(true); + const ULONG length = 8 + (ULONG)content.size(); + const ULONG tableSize = (length + 7) & ~7u; + pe.resize(1024 + tableSize); + Set32(pe, 1024, length); + Set16(pe, 1028, 0x0200); + Set16(pe, 1030, 2); + std::memcpy(pe.data() + 1032, content.data(), content.size()); + IMAGE_DATA_DIRECTORY dirs[5] = {}; + dirs[4] = {1024, tableSize}; + UINT32 algorithm = 0; + Check( + DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) == STATUS_SUCCESS && + algorithm == CALG_SHA256, + "AlgorithmIdentifier NULL optional" + ); + // Change both copies of the SHA-256 OID to an unsupported family member. + const UCHAR oid[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01}; + + for (size_t i = 1032; i + sizeof(oid) <= pe.size(); ++i) + { + if (!std::memcmp(pe.data() + i, oid, sizeof(oid))) + { + pe[i + sizeof(oid) - 1] = 0x7F; + } + } + + Check( + DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) == + STATUS_NOT_SUPPORTED && + !algorithm, + "unsupported SPC algorithm does not default to SHA1" + ); + } + + IMAGE_DATA_DIRECTORY dirs[5] = {}; + UINT32 actual = 123; + Check( + DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual) == STATUS_NOT_FOUND && + actual == 0, + "absent DER does not silently select SHA1" + ); +} + +static void +SystemImageTests() +{ + WCHAR system[MAX_PATH]; + Check(GetSystemDirectoryW(system, MAX_PATH) != 0, "system directory"); + + for (const WCHAR* name : {L"ci.dll", L"ntdll.dll", L"kernel32.dll", L"notepad.exe"}) + { + std::wstring path = std::wstring(system) + L"\\" + name; + HANDLE file = CreateFileW( + path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, 0, nullptr + ); + Check(file != INVALID_HANDLE_VALUE, "system PE corpus file"); + LARGE_INTEGER size; + Check( + GetFileSizeEx(file, &size) && size.QuadPart > 0 && size.QuadPart < 32 * 1024 * 1024, + "bounded system PE size" + ); + std::vector b((size_t)size.QuadPart); + DWORD read; + Check(ReadFile(file, b.data(), (DWORD)b.size(), &read, nullptr) && read == b.size(), "read system PE fixture"); + CloseHandle(file); + + for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512}) + { + Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "real system PE Windows SIP differential"); + } + + IMAGE_DOS_HEADER dos; + std::memcpy(&dos, b.data(), sizeof(dos)); + USHORT magic; + const ULONG optional = (ULONG)dos.e_lfanew + 24; + std::memcpy(&magic, b.data() + optional, 2); + const ULONG directory = magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC + ? FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory) + : FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory); + IMAGE_DATA_DIRECTORY dirs[5]; + std::memcpy(dirs, b.data() + optional + directory, sizeof(dirs)); + + if (dirs[4].Size) + { + UINT32 actual; + Check( + NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual)), + "real embedded SPC digest algorithm" + ); + // Independent Windows ASN.1 decoder: extract signed content with + // CryptQueryObject/CryptMsg, then decode SPC_INDIRECT_DATA_OBJID. + ULONG certLength; + std::memcpy(&certLength, b.data() + dirs[4].VirtualAddress, 4); + CRYPT_DATA_BLOB blob = {certLength - 8, b.data() + dirs[4].VirtualAddress + 8}; + HCRYPTMSG message = nullptr; + Check( + CryptQueryObject( + CERT_QUERY_OBJECT_BLOB, + &blob, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED, + CERT_QUERY_FORMAT_FLAG_BINARY, + 0, + nullptr, + nullptr, + nullptr, + nullptr, + &message, + nullptr + ) != FALSE, + "Windows decode PKCS7" + ); + DWORD contentSize = 0; + Check( + CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, nullptr, &contentSize) != FALSE, + "Windows SPC content size" + ); + std::vector content(contentSize); + Check( + CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, content.data(), &contentSize) != FALSE, + "Windows SPC content" + ); + SIP_INDIRECT_DATA* indirect = nullptr; + DWORD decodedSize = 0; + Check( + CryptDecodeObjectEx( + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + SPC_INDIRECT_DATA_OBJID, + content.data(), + contentSize, + CRYPT_DECODE_ALLOC_FLAG, + nullptr, + &indirect, + &decodedSize + ) != FALSE, + "Windows decode indirect data" + ); + const char* oid = indirect->DigestAlgorithm.pszObjId; + const UINT32 expected = !std::strcmp(oid, szOID_OIWSEC_sha1) ? CALG_SHA1 + : !std::strcmp(oid, szOID_NIST_sha256) ? CALG_SHA256 + : !std::strcmp(oid, szOID_NIST_sha384) ? CALG_SHA384 + : CALG_SHA512; + Check(actual == expected, "SPC digest algorithm matches Windows ASN1 decoder"); + auto digest = ExplicitDigest(b, actual); + Check( + digest.size() == indirect->Digest.cbData && + !std::memcmp(digest.data(), indirect->Digest.pbData, digest.size()), + "computed digest matches real embedded SPC digest" + ); + LocalFree(indirect); + CryptMsgClose(message); + } + } +} + +int +main() +{ + SystemImageTests(); + ExtendedTests(); + + for (bool pe64 : {false, true}) + { + for (bool certificate : {false, true}) + { + auto b = Image(pe64, 128, certificate); + auto result = Digest(b.data(), (ULONG)b.size()); + Check( + result == Reference(b, 216, pe64 ? 296 : 280, 1024), "fixture digest equals independent CNG reference" + ); + Set32(b, 216, 0x12345678); + Check(result == Digest(b.data(), (ULONG)b.size()), "checksum excluded"); + + if (certificate) + { + b.back() ^= 0x55; + Check(result == Digest(b.data(), (ULONG)b.size()), "certificate contents excluded"); + } + } + } + + auto large = Image(true, 4 * 1024 * 1024); + Check( + Digest(large.data(), (ULONG)large.size()) == Reference(large, 4194392, 4194472, (ULONG)large.size()), + "large DOS stub" + ); + auto b = Image(true); + + for (ULONG size = 0; size < 392; ++size) + { + std::vector prefix(b.begin(), b.begin() + size); + + if (size == 0) + { + Digest(b.data(), 0, STATUS_INVALID_IMAGE_NOT_MZ); + } + else + { + GuardedInvalid(prefix, size < 64 ? STATUS_INVALID_IMAGE_NOT_MZ : STATUS_INVALID_IMAGE_FORMAT); + } + } + + auto bad = b; + Set32(bad, 60, 0xffffffff); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 60, 0x7fffffff); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set16(bad, 148, 1); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set16(bad, 152, 0x999); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 260, 17); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + + for (ULONG offset : {200UL, 2048UL, 0xffffffffUL}) + { + bad = b; + Set32(bad, 296, offset); + Set32(bad, 300, 32); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + } + + bad = b; + Set32(bad, 300, 32); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 296, 800); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 296, 800); + Set32(bad, 300, 0xffffffff); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = Image(true, 128, true); + Set32(bad, 1024, 33); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = Image(true, 128, true); + Set32(bad, 1024, 7); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = Image(true); + Set32(bad, 260, 0); // No directory entry: don't exclude its physical bytes. + Check(Digest(bad.data(), (ULONG)bad.size()) == Reference(bad, 216, 1024, 1024), "absent security directory"); + + for (int failure : {1, 2, 3, 4}) + { + failOperation = failure; + + for (int call = 1; call <= (failure == 2 ? 3 : 1); ++call) + { + failHashCall = call; + Digest(b.data(), (ULONG)b.size(), failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL); + } + } + + failOperation = 0; + Digest(nullptr, 1024, STATUS_INVALID_PARAMETER); + IMAGE_DATA_DIRECTORY directory = {1024, 32}; + MINCRYPT_POLICY_INFO policy = {}; + MINCRYPT_POLICY_INFO timestamp = {}; + LARGE_INTEGER signing = {}; + UCHAR hash[20] = {}; + bytesRead = 16; + ciCalls = 0; + Check( + DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp) == + STATUS_END_OF_FILE, + "short certificate read rejected" + ); + Check(ciCalls == 0 && allocations == 0, "short read does not reach CI or leak"); + bytesRead = 32; + Check( + NT_SUCCESS( + DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp) + ), + "complete certificate read accepted" + ); + Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once"); + // Failure paths must release previous attempts before embedded/catalog/reload reuse. + allocatePolicies = true; + embeddedResult = STATUS_INVALID_IMAGE_HASH; + catalogResults[0] = STATUS_INVALID_IMAGE_HASH; + catalogResults[1] = STATUS_SUCCESS; + catalogCalls = 0; + expectedCatalogMask = 0x8000; + expectedTrustedMarker = TRUE; + ULONG source = 0xDEAD; + Check( + NT_SUCCESS(DomitoValidateFileLegacyModeEx( + (HANDLE)1, + hash, + 20, + CALG_SHA1, + &directory, + &policy, + &signing, + ×tamp, + expectedCatalogMask, + TRUE, + &source + )), + "explicit catalog fallback" + ); + Check( + source == DOMITO_CI_SOURCE_CATALOG && catalogCalls == 2 && allocations == 2, + "final policy ownership and catalog source" + ); + CiFreePolicyInfo(&policy); + CiFreePolicyInfo(×tamp); + Check(allocations == 0, "final results freed once"); + source = 123; + Check( + DomitoValidateFileLegacyModeEx( + (HANDLE)1, hash, 20, CALG_SHA256, &directory, &policy, &signing, ×tamp, 0, FALSE, &source + ) == STATUS_INVALID_PARAMETER && + !source, + "algorithm and length mismatch rejected" + ); + Check( + DomitoValidateFileLegacyModeEx( + (HANDLE)1, hash, 20, CALG_SHA384, &directory, &policy, &signing, ×tamp, 0, FALSE, &source + ) == STATUS_NOT_SUPPORTED, + "legacy catalog algorithm capability explicit" + ); + observedFileSize = 1040; + Check( + DomitoValidateFileLegacyModeEx( + (HANDLE)1, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp, 0, FALSE, &source + ) == STATUS_INVALID_IMAGE_FORMAT, + "certificate file range checked before allocation" + ); + Check(allocations == 0, "invalid file range never allocated"); + observedFileSize = 1056; + expectedCatalogMask = 0x8000; + expectedTrustedMarker = TRUE; + catalogCalls = 0; + catalogResults[1] = STATUS_INVALID_IMAGE_HASH; + Check( + DomitoValidateFileLegacyModeEx( + (HANDLE)1, + hash, + 20, + CALG_SHA1, + &directory, + &policy, + &signing, + ×tamp, + expectedCatalogMask, + TRUE, + &source + ) == STATUS_INVALID_IMAGE_HASH, + "final catalog failure retained" + ); + Check(!source && allocations == 2, "final failed-call policies remain caller-owned"); + CiFreePolicyInfo(&policy); + CiFreePolicyInfo(×tamp); + Check(allocations == 0, "failed final policies freed once"); + std::printf("PASS: %u digest/legacy checks (Windows SIP, Crypt32, guard pages, failures, ownership).\n", checks); +} diff --git a/tests/mincrypt-layout-tests.cpp b/tests/mincrypt-layout-tests.cpp new file mode 100644 index 0000000..2c674da --- /dev/null +++ b/tests/mincrypt-layout-tests.cpp @@ -0,0 +1,55 @@ +// Compile with the WDK for supported 64-bit targets. No driver is linked/loaded. +#include +#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 diff --git a/tests/run-ci-probe-tests.py b/tests/run-ci-probe-tests.py new file mode 100644 index 0000000..00f0861 --- /dev/null +++ b/tests/run-ci-probe-tests.py @@ -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) diff --git a/tests/run-ci-validation-tests.py b/tests/run-ci-validation-tests.py new file mode 100644 index 0000000..f535eb7 --- /dev/null +++ b/tests/run-ci-validation-tests.py @@ -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) diff --git a/tests/run-common-tests.py b/tests/run-common-tests.py new file mode 100644 index 0000000..cce1bf7 --- /dev/null +++ b/tests/run-common-tests.py @@ -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) diff --git a/tests/run-consumer-helper-tests.py b/tests/run-consumer-helper-tests.py new file mode 100644 index 0000000..1c0952c --- /dev/null +++ b/tests/run-consumer-helper-tests.py @@ -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 ") : mincrypt.index("#include ") + + len("#include ") +] +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) diff --git a/tests/run-deprecation-tests.py b/tests/run-deprecation-tests.py new file mode 100644 index 0000000..7e9e620 --- /dev/null +++ b/tests/run-deprecation-tests.py @@ -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 \n#include \n#include \n#include \n#include \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") diff --git a/tests/run-digest-tests.py b/tests/run-digest-tests.py new file mode 100644 index 0000000..b59e062 --- /dev/null +++ b/tests/run-digest-tests.py @@ -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)