Add explicit Authenticode workflows and correct CI helper contracts

This commit is contained in:
nefarius committed 2026-10-26 17:55:58 +01:00
1 parent 1026c48324
commit 7aa23f50be
18 files changed
+1948 -181

No files matched your search

+5
View File
@@ -1,5 +1,10 @@
# CI APIs # CI APIs
These historical availability notes are unverified. The analyzed ci.dll
10.0.26100.8328 x64 still exports the digest-based APIs listed below. Exact
first-available builds and ABI changes need matching old binaries. See
[the current contracts and remaining work](docs/mincrypt-contracts.md).
## Available starting with 1511 ## Available starting with 1511
- CiCheckSignedFile - CiCheckSignedFile
+5 -1
View File
@@ -24,7 +24,7 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
- I do not want to force any consumer of the library to drag C++ paradigms into their project. I do expect the user to utilize a modern compiler though, so the library sources themselves may have some 'C++-ish touches' here and there, for my own convenience 😉 - I do not want to force any consumer of the library to drag C++ paradigms into their project. I do expect the user to utilize a modern compiler though, so the library sources themselves may have some 'C++-ish touches' here and there, for my own convenience 😉
- Compatibility with every Windows 10 version. - Compatibility with every Windows 10 version.
- APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞 - APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞
- **Caution:** this claim comes with an asterisk though; due to PatchGuard (or some other security mechanism) I couldn't get run-time dynamic linking for `ci.dll` exports to work, so you need to actively avoid implementing code depending on exports that do not exist on earlier versions of Windows. Once I have a compatibility matrix for all of them I'll update the documentation accordingly. - CI entry points are currently static imports from `ci.lib`; consumers must avoid imports unavailable on their target Windows builds. Export lookup now uses the correct name-ordinal mapping and rejects forwarders, but this does not establish a complete CI availability or runtime ABI matrix. See the remaining-work ledger below.
- No conflicts with WDF or DMF - No conflicts with WDF or DMF
- The consuming driver *may* (but doesn't have to) utilize Microsoft WDF or DMF in addition without having to fear any incompatibilities 💪 - The consuming driver *may* (but doesn't have to) utilize Microsoft WDF or DMF in addition without having to fear any incompatibilities 💪
- Reliable SAL annotations. - Reliable SAL annotations.
@@ -73,6 +73,10 @@ Copy the provided `Domito.props` file into the directory of your `.vcxproj` file
## Code Integrity API notes ## 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) - [CiValidateFileObject flag 0x20: evidence, limitations, and kernel test procedure](docs/ci-validate-file-object.md)
## Sources & 3rd party credits ## Sources & 3rd party credits
+142
View File
@@ -0,0 +1,142 @@
# Authenticode digests and legacy verification
Prefer `DomitoCalculatePortableExecutableDigestEx` when computing a PE digest.
It takes an explicit SHA-1/256/384/512 algorithm, validates its supported PE layout,
and returns a digest independently of whether the file has an embedded signature.
This allows SHA-256 catalog lookup for a file without an embedded certificate.
Hash calculation is separate from signature verification and signer acceptance.
The older `DomitoCalculatePortableExecutableDigest` and
`DomitoGetPortableExecutableDigestKind` retain their compatibility behavior:
linear byte hashing and heuristic pattern discovery with a SHA-1 fallback. They
remain bounded, but should not be used to discover a general signature policy.
The new API is additive so existing callers do not silently acquire different
selection or rejection rules.
## Supported PE layout
The explicit API validates DOS/NT/optional-header spans, PE32 and PE32+ magic,
declared data-directory count, the complete section table, `SizeOfHeaders`, every
nonempty section's raw interval, and the certificate table. It sorts section
descriptors by raw file offset before validating their sequence. At most 96
sections are accepted, keeping stack storage and sorting work bounded.
Nonempty sections must form a contiguous sequence starting at `SizeOfHeaders`.
Section-header order can differ from raw-file order. Zero-raw-size sections
contribute no bytes. An overlay between the raw sections and the certificate
table, or EOF for an unsigned file, is included. The checksum and physical
certificate-directory slot are excluded. A present certificate table must be
8-byte aligned and end at EOF; every entry must have a bounded length, version
`0x200`, type PKCS#7 SignedData, and complete 8-byte alignment storage.
For this subset, sorted section bytes coincide with linear file order, so the
implementation can stream validated ranges directly to BCrypt without copying
or allocating a whole-file scratch buffer. Digest allocation is the only pool
allocation in the hashing path and is released on every failure.
Gaps, overlapping raw sections, certificates in the middle of a file, and absent
certificate-directory slots are unsupported. Some malformed inputs return a
format error; ambiguous or excluded layouts return `STATUS_NOT_SUPPORTED`.
The missing-slot boundary is deliberate: Windows SIP produced a different hash
than the old linear helper on a fixture with zero declared directories. Do not
infer Windows parity from the older helper's successful return on that input.
This is a validated subset, not a claim that every PE accepted by every Windows
SIP is supported. Microsoft's [PE format documentation](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format)
describes section ordering and exclusions, but unusual overlay behavior requires
measurements against the relevant Windows implementation. The local tests use
the actual [CryptSIPCreateIndirectData](https://learn.microsoft.com/en-us/windows/win32/api/mssip/nf-mssip-cryptsipcreateindirectdata)
provider as a differential oracle, rather than treating a prose summary as the
complete algorithm. SIP hash agreement does not establish CI trust-policy parity.
## Discovering the first embedded content's digest algorithm
`DomitoGetPortableExecutableDigestKindEx` navigates a bounded DER subset:
PKCS#7 ContentInfo / SignedData / encapsulated `SpcIndirectDataContent` /
`DigestInfo.AlgorithmIdentifier`. It reads the algorithm associated with the
file digest, avoiding unrelated algorithm patterns in certificates, attributes,
timestamps, or signer information. SHA-1/256/384/512 are recognized. Algorithm
parameters may be absent or ASN.1 NULL; the digest OCTET STRING must have the
corresponding length. DER lengths must be bounded, definite, and minimally
encoded. Up to seven zero bytes of aligned padding after the outer DER are
accepted when included in `WIN_CERTIFICATE.dwLength`, as observed on Microsoft
system binaries. Other trailing bytes are rejected.
An absent certificate table returns `STATUS_NOT_FOUND`, an unsupported algorithm
or certificate type returns `STATUS_NOT_SUPPORTED`, and malformed supported
structure returns a format error. The output algorithm is zero on failure; no
default algorithm is silently manufactured.
The caller supplies an array containing the security-directory entry and a
stable input buffer. The helper inspects the first table entry. It does not
enumerate nested/multiple signatures, validate the entire ASN.1 document,
validate signerInfos, verify cryptographic signatures, or establish trust.
Indefinite-length BER is outside this supported DER subset. Choose explicit
algorithms when handling catalog-only files or application-defined lookup order.
## Explicit legacy catalog policy
`DomitoValidateFileLegacyModeEx` takes a caller-selected
`AcceptedCatalogPolicyBits` mask, a `RequireTrustedCatalog` marker gate, and a
`ValidationSource` output. Source is `DOMITO_CI_SOURCE_EMBEDDED` or
`DOMITO_CI_SOURCE_CATALOG` on success and zero on failure.
This helper accepts SHA-1 with exactly 20 bytes or SHA-256 with exactly 32 bytes.
Unsupported algorithms return `STATUS_NOT_SUPPORTED`; mismatched lengths return
`STATUS_INVALID_PARAMETER`. The underlying observed embedded API supports more
algorithms than this helper's catalog workflow, as documented in
[the mincrypt contracts](mincrypt-contracts.md).
Both policy objects must be zero-initialized before first use, or own valid CI
results. The helper frees previous contents on entry and before each retry and
clears signing time between attempts. The caller owns both final policy results,
including any returned on failure, and frees them with `CiFreePolicyInfo`.
Embedded-to-catalog fallback and catalog reload occur only for
`STATUS_INVALID_IMAGE_HASH`; resource, I/O, and other policy errors propagate.
Before certificate allocation, the helper references the file object and checks
the supplied table interval against the current file size. It then uses the
exact-read `DomitoReadFileAt`, which waits on a private completion event. An EOF
short read fails before CI sees any uninitialized bytes. The caller must keep
file identity/bytes stable between hashing and consumption; a size check cannot
prevent concurrent mutation. Handles must be valid in the current process
context and satisfy the documented kernel I/O/APC requirements.
The older `DomitoValidateFileLegacyMode` delegates with the existing
`DOMITO_CI_LEGACY_CATALOG_POLICY_BITS` (`0x2007F`) and a false marker gate. That
value is a compatibility default with broad categories, not an application trust
recommendation. The explicit mask uses **any-overlap** catalog filtering.
For a catalog-only file, choose an allowed algorithm sequence as application
policy (for example SHA-256 followed by SHA-1 when legacy acceptance is intended).
For each algorithm, call `DomitoCalculatePortableExecutableDigestEx`, then
`DomitoValidateFileLegacyModeEx` with a zero security directory. Free the digest
and both policy objects before retrying. Continue to another algorithm only on
the specifically intended lookup/signature-mismatch result, not on all failures.
Preserve the actual algorithm and validation source in the application's result.
This supports an explicit search strategy; it does not reproduce every native
CI scenario-selection rule or WinVerifyTrust policy.
## Validation performed
On the local Windows 26100 system, host tests compare all four digest algorithms
against Windows SIP for PE32/PE32+, unsigned/signed fixtures, overlays, reverse
section-header order, multiple certificate entries, and certificate alignment.
The installed `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` form a
real-file corpus. Embedded digest algorithms are independently extracted with
Crypt32, and computed file digests are compared with the signed
`SpcIndirectDataContent` values where an embedded signature exists. This reads
files and hashes bytes; it does not load or execute them as code.
Guard-page truncations, malformed offsets/lengths, unsupported layouts,
algorithm selection, injected CNG/allocation failures, retry ownership, exact
read failures, and explicit catalog controls are also covered. Actual CI calls
are mocked in those host tests. The kernel translation units and layout
assertions compile for x64/ARM64 with warnings treated as errors; the opt-in
26100 runtime-layout observations remain x64-specific.
Still pending are historical/ARM64 CI runtime compatibility, Driver Verifier,
real catalog-only acceptance across algorithms, nested/multiple-signature
selection, weak-crypto/revocation/timestamp policy, and extension of supported PE
layouts. Use the [remaining-work ledger](implementation-status.md) and the
[flag probe](ci-validate-file-object.md) to plan the VM run.
+79
View File
@@ -0,0 +1,79 @@
# 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
- **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 layouts remain compatible; runtime layout identity cannot be inferred
from `NTDDI_VERSION` or `ChainInfo->Size` alone.
## Requires the test VM or additional matching binaries
The VM is not available yet. Host mocks and compilation do not close these:
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. Verify historical and ARM64 ABI, structure evolution and first-available
export versions using matching binaries/symbols and runtime tests. Preserve
default layouts until that evidence supports changing them.
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. Establish multiple/nested signature-selection behavior with a signed corpus;
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 three 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 opt-in observed-build header. Those are build checks,
not a claim that historical or ARM64 CI runtime ABI has been verified.
+211
View File
@@ -0,0 +1,211 @@
# 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. These observations do not establish
older-Windows or ARM64 ABI compatibility. Compile-target `NTDDI_VERSION` is not
runtime layout negotiation.
## 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 this build 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`.
## 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 remain implemented on the analyzed Windows 11 build. The older
header's “Win7–8.1 only” comments described a workflow, not verified export
availability. Exact first-available builds and pre/post-KB3033929 ABI changes
still require matching historical binaries. Import-library symbol names and
stdcall stack-byte counts alone do not settle parameter 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: historical/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.
+157
View File
@@ -0,0 +1,157 @@
/* 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
+68 -33
View File
@@ -277,6 +277,8 @@ MINCRYPT_DIGEST_BLOB, * PMINCRYPT_DIGEST_BLOB,
MINCRYPT_DER_BLOB, * PMINCRYPT_DER_BLOB, MINCRYPT_DER_BLOB, * PMINCRYPT_DER_BLOB,
MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB; MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB;
// Encoded, length-delimited name bytes; not necessarily ANSI or NUL-terminated.
// The observed 26100 x64 string tag occupies padding; see the opt-in version header.
typedef struct _MINCRYPT_NAME_BLOB typedef struct _MINCRYPT_NAME_BLOB
{ {
_Field_size_bytes_(Size) PCHAR Data; _Field_size_bytes_(Size) PCHAR Data;
@@ -287,6 +289,8 @@ typedef struct _MINCRYPT_NAME_BLOB
#define MINCRYPT_MAX_HASH_LENGTH (64) #define MINCRYPT_MAX_HASH_LENGTH (64)
#define MINCRYPT_SHA1_LENGTH (160/8) #define MINCRYPT_SHA1_LENGTH (160/8)
#define MINCRYPT_SHA256_LENGTH (256/8) #define MINCRYPT_SHA256_LENGTH (256/8)
#define MINCRYPT_SHA384_LENGTH (384/8)
#define MINCRYPT_SHA512_LENGTH (512/8)
typedef struct _MINCRYPT_CHAIN_ELEMENT typedef struct _MINCRYPT_CHAIN_ELEMENT
{ {
@@ -301,9 +305,11 @@ typedef struct _MINCRYPT_CHAIN_ELEMENT
}MINCRYPT_CHAIN_ELEMENT, * PMINCRYPT_CHAIN_ELEMENT; }MINCRYPT_CHAIN_ELEMENT, * PMINCRYPT_CHAIN_ELEMENT;
// CI owns one chain allocation; nested pointers borrow that allocation.
// Do not free child blobs separately or use memcpy to create a second owner.
typedef struct _MINCRYPT_CHAIN_INFO typedef struct _MINCRYPT_CHAIN_INFO
{ {
UINT32 Size; UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data.
MINCERT_BLOB* PublicKeys; MINCERT_BLOB* PublicKeys;
UINT32 NumberOfPublicKeys; UINT32 NumberOfPublicKeys;
@@ -319,7 +325,9 @@ typedef struct _MINCRYPT_CHAIN_INFO
// ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc. // ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc.
MINCRYPT_ATTR_BLOB AuthenticodeAttributes; MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
// UINT8 Hash[32]; // Observed 26100 x64 adds RootId at +0x34 and a 32-byte platform-manifest
// binary ID at +0x48. These are exposed only by Domito.MinCrypt.26100.h.
// Do not infer their availability solely from NTDDI_VERSION or Size.
#endif // NTDDI_VERSION >= NTDDI_WIN10 #endif // NTDDI_VERSION >= NTDDI_WIN10
@@ -338,7 +346,7 @@ typedef struct _MINCRYPT_CHAIN_INFO
typedef struct _MINCRYPT_POLICY_INFO typedef struct _MINCRYPT_POLICY_INFO
{ {
UINT32 Size; UINT32 Size;
UINT32 VerificationStatus; NTSTATUS VerificationStatus; // Check API return status separately.
UINT32 PolicyBits; UINT32 PolicyBits;
MINCRYPT_CHAIN_INFO* ChainInfo; MINCRYPT_CHAIN_INFO* ChainInfo;
@@ -347,8 +355,8 @@ typedef struct _MINCRYPT_POLICY_INFO
#if (NTDDI_VERSION >= NTDDI_WIN10) #if (NTDDI_VERSION >= NTDDI_WIN10)
LARGE_INTEGER NotBefore; // The certificate is not valid before this time LARGE_INTEGER NotBefore; // Observed: signer certificate validity start.
LARGE_INTEGER NotAfter; // The certificate is not valid after this time LARGE_INTEGER NotAfter; // Observed: signer certificate validity end.
#endif // NTDDI_VERSION >= NTDDI_WIN10 #endif // NTDDI_VERSION >= NTDDI_WIN10
@@ -358,12 +366,16 @@ typedef struct _MINCRYPT_POLICY_INFO
/** /**
* Resets a PolicyInfo struct - frees the dynamically allocated buffer in PolicyInfo (ChainInfo) if not null. * Release a known initialized policy's owned ChainInfo allocation and reset it.
* Zeros the entire PolicyInfo struct. * Zero-initialize policy objects before first use. Release both signer and TSA
* policies after use, including failures, and before reusing output storage.
* Borrowed child pointers become invalid when the owner is released. Never clear
* Size while a chain is still owned. Size does not negotiate the runtime layout.
* *
* @param PolicyInfo - the struct to reset. * @param PolicyInfo - initialized policy, not arbitrary uninitialized storage.
* *
* @return the struct which was reset. * Ignore the return value. The legacy PVOID declaration is retained for source
* compatibility; observed 26100 x64 code provides no defined pointer return.
*/ */
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI MINCRYPTAPI
@@ -375,18 +387,23 @@ CiFreePolicyInfo(
/** /**
* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10! * Digest-based workflow, also implemented on observed Windows 11 build 26100.
* Prefer the file-object workflow when its policy fits. Older-build ABI and
* KB3033929 requirements need a separately verified compatibility matrix.
* *
* Given a file digest and signature of a file, verify the signature and provide information regarding * Given a file digest and signature of a file, verify the signature and provide information regarding
* the certificates that was used for signing (the entire certificate chain) * the certificates that was used for signing (the entire certificate chain)
* *
* @param Hash - buffer containing the digest * @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
* *
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256 * @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
* *
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C) * @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
* *
* @param SecurityDirectory - pointer to the start of the security directory * @param SecurityDirectory - WIN_CERTIFICATE header, not the PKCS#7 payload.
* Observed 26100 x64 checks 4-byte pointer alignment, revision 0x200,
* type 2, supplied size >=12 and 8 < dwLength <= supplied size.
* This wrapper selects the first entry; it does not enumerate the table.
* *
* @param SizeOfSecurityDirectory - size the security directory * @param SizeOfSecurityDirectory - size the security directory
* *
@@ -396,9 +413,12 @@ CiFreePolicyInfo(
* *
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain * @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain
* *
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified. * @return Overall verification NTSTATUS. A successful policy VerificationStatus
* does not replace this return value: the final file-digest comparison
* can still fail. Both policy outputs may carry diagnostics on failure.
* See docs/mincrypt-contracts.md for ownership and policy limits.
* Various error values otherwise, for example: * Various error values otherwise, for example:
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature * STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked * STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired * STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/ */
@@ -407,10 +427,10 @@ MINCRYPTAPI
NTSTATUS NTSTATUS
NTAPI NTAPI
CiCheckSignedFile( CiCheckSignedFile(
_In_ PVOID Hash, _In_reads_bytes_(HashSize) PVOID Hash,
_In_ UINT32 HashSize, _In_ UINT32 HashSize,
_In_ ALG_ID HashAlgId, _In_ ALG_ID HashAlgId,
_In_ PVOID SecurityDirectory, _In_reads_bytes_(SizeOfSecurityDirectory) PVOID SecurityDirectory,
_In_ UINT32 SizeOfSecurityDirectory, _In_ UINT32 SizeOfSecurityDirectory,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_ LARGE_INTEGER* SigningTime, _Out_ LARGE_INTEGER* SigningTime,
@@ -418,36 +438,51 @@ CiCheckSignedFile(
); );
// Existing Domito compatibility filter, not a universal trusted-root policy.
// Includes test/DMD categories and a diagnostic bit; select policy deliberately.
#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
/** /**
* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10! * Digest-based workflow, also implemented on observed Windows 11 build 26100.
* Prefer the file-object workflow when its policy fits. Older-build ABI and
* KB3033929 requirements need a separately verified compatibility matrix.
* *
* Checks if the SHA-1 message digest is contained within a verified system catalog * Finds a caller-computed file digest in an accepted installed catalog.
* Observed 26100 x64 file-hash lookup supports SHA-1 and SHA-256.
* *
* @note must be attached to the PsInitialSystemProcess first! * @note must be attached to the PsInitialSystemProcess first!
* *
* @param Hash - buffer containing the digest * @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
* *
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256 * @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
* *
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C) * @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
* *
* @param IsReloadCatalogs - is reload catalogs cache. * @param ReloadCatalogs - request catalog-store reload; process/silo and cache state matter.
* *
* @param Always0 - this is for IsReloadCatalogs, Always0 != 0 ? 16 : 24; * @param RequireTrustedCatalog - nonzero requires the observed catalog TRUSTED
* extension marker. This does not itself establish Microsoft signer trust.
* *
* @param Always2007F - unknown, always 0x2007F, maybe a mask. * @param AcceptedCatalogPolicyBits - candidate catalog policy-bit filter.
* Observed 26100 x64 requires ANY overlap with catalog PolicyBits, not
* every requested bit. This is not a flags-to-enable input.
* *
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain. * @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
* *
* @param CatalogName[out option] - catalog file name. * @param CatalogName[out option] - initialize empty; observed 26100 x64 returns
* a separate duplicated string allocation. Release a returned Buffer
* with RtlFreeUnicodeString, separately from both policy outputs.
* *
* @param SigningTime[out] - when the file was signed (FILETIME format) * @param SigningTime[out] - when the file was signed (FILETIME format)
* *
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain. * @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain.
* *
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified. * @return Overall verification NTSTATUS. A successful policy VerificationStatus
* does not replace this return value. Catalog selection and the
* selected policy still apply. Outputs may carry diagnostics on failure.
* See docs/mincrypt-contracts.md for ownership and policy limits.
* Various error values otherwise, for example: * Various error values otherwise, for example:
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature * STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked * STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired * STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/ */
@@ -456,12 +491,12 @@ MINCRYPTAPI
NTSTATUS NTSTATUS
NTAPI NTAPI
CiVerifyHashInCatalog( CiVerifyHashInCatalog(
_In_ PVOID Hash, _In_reads_bytes_(HashSize) PVOID Hash,
_In_ UINT32 HashSize, _In_ UINT32 HashSize,
_In_ ALG_ID HashAlgId, _In_ ALG_ID HashAlgId,
_In_ BOOLEAN IsReloadCatalogs, _In_ BOOLEAN ReloadCatalogs,
_In_ UINT32 Always0, _In_ UINT32 RequireTrustedCatalog,
_In_ UINT32 Always2007F, _In_ UINT32 AcceptedCatalogPolicyBits,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_opt_ UNICODE_STRING* CatalogName, _Out_opt_ UNICODE_STRING* CatalogName,
_Out_ LARGE_INTEGER* SigningTime, _Out_ LARGE_INTEGER* SigningTime,
@@ -552,7 +587,7 @@ CiSetTrustedOriginClaimId(
* @return STATUS_SUCCESS when verification succeeds under the selected CI policy. * @return STATUS_SUCCESS when verification succeeds under the selected CI policy.
* This does not by itself establish the application's signer trust policy. * This does not by itself establish the application's signer trust policy.
* Various error values otherwise, for example: * Various error values otherwise, for example:
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature * STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked * STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired * STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
*/ */
@@ -562,12 +597,12 @@ NTSTATUS
NTAPI NTAPI
CiValidateFileObject( CiValidateFileObject(
_In_ FILE_OBJECT* FileObject, _In_ FILE_OBJECT* FileObject,
_In_opt_ UINT32 ValidationFlags, _In_ UINT32 ValidationFlags,
_In_opt_ UINT32 RequestedSigningLevel, _In_ UINT32 RequestedSigningLevel,
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo, _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
_Out_ LARGE_INTEGER* SigningTime, _Out_ LARGE_INTEGER* SigningTime,
_Out_ UINT8* Hash, _Out_writes_bytes_(*HashSize) UINT8* Hash,
_Inout_ UINT32* HashSize, _Inout_ UINT32* HashSize,
_Out_ ALG_ID* HashAlgId _Out_ ALG_ID* HashAlgId
); );
+96 -3
View File
@@ -184,6 +184,43 @@ DomitoGetPortableExecutableDigestKind(
_In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory _In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory
); );
// 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. // Calculates a PE digest using the existing linear hashing convention.
// Validates accessed header/certificate ranges and clears outputs on failure // Validates accessed header/certificate ranges and clears outputs on failure
@@ -217,8 +254,40 @@ DomitoFreePortableExecutableDigest(
_In_ PVOID pDigestOut _In_ PVOID pDigestOut
); );
#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
);
// //
// Verifies if the Authenticode signature of a give PE file matches the provided (e.g. SHA1) file digest. // 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) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
@@ -258,7 +327,10 @@ DomitoFindModuleBaseAddress(
); );
// //
// Finds the address of an exported function by name. // Finds an export by name (case-insensitive) in a valid loader-mapped module.
// ModuleBase must remain mapped; arbitrary file buffers are not supported.
// Export table spans are checked against SizeOfImage. Forwarded exports return
// STATUS_NOT_SUPPORTED; the optional output is cleared before lookup.
// //
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
@@ -289,7 +361,28 @@ DomitoMemorySearchPattern(
); );
// //
// Reads from the beginning of a file until the end or the buffer size is reached. // Reads exactly BufferSize bytes at a nonnegative absolute file offset.
// Same completion, buffer, handle, APC, and short-read contract as DomitoReadFile.
//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
DomitoReadFileAt(
_In_ HANDLE FileHandle,
_Out_writes_bytes_(BufferSize) PVOID Buffer,
_In_ ULONG BufferSize,
_In_ const LARGE_INTEGER* ByteOffset
);
//
// Reads exactly BufferSize bytes from file offset zero. Short reads return
// STATUS_END_OF_FILE; Buffer may then contain a partial read. Zero length succeeds.
// Waits for pending I/O on a private event before returning. Requires PASSIVE_LEVEL
// with special kernel APCs enabled, a readable handle valid in the current process
// (or a kernel handle), and a buffer that remains valid for the entire call.
// For synchronous handles, the explicit read also updates the file position.
// //
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
+378 -48
View File
@@ -89,11 +89,252 @@ DomitoGetPortableExecutableDigestKind(
return CALG_SHA1; 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, 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, wrapped, signedData, ignored, contentInfo, indirect, digest, algorithm, 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, 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(&section, 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, 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) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
NTSTATUS static NTSTATUS
DomitoCalculatePortableExecutableDigest( DomitoCalculatePortableExecutableDigestInternal(
_In_ UINT32 ExplicitCalg,
_In_ PUCHAR pPeBytes, _In_ PUCHAR pPeBytes,
_In_ ULONG PeSize, _In_ ULONG PeSize,
_Out_ PUINT32 pDigestCalgOut, _Out_ PUINT32 pDigestCalgOut,
@@ -231,7 +472,8 @@ DomitoCalculatePortableExecutableDigest(
BCRYPT_HASH_HANDLE hbHash = nullptr; BCRYPT_HASH_HANDLE hbHash = nullptr;
ULONG hashLength = 0; ULONG hashLength = 0;
ULONG resultLength = 0; ULONG resultLength = 0;
const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories); const UINT32 peDigestKind = ExplicitCalg ? ExplicitCalg :
DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories);
NTSTATUS status = BCryptOpenAlgorithmProvider( NTSTATUS status = BCryptOpenAlgorithmProvider(
&hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0); &hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0);
if (!NT_SUCCESS(status)) if (!NT_SUCCESS(status))
@@ -315,6 +557,47 @@ cleanup:
} }
_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) _IRQL_requires_max_(DISPATCH_LEVEL)
void void
DomitoFreePortableExecutableDigest( DomitoFreePortableExecutableDigest(
@@ -333,8 +616,8 @@ _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
#pragma code_seg("PAGED") #pragma code_seg("PAGED")
NTSTATUS static NTSTATUS
DomitoValidateFileLegacyMode( DomitoValidateFileLegacyModeInternal(
_In_ HANDLE FileHandle, _In_ HANDLE FileHandle,
_In_ PVOID Hash, _In_ PVOID Hash,
_In_ UINT32 HashSize, _In_ UINT32 HashSize,
@@ -342,11 +625,20 @@ DomitoValidateFileLegacyMode(
_In_ const IMAGE_DATA_DIRECTORY * SecurityDirectory, _In_ const IMAGE_DATA_DIRECTORY * SecurityDirectory,
_Inout_ MINCRYPT_POLICY_INFO * PolicyInfo, _Inout_ MINCRYPT_POLICY_INFO * PolicyInfo,
_Out_ LARGE_INTEGER * SigningTime, _Out_ LARGE_INTEGER * SigningTime,
_Inout_ MINCRYPT_POLICY_INFO * TimeStampPolicyInfo _Inout_ MINCRYPT_POLICY_INFO * TimeStampPolicyInfo,
_In_ UINT32 AcceptedCatalogPolicyBits,
_In_ BOOLEAN RequireTrustedCatalog,
_Out_ PULONG ValidationSource
) )
{ {
PAGED_CODE(); 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; NTSTATUS status = STATUS_SUCCESS;
PVOID certDirectory = nullptr; PVOID certDirectory = nullptr;
KAPC_STATE systemContext = {}; KAPC_STATE systemContext = {};
@@ -358,15 +650,48 @@ DomitoValidateFileLegacyMode(
CiFreePolicyInfo(PolicyInfo); CiFreePolicyInfo(PolicyInfo);
CiFreePolicyInfo(TimeStampPolicyInfo); 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 (HashSize != (UINT32)(HashAlgId == CALG_SHA1 ? MINCRYPT_SHA1_LENGTH : MINCRYPT_SHA256_LENGTH))
{
status = STATUS_INVALID_PARAMETER;
break;
}
if ((SecurityDirectory->Size != 0) != (SecurityDirectory->VirtualAddress != 0))
{
status = STATUS_INVALID_IMAGE_FORMAT;
break; break;
} }
if (SecurityDirectory->Size != 0u && if (SecurityDirectory->Size != 0u &&
SecurityDirectory->VirtualAddress != 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);
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) #pragma warning(disable:4996)
certDirectory = ExAllocatePoolWithTag( certDirectory = ExAllocatePoolWithTag(
PagedPool, PagedPool,
@@ -381,44 +706,10 @@ DomitoValidateFileLegacyMode(
} }
LARGE_INTEGER offset = {}; LARGE_INTEGER offset = {};
IO_STATUS_BLOCK ioStatusBlock = {}; offset.QuadPart = SecurityDirectory->VirtualAddress;
status = DomitoReadFileAt(FileHandle, certDirectory, SecurityDirectory->Size, &offset);
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;
}
if (!NT_SUCCESS(status)) if (!NT_SUCCESS(status))
{
break; break;
}
if (ioStatusBlock.Information != SecurityDirectory->Size)
{
status = STATUS_END_OF_FILE;
break;
}
KeStackAttachProcess(PsInitialSystemProcess, &systemContext); KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
{ {
@@ -437,6 +728,7 @@ DomitoValidateFileLegacyMode(
if (NT_SUCCESS(status)) if (NT_SUCCESS(status))
{ {
*ValidationSource = DOMITO_CI_SOURCE_EMBEDDED;
break; break;
} }
@@ -446,6 +738,9 @@ DomitoValidateFileLegacyMode(
} }
} }
CiFreePolicyInfo(PolicyInfo);
CiFreePolicyInfo(TimeStampPolicyInfo);
SigningTime->QuadPart = 0;
KeStackAttachProcess(PsInitialSystemProcess, &systemContext); KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
{ {
status = CiVerifyHashInCatalog( status = CiVerifyHashInCatalog(
@@ -453,8 +748,8 @@ DomitoValidateFileLegacyMode(
HashSize, HashSize,
HashAlgId, HashAlgId,
FALSE, FALSE,
0, RequireTrustedCatalog,
0x2007F, AcceptedCatalogPolicyBits,
PolicyInfo, PolicyInfo,
NULL, NULL,
SigningTime, SigningTime,
@@ -463,13 +758,16 @@ DomitoValidateFileLegacyMode(
if (status == STATUS_INVALID_IMAGE_HASH) if (status == STATUS_INVALID_IMAGE_HASH)
{ {
CiFreePolicyInfo(PolicyInfo);
CiFreePolicyInfo(TimeStampPolicyInfo);
SigningTime->QuadPart = 0;
status = CiVerifyHashInCatalog( status = CiVerifyHashInCatalog(
Hash, Hash,
HashSize, HashSize,
HashAlgId, HashAlgId,
TRUE, TRUE,
0, RequireTrustedCatalog,
0x2007F, AcceptedCatalogPolicyBits,
PolicyInfo, PolicyInfo,
NULL, NULL,
SigningTime, SigningTime,
@@ -478,6 +776,8 @@ DomitoValidateFileLegacyMode(
} }
} }
KeUnstackDetachProcess(&systemContext); KeUnstackDetachProcess(&systemContext);
if (NT_SUCCESS(status))
*ValidationSource = DOMITO_CI_SOURCE_CATALOG;
} while (FALSE); } while (FALSE);
@@ -488,4 +788,34 @@ DomitoValidateFileLegacyMode(
return status; 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() #pragma code_seg()
+165 -64
View File
@@ -129,6 +129,13 @@ DomitoFindModuleBaseAddress(
return status; return status;
} }
// This helper handles directory RVAs after the loader-mapped header establishes
// SizeOfImage. ModuleBase is not an arbitrary file buffer (there is no size input).
static BOOLEAN DomitoImageRvaSpan(ULONG ImageSize, ULONG Rva, ULONG Count, ULONG ElementSize)
{
return Rva <= ImageSize && ElementSize != 0 && Count <= (ImageSize - Rva) / ElementSize;
}
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
@@ -139,56 +146,113 @@ DomitoFindExportedFunctionAddress(
_Inout_opt_ PVOID * FunctionAddress _Inout_opt_ PVOID * FunctionAddress
) )
{ {
NTSTATUS status = STATUS_NOT_FOUND; if (FunctionAddress)
ULONG exportSize; {
*FunctionAddress = nullptr;
if (G_Common.RtlImageDirectoryEntryToData == NULL) }
if (!ModuleBase || !FunctionName || !FunctionName->Buffer || !FunctionName->Length)
{
return STATUS_INVALID_PARAMETER;
}
if (!G_Common.RtlImageDirectoryEntryToData)
{ {
return STATUS_NOT_IMPLEMENTED; return STATUS_NOT_IMPLEMENTED;
} }
// Retrieve the export directory information // Initial headers belong to a valid loaded module, as required by this API.
const PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)G_Common.RtlImageDirectoryEntryToData( const PUCHAR base = (PUCHAR)ModuleBase;
ModuleBase, IMAGE_DOS_HEADER dos;
TRUE, memcpy(&dos, base, sizeof(dos));
IMAGE_DIRECTORY_ENTRY_EXPORT, if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < (LONG)sizeof(dos))
&exportSize {
); return STATUS_INVALID_IMAGE_FORMAT;
}
if (exportDirectory == NULL) 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));
if (signature != IMAGE_NT_SIGNATURE ||
(magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC && magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC))
{
return STATUS_INVALID_IMAGE_FORMAT;
}
// 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; return STATUS_INVALID_IMAGE_FORMAT;
} }
STRING currentFunctionName; ULONG exportSize = 0;
const PVOID directory = G_Common.RtlImageDirectoryEntryToData(
const PULONG functionAddresses = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfFunctions); ModuleBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize);
const PULONG functionNames = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNames); const ULONG_PTR directoryOffset = (ULONG_PTR)directory - (ULONG_PTR)ModuleBase;
const PUSHORT functionOrdinals = (PUSHORT)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNameOrdinals); if (!directory || directoryOffset > imageSize || exportSize < sizeof(IMAGE_EXPORT_DIRECTORY) ||
exportSize > imageSize - directoryOffset)
for (ULONG i = 0; i < exportDirectory->NumberOfNames; i++)
{ {
const char* functionName = (const char*)((ULONG_PTR)ModuleBase + functionNames[i]); return STATUS_INVALID_IMAGE_FORMAT;
const USHORT functionOrdinal = functionOrdinals[i]; }
UNREFERENCED_PARAMETER(functionOrdinal); IMAGE_EXPORT_DIRECTORY exports;
memcpy(&exports, directory, sizeof(exports));
const ULONG functionRva = functionAddresses[i]; if (!DomitoImageRvaSpan(imageSize, exports.AddressOfFunctions, exports.NumberOfFunctions, sizeof(ULONG)) ||
const PVOID functionAddress = (PVOID)((ULONG_PTR)ModuleBase + functionRva); !DomitoImageRvaSpan(imageSize, exports.AddressOfNames, exports.NumberOfNames, sizeof(ULONG)) ||
!DomitoImageRvaSpan(imageSize, exports.AddressOfNameOrdinals, exports.NumberOfNames, sizeof(USHORT)))
RtlInitAnsiString(&currentFunctionName, functionName);
if (0 == RtlCompareString(FunctionName, &currentFunctionName, TRUE))
{ {
return STATUS_INVALID_IMAGE_FORMAT;
}
for (ULONG i = 0; i < exports.NumberOfNames; ++i)
{
ULONG nameRva;
USHORT ordinal;
memcpy(&nameRva, base + exports.AddressOfNames + i * sizeof(ULONG), sizeof(nameRva));
memcpy(&ordinal, base + exports.AddressOfNameOrdinals + i * sizeof(USHORT), sizeof(ordinal));
if (nameRva >= imageSize || ordinal >= exports.NumberOfFunctions)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
ULONG length = 0;
while (length < imageSize - nameRva && base[nameRva + length] != 0)
{
++length;
}
if (length == imageSize - nameRva || length > MAXUSHORT)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
ANSI_STRING current;
current.Buffer = (PCHAR)(base + nameRva);
current.Length = (USHORT)length;
current.MaximumLength = (USHORT)length;
if (RtlCompareString(FunctionName, &current, TRUE) != 0)
{
continue;
}
ULONG functionRva;
memcpy(&functionRva, base + exports.AddressOfFunctions + ordinal * sizeof(ULONG), sizeof(functionRva));
if (!functionRva)
{
return STATUS_NOT_FOUND; // Empty EAT slot.
}
if (functionRva >= imageSize)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
if (functionRva >= directoryOffset && functionRva - directoryOffset < exportSize)
{
return STATUS_NOT_SUPPORTED; // Forwarder string, not a callable address.
}
if (FunctionAddress) if (FunctionAddress)
{ {
status = STATUS_SUCCESS; *FunctionAddress = base + functionRva;
*FunctionAddress = functionAddress;
} }
break; return STATUS_SUCCESS;
} }
} return STATUS_NOT_FOUND;
return status;
} }
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
@@ -240,6 +304,68 @@ DomitoMemorySearchPattern(
return STATUS_NOT_FOUND; return STATUS_NOT_FOUND;
} }
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
NTSTATUS
DomitoReadFileAt(
_In_ HANDLE FileHandle,
_Out_ PVOID Buffer,
_In_ ULONG BufferSize,
_In_ const LARGE_INTEGER* ByteOffset
)
{
if (!FileHandle || !ByteOffset || ByteOffset->QuadPart < 0 || (!Buffer && BufferSize))
{
return STATUS_INVALID_PARAMETER;
}
if (!BufferSize)
{
return STATUS_SUCCESS;
}
HANDLE eventHandle = nullptr;
OBJECT_ATTRIBUTES attributes;
InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr);
NTSTATUS status = ZwCreateEvent(&eventHandle, EVENT_ALL_ACCESS, &attributes,
NotificationEvent, FALSE);
if (!NT_SUCCESS(status))
{
return status;
}
PVOID eventObject = nullptr;
status = ObReferenceObjectByHandle(eventHandle, SYNCHRONIZE, *ExEventObjectType,
KernelMode, &eventObject, nullptr);
if (!NT_SUCCESS(status))
{
ZwClose(eventHandle);
return status;
}
IO_STATUS_BLOCK ioStatusBlock = {};
LARGE_INTEGER offset = *ByteOffset;
status = ZwReadFile(FileHandle, eventHandle, nullptr, nullptr, &ioStatusBlock,
Buffer, BufferSize, &offset, nullptr);
if (status == STATUS_PENDING)
{
// A private event identifies this request even if the file has other I/O.
// KernelMode + nonalertable + no timeout cannot return before completion.
KeWaitForSingleObject(eventObject, Executive, KernelMode, FALSE, nullptr);
status = ioStatusBlock.Status;
}
if (NT_SUCCESS(status))
{
status = ioStatusBlock.Status;
if (NT_SUCCESS(status) && ioStatusBlock.Information != BufferSize)
{
status = STATUS_END_OF_FILE;
}
}
ObDereferenceObject(eventObject);
ZwClose(eventHandle);
return status;
}
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL) _IRQL_requires_max_(PASSIVE_LEVEL)
@@ -250,33 +376,8 @@ DomitoReadFile(
_In_ ULONG BufferSize _In_ ULONG BufferSize
) )
{ {
NTSTATUS status = STATUS_SUCCESS; const LARGE_INTEGER offset = {};
IO_STATUS_BLOCK ioStatusBlock; return DomitoReadFileAt(FileHandle, Buffer, BufferSize, &offset);
// Read the file into memory using ZwReadFile
if (!NT_SUCCESS(status = ZwReadFile(
FileHandle,
NULL,
NULL,
NULL,
&ioStatusBlock,
Buffer,
BufferSize,
NULL,
NULL
)))
{
return status;
}
// Check if the file was read successfully
if (!NT_SUCCESS(ioStatusBlock.Status))
{
return ioStatusBlock.Status;
}
// File read successfully
return status;
} }
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
@@ -306,7 +407,7 @@ DomitoGetProcessImageName(
} }
CLIENT_ID cid; CLIENT_ID cid;
cid.UniqueProcess = (HANDLE)ProcessId; cid.UniqueProcess = ULongToHandle(ProcessId);
cid.UniqueThread = NULL; cid.UniqueThread = NULL;
OBJECT_ATTRIBUTES objAttr; OBJECT_ATTRIBUTES objAttr;
+1
View File
@@ -127,6 +127,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\include\Domito.MinCrypt.h" /> <ClInclude Include="..\include\Domito.MinCrypt.h" />
<ClInclude Include="..\include\Domito.MinCrypt.26100.h" />
<ClInclude Include="..\include\Domito.h" /> <ClInclude Include="..\include\Domito.h" />
<ClInclude Include="Domito.Internal.h" /> <ClInclude Include="Domito.Internal.h" />
</ItemGroup> </ItemGroup>
+3
View File
@@ -39,6 +39,9 @@
<ClInclude Include="..\include\Domito.h"> <ClInclude Include="..\include\Domito.h">
<Filter>Header Files\Public</Filter> <Filter>Header Files\Public</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="..\include\Domito.MinCrypt.26100.h">
<Filter>Header Files\Public</Filter>
</ClInclude>
<ClInclude Include="..\include\Domito.MinCrypt.h"> <ClInclude Include="..\include\Domito.MinCrypt.h">
<Filter>Header Files\Public</Filter> <Filter>Header Files\Public</Filter>
</ClInclude> </ClInclude>
+49 -17
View File
@@ -1,30 +1,62 @@
# Code Integrity regression tests # Regression tests
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3: From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
```powershell ```powershell
python tests\run-digest-tests.py python tests\run-digest-tests.py
python tests\run-common-tests.py
python tests\run-ci-probe-tests.py
``` ```
The runner extracts the production digest, pattern-search, algorithm-mapping, Build outputs are temporary. No driver is installed or loaded. Test runners
and legacy-validation functions without changing their bodies. The C++ harness extract the actual production functions or include the probe body unchanged;
uses real user-mode BCrypt hashing and test doubles for pool allocation, file kernel calls, CI calls, and pool allocation are replaced with test doubles.
reads, and CI. No driver is installed or loaded. Build outputs are temporary. The digest harness uses real BCrypt, Crypt32 and the Windows PE SIP provider.
Coverage includes PE32/PE32+ digest fixtures, checksum and certificate exclusions, ## Digest and legacy verification tests
a DOS stub larger than 4 MiB, truncated headers next to inaccessible guard pages,
invalid offsets and lengths, absent security-directory entries, injected hashing
and allocation failures, deterministic failure outputs, allocation ownership,
and rejection of successful-but-short certificate reads before invoking CI.
The tests do not validate CI trust policy, asynchronous kernel I/O behavior, or Coverage includes PE32/PE32+ files, all four supported hash algorithms, checksum
general Authenticode hashing of unusual section layouts. The digest implementation and certificate exclusions, overlays, reverse section-header order, multiple
still uses its existing linear byte-order convention and heuristic signature certificate entries, certificate padding, unsupported section layouts, bounded
algorithm detection; replacing those requires a separate compatibility corpus. 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.
## File-object validation flag probe ## File-object validation flag probe
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md) See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
for `CiValidateFileObject` flag `0x20`. Run `python tests\run-ci-probe-tests.py` for `CiValidateFileObject` flag `0x20`. The host runner tests probe plumbing with
from the same developer shell to test probe plumbing with mock CI. The kernel mock CI. The kernel probe must still run in a suitable VM to verify actual
probe must still be run in a suitable VM to verify actual policy behavior. 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.
+152
View File
@@ -0,0 +1,152 @@
// Tests extract the unchanged production bodies. Kernel I/O is simulated.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#include <cctype>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define MAXUSHORT 0xffff
#define Executive 0
#define KernelMode 0
#define NotificationEvent 0
#define OBJ_KERNEL_HANDLE 0x200
#define InitializeObjectAttributes(a,n,f,r,s) ((a)->Attributes=(f))
struct OBJECT_ATTRIBUTES { ULONG Attributes; };
struct ANSI_STRING { USHORT Length, 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, fileToken;
static unsigned created, referenced, closed, dereferenced, reads, waits;
static int ioMode;
static bool outstanding;
static IO_STATUS_BLOCK* pendingIosb;
static ULONG requested;
static LONGLONG expectedOffset;
static NTSTATUS ZwCreateEvent(HANDLE* event, ACCESS_MASK access, OBJECT_ATTRIBUTES* attributes, int type, BOOLEAN signaled) {
Check(access == EVENT_ALL_ACCESS && attributes->Attributes == OBJ_KERNEL_HANDLE && type == NotificationEvent && !signaled, "private kernel event");
if (ioMode == 4) return STATUS_INSUFFICIENT_RESOURCES;
++created; *event = &eventToken; return STATUS_SUCCESS;
}
static NTSTATUS ObReferenceObjectByHandle(HANDLE event, ACCESS_MASK access, void* type, int mode, void** object, void*) {
Check(event == &eventToken && access == SYNCHRONIZE && type == *ExEventObjectType && mode == KernelMode, "reference own event");
if (ioMode == 5) return STATUS_ACCESS_DENIED;
++referenced; *object = &eventToken; return STATUS_SUCCESS;
}
static void ZwClose(HANDLE event) {
Check(event == &eventToken && !outstanding, "close only after completion"); ++closed;
}
static void ObDereferenceObject(void* object) {
Check(object == &eventToken && !outstanding, "dereference only after completion"); ++dereferenced;
}
static NTSTATUS ZwReadFile(HANDLE file, HANDLE event, void*, void*, IO_STATUS_BLOCK* iosb, void*, ULONG size, LARGE_INTEGER* offset, void*) {
++reads;
Check(file == &fileToken && event == &eventToken && offset && offset->QuadPart == expectedOffset, "file and explicit offset");
Check(iosb->Status == 0 && iosb->Information == 0, "initialized IOSB");
requested = size;
if (ioMode == 6) return STATUS_ACCESS_DENIED;
if (ioMode == 1 || ioMode == 2) { outstanding = true; pendingIosb = iosb; return STATUS_PENDING; }
iosb->Status = STATUS_SUCCESS; iosb->Information = ioMode == 3 ? size - 1 : size;
return STATUS_SUCCESS;
}
static NTSTATUS KeWaitForSingleObject(void* object, int reason, int mode, BOOLEAN alertable, LARGE_INTEGER* timeout) {
++waits;
Check(object == &eventToken && reason == Executive && mode == KernelMode && !alertable && !timeout && outstanding, "noninterruptible per-request wait");
outstanding = false;
pendingIosb->Status = ioMode == 2 ? STATUS_END_OF_FILE : STATUS_SUCCESS;
pendingIosb->Information = ioMode == 2 ? 0 : requested;
return STATUS_SUCCESS;
}
#include "common-under-test.inc"
static std::vector<UCHAR> Image() {
std::vector<UCHAR> image(4096);
auto dos = (IMAGE_DOS_HEADER*)image.data(); dos->e_magic = IMAGE_DOS_SIGNATURE; dos->e_lfanew = 128;
auto nt = (IMAGE_NT_HEADERS64*)(image.data() + 128); nt->Signature = IMAGE_NT_SIGNATURE;
nt->OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; nt->OptionalHeader.SizeOfImage = (ULONG)image.size();
auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data() + 512);
exports->NumberOfFunctions = 3; exports->NumberOfNames = 2;
exports->AddressOfFunctions = 801; exports->AddressOfNames = 901; exports->AddressOfNameOrdinals = 1001;
const ULONG functions[] = {2000, 2100, 2200}, names[] = {1100, 1120};
const USHORT ordinals[] = {2, 0};
std::memcpy(image.data()+801, functions, sizeof(functions)); std::memcpy(image.data()+901, names, sizeof(names));
std::memcpy(image.data()+1001, ordinals, sizeof(ordinals));
std::memcpy(image.data()+1100, "Beta", 5); std::memcpy(image.data()+1120, "Alpha", 6);
return image;
}
static NTSTATUS Lookup(std::vector<UCHAR>& image, const char* name, PVOID* output) {
ANSI_STRING string = {(USHORT)std::strlen(name), (USHORT)std::strlen(name), (PCHAR)name};
exportPointer = image.data() + 512; exportBytes = 256;
return DomitoFindExportedFunctionAddress(image.data(), &string, output);
}
int main() {
auto image = Image(); PVOID output = nullptr;
Check(Lookup(image,"Beta",&output) == STATUS_SUCCESS && output == image.data()+2200, "ordinal-indexed EAT");
Check(Lookup(image,"aLpHa",&output) == STATUS_SUCCESS && output == image.data()+2000, "case-insensitive name");
Check(Lookup(image,"Alpha",nullptr) == STATUS_SUCCESS, "optional output discovery");
Check(Lookup(image,"missing",&output) == STATUS_NOT_FOUND && !output, "notfound clears output");
auto exports = (IMAGE_EXPORT_DIRECTORY*)(image.data()+512);
const USHORT invalidOrdinal = 3; std::memcpy(image.data()+1001,&invalidOrdinal,2);
Check(Lookup(image,"Beta",&output) == STATUS_INVALID_IMAGE_FORMAT && !output, "ordinal bounded");
image=Image(); const ULONG forwarder=600; std::memcpy(image.data()+801+8,&forwarder,4);
Check(Lookup(image,"Beta",&output) == STATUS_NOT_SUPPORTED && !output, "forwarder rejected");
image=Image(); const ULONG zero=0; std::memcpy(image.data()+801+8,&zero,4);
Check(Lookup(image,"Beta",&output) == STATUS_NOT_FOUND, "empty EAT slot");
image=Image(); const ULONG outside=4096; std::memcpy(image.data()+801+8,&outside,4);
Check(Lookup(image,"Beta",&output) == STATUS_INVALID_IMAGE_FORMAT, "function RVA bounded");
image=Image(); exports=(IMAGE_EXPORT_DIRECTORY*)(image.data()+512); exports->NumberOfNames=0xffffffff;
Check(Lookup(image,"Beta",&output) == STATUS_INVALID_IMAGE_FORMAT, "table multiplication bounded");
image=Image(); exports=(IMAGE_EXPORT_DIRECTORY*)(image.data()+512); exports->AddressOfFunctions=4090;
Check(Lookup(image,"Beta",&output) == STATUS_INVALID_IMAGE_FORMAT, "EAT span bounded");
image=Image(); const ULONG lateName=4094; std::memcpy(image.data()+901,&lateName,4); image[4094]='x';image[4095]='x';
Check(Lookup(image,"Beta",&output) == STATUS_INVALID_IMAGE_FORMAT, "unterminated name bounded");
image=Image(); std::memcpy(image.data()+901,&outside,4);
Check(Lookup(image,"Beta",&output) == STATUS_INVALID_IMAGE_FORMAT, "name RVA bounded");
image=Image(); ANSI_STRING name={4,4,(PCHAR)"Beta"}; exportPointer=image.data()+512; exportBytes=4096;
Check(DomitoFindExportedFunctionAddress(image.data(),&name,&output)==STATUS_INVALID_IMAGE_FORMAT, "export directory span bounded");
output=(void*)1; Check(DomitoFindExportedFunctionAddress(nullptr,&name,&output)==STATUS_INVALID_PARAMETER && !output,"invalid arguments clear output");
char buffer[32];
for (ioMode=0;ioMode<=6;++ioMode) {
created=referenced=closed=dereferenced=reads=waits=0; outstanding=false; expectedOffset=123;
LARGE_INTEGER offset;offset.QuadPart=expectedOffset;
NTSTATUS status=DomitoReadFileAt(&fileToken,buffer,sizeof(buffer),&offset);
const NTSTATUS expected[]={STATUS_SUCCESS,STATUS_SUCCESS,STATUS_END_OF_FILE,STATUS_END_OF_FILE,STATUS_INSUFFICIENT_RESOURCES,STATUS_ACCESS_DENIED,STATUS_ACCESS_DENIED};
Check(status==expected[ioMode],"read status and exact-byte contract");
Check(created==closed && referenced==dereferenced && !outstanding,"event lifetime balanced");
Check(waits==((ioMode==1||ioMode==2)?1u:0u),"wait pending only");
}
ioMode=0; expectedOffset=0; Check(DomitoReadFile(&fileToken,buffer,sizeof(buffer))==STATUS_SUCCESS,"zero-offset wrapper");
LARGE_INTEGER offset={}; offset.QuadPart=-1;
Check(DomitoReadFileAt(&fileToken,buffer,sizeof(buffer),&offset)==STATUS_INVALID_PARAMETER,"negative offset rejected");
Check(DomitoReadFileAt(&fileToken,buffer,sizeof(buffer),nullptr)==STATUS_INVALID_PARAMETER,"null offset rejected");
offset.QuadPart=0;
Check(DomitoReadFileAt(&fileToken,nullptr,0,&offset)==STATUS_SUCCESS,"zero byte read");
Check(DomitoReadFileAt(&fileToken,nullptr,1,&offset)==STATUS_INVALID_PARAMETER,"null buffer rejected");
std::printf("PASS: %u common-helper checks (mock kernel I/O)\n", checks);
}
+394 -14
View File
@@ -6,10 +6,12 @@
#include <ntstatus.h> #include <ntstatus.h>
#include <bcrypt.h> #include <bcrypt.h>
#include <wintrust.h> #include <wintrust.h>
#include <mssip.h>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <vector> #include <vector>
#include <cstring> #include <cstring>
#include <string>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0) #define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define ASSERT(x) Check(!!(x), #x) #define ASSERT(x) Check(!!(x), #x)
@@ -19,11 +21,21 @@
#define PAGED_CODE() ((void)0) #define PAGED_CODE() ((void)0)
#define MINCRYPT_SHA1_LENGTH 20 #define MINCRYPT_SHA1_LENGTH 20
#define MINCRYPT_SHA256_LENGTH 32 #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; typedef const UCHAR* PCUCHAR;
static unsigned checks, allocations, ciCalls, hashCalls; static unsigned checks, allocations, ciCalls, hashCalls;
static int failOperation, failHashCall; static int failOperation, failHashCall;
static ULONG_PTR bytesRead; 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) static void Check(bool ok, const char* message)
{ {
++checks; ++checks;
@@ -61,33 +73,67 @@ static NTSTATUS TestFinishHash(BCRYPT_HASH_HANDLE h, PUCHAR b, ULONG n, ULONG f)
} }
// Minimal dependencies for the legacy wrapper; no actual CI code is invoked. // Minimal dependencies for the legacy wrapper; no actual CI code is invoked.
struct MINCRYPT_POLICY_INFO { ULONG Size; }; struct MINCRYPT_POLICY_INFO { ULONG Size; PVOID ChainInfo; };
struct KAPC_STATE { ULONG unused; }; struct KAPC_STATE { ULONG unused; };
struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; }; struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; };
static PVOID PsInitialSystemProcess = nullptr; static PVOID PsInitialSystemProcess = nullptr;
static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* p) { p->Size = 0; } 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 KeStackAttachProcess(PVOID, KAPC_STATE*) {}
static void KeUnstackDetachProcess(KAPC_STATE*) {} static void KeUnstackDetachProcess(KAPC_STATE*) {}
static NTSTATUS ZwReadFile(HANDLE, PVOID, PVOID, PVOID, IO_STATUS_BLOCK* io, typedef PVOID PFILE_OBJECT;
PVOID buffer, ULONG size, LARGE_INTEGER*, PVOID) static PVOID fileObjectType;
static PVOID* IoFileObjectType = &fileObjectType;
#define KernelMode 0
static NTSTATUS ObReferenceObjectByHandle(HANDLE, ULONG, PVOID, int, PVOID* object, PVOID)
{ {
io->Status = STATUS_SUCCESS; *object = &fileObjectType;
io->Information = bytesRead; 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)); std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size));
return STATUS_SUCCESS; return bytesRead == size ? STATUS_SUCCESS : STATUS_END_OF_FILE;
} }
static NTSTATUS ZwWaitForSingleObject(HANDLE, BOOLEAN, PVOID) { return STATUS_SUCCESS; }
static NTSTATUS CiCheckSignedFile(PVOID, UINT32, ALG_ID, PVOID, UINT32, static NTSTATUS CiCheckSignedFile(PVOID, UINT32, ALG_ID, PVOID, UINT32,
MINCRYPT_POLICY_INFO*, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*) MINCRYPT_POLICY_INFO* policy, LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
{ {
++ciCalls; ++ciCalls;
return STATUS_SUCCESS; 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, UINT32, UINT32, static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN reload,
MINCRYPT_POLICY_INFO*, PVOID, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*) UINT32 trusted, UINT32 mask, MINCRYPT_POLICY_INFO* policy, PVOID,
LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
{ {
++ciCalls; ++ciCalls;
return STATUS_SUCCESS; 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 BCryptCreateHash TestCreateHash
@@ -108,21 +154,32 @@ static std::vector<UCHAR> Image(bool pe64, ULONG nt = 128, bool certificate = fa
std::memcpy(b.data() + nt, &signature, sizeof(signature)); std::memcpy(b.data() + nt, &signature, sizeof(signature));
IMAGE_FILE_HEADER f = {}; IMAGE_FILE_HEADER f = {};
f.NumberOfSections = 1; 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); f.SizeOfOptionalHeader = pe64 ? sizeof(IMAGE_OPTIONAL_HEADER64) : sizeof(IMAGE_OPTIONAL_HEADER32);
std::memcpy(b.data() + nt + 4, &f, sizeof(f)); std::memcpy(b.data() + nt + 4, &f, sizeof(f));
IMAGE_OPTIONAL_HEADER64 h64 = {}; IMAGE_OPTIONAL_HEADER64 h64 = {};
IMAGE_OPTIONAL_HEADER32 h32 = {}; IMAGE_OPTIONAL_HEADER32 h32 = {};
h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC; h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16; 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; h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384;
if (certificate) if (certificate)
{ {
h64.DataDirectory[4] = h32.DataDirectory[4] = { nt + 896, 32 }; h64.DataDirectory[4] = h32.DataDirectory[4] = { nt + 896, 32 };
ULONG length = 32; ULONG length = 32;
std::memcpy(b.data() + nt + 896, &length, sizeof(length)); std::memcpy(b.data() + nt + 896, &length, sizeof(length));
USHORT revision = 0x0200, 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); std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader);
IMAGE_SECTION_HEADER section = {}; 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; section.PointerToRawData = nt + 384; section.SizeOfRawData = 512;
std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, &section, sizeof(section)); std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, &section, sizeof(section));
for (ULONG i = nt + 384; i < nt + 896; ++i) b[i] = (UCHAR)(i * 31); for (ULONG i = nt + 384; i < nt + 896; ++i) b[i] = (UCHAR)(i * 31);
@@ -187,10 +244,295 @@ static void GuardedInvalid(const std::vector<UCHAR>& b, NTSTATUS expected)
UCHAR* input = region + span - b.size(); UCHAR* input = region + span - b.size();
std::memcpy(input, b.data(), b.size()); std::memcpy(input, b.data(), b.size());
Digest(input, (ULONG)b.size(), expected); Digest(input, (ULONG)b.size(), expected);
ULONG strictSize = 0, 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); VirtualFree(region, 0, MEM_RELEASE);
} }
static std::vector<UCHAR> ExplicitDigest(const std::vector<UCHAR>& b, UINT32 calg,
NTSTATUS expected = STATUS_SUCCESS)
{
ULONG length = 0xdead, 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<UCHAR> 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<UCHAR> SipDigest(const std::vector<UCHAR>& b, UINT32 calg)
{
WCHAR tempPath[MAX_PATH], 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<char*>(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<UCHAR> buffer(size);
auto indirect = (SIP_INDIRECT_DATA*)buffer.data();
Check(CryptSIPCreateIndirectData(&info, &size, indirect) != FALSE, "Windows SIP digest");
std::vector<UCHAR> result(indirect->Digest.pbData, indirect->Digest.pbData + indirect->Digest.cbData);
CloseHandle(file);
return result;
}
static std::vector<UCHAR> Der(UCHAR tag, const std::vector<UCHAR>& value)
{
std::vector<UCHAR> 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<UCHAR> Join(std::initializer_list<std::vector<UCHAR>> values)
{
std::vector<UCHAR> out;
for (auto& value : values) out.insert(out.end(), value.begin(), value.end());
return out;
}
static std::vector<UCHAR> SignedContent(UINT32 calg, bool nullParameters = true)
{
std::vector<UCHAR> oid = calg == CALG_SHA1 ? std::vector<UCHAR>{0x2B,0x0E,0x03,0x02,0x1A} :
std::vector<UCHAR>{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<UCHAR>{}}));
auto digest = Der(0x30, Join({algorithm, Der(0x04, std::vector<UCHAR>(
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<UINT32>{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 = {}, 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<UINT32>{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<UCHAR> 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<UINT32>{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<UCHAR> 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() int main()
{ {
SystemImageTests();
ExtendedTests();
for (bool pe64 : {false, true}) for (bool pe64 : {false, true})
{ {
for (bool certificate : {false, true}) for (bool certificate : {false, true})
@@ -255,5 +597,43 @@ int main()
Check(NT_SUCCESS(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, Check(NT_SUCCESS(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory,
&policy, &signing, &timestamp)), "complete certificate read accepted"); &policy, &signing, &timestamp)), "complete certificate read accepted");
Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once"); Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once");
std::printf("PASS: %u checks (guarded malformed inputs, PE32/PE32+, CNG digests, failure cleanup, short reads).\n", checks); // 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, &timestamp, 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(&timestamp);
Check(allocations == 0, "final results freed once");
source = 123;
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA256,
&directory, &policy, &signing, &timestamp, 0, FALSE, &source) == STATUS_INVALID_PARAMETER && !source,
"algorithm and length mismatch rejected");
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA384,
&directory, &policy, &signing, &timestamp, 0, FALSE, &source) == STATUS_NOT_SUPPORTED,
"legacy catalog algorithm capability explicit");
observedFileSize = 1040;
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA1,
&directory, &policy, &signing, &timestamp, 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, &timestamp, 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(&timestamp);
Check(allocations == 0, "failed final policies freed once");
std::printf("PASS: %u digest/legacy checks (Windows SIP, Crypt32, guard pages, failures, ownership).\n", checks);
} }
+24
View File
@@ -0,0 +1,24 @@
// Compile with the WDK for supported 64-bit targets. No driver is linked/loaded.
#include <ntifs.h>
#include "Domito.MinCrypt.h"
#if NTDDI_VERSION >= NTDDI_WIN10
C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 48);
C_ASSERT(FIELD_OFFSET(MINCRYPT_POLICY_INFO, VerificationStatus) == 4);
C_ASSERT(FIELD_OFFSET(MINCRYPT_POLICY_INFO, ChainInfo) == 16);
C_ASSERT(FIELD_OFFSET(MINCRYPT_POLICY_INFO, NotBefore) == 32);
C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 72);
C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, ChainElements) == 40);
C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, AuthenticodeAttributes) == 56);
#else
C_ASSERT(sizeof(MINCRYPT_POLICY_INFO) == 32);
C_ASSERT(sizeof(MINCRYPT_CHAIN_INFO) == 40);
#endif
C_ASSERT(sizeof(MINCRYPT_NAME_BLOB) == 16);
C_ASSERT(sizeof(MINCRYPT_CHAIN_ELEMENT) == 120);
C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_ELEMENT, Certificate) == 104);
#if defined(_M_AMD64)
// These opt-in declarations establish observations only for the named x64 build.
#include "Domito.MinCrypt.26100.h"
#endif
+18
View File
@@ -0,0 +1,18 @@
"""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)
+1 -1
View File
@@ -30,5 +30,5 @@ with tempfile.TemporaryDirectory(prefix='domito-digest-tests-') as temp:
test_source = Path(__file__).with_name('digest-tests.cpp') test_source = Path(__file__).with_name('digest-tests.cpp')
subprocess.run(['cl', '/nologo', '/EHsc', '/W4', '/WX', '/std:c++17', subprocess.run(['cl', '/nologo', '/EHsc', '/W4', '/WX', '/std:c++17',
'/I' + str(build), str(test_source), '/Fe' + str(build/'digest-tests.exe'), '/I' + str(build), str(test_source), '/Fe' + str(build/'digest-tests.exe'),
'/Fo' + str(build/'digest-tests.obj'), '/link', 'bcrypt.lib'], cwd=build, check=True) '/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) subprocess.run([str(build/'digest-tests.exe')], cwd=build, check=True)