Add explicit Authenticode workflows and correct CI helper contracts
This commit is contained in:
1 parent
1026c48324
commit
7aa23f50be
18 files changed
+1948
-181
No files matched your search
@@ -1,5 +1,10 @@
|
||||
# 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
|
||||
|
||||
- CiCheckSignedFile
|
||||
|
||||
@@ -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 😉
|
||||
- Compatibility with every Windows 10 version.
|
||||
- APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞
|
||||
- **Caution:** this claim comes with an asterisk though; due to PatchGuard (or some other security mechanism) I couldn't get run-time dynamic linking for `ci.dll` exports to work, so you need to actively avoid implementing code depending on exports that do not exist on earlier versions of Windows. Once I have a compatibility matrix for all of them I'll update the documentation accordingly.
|
||||
- CI entry points are currently static imports from `ci.lib`; consumers must avoid imports unavailable on their target Windows builds. 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
|
||||
- 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.
|
||||
@@ -73,6 +73,10 @@ Copy the provided `Domito.props` file into the directory of your `.vcxproj` file
|
||||
|
||||
## Code Integrity API notes
|
||||
|
||||
- [Algorithm-explicit Authenticode hashing and legacy catalog policy](docs/authenticode-digests.md)
|
||||
- [Mincrypt contracts, lifetimes, flags and opt-in x64 26100 layouts](docs/mincrypt-contracts.md)
|
||||
- [Implemented corrections and remaining VM/binary work](docs/implementation-status.md)
|
||||
|
||||
- [CiValidateFileObject flag 0x20: evidence, limitations, and kernel test procedure](docs/ci-validate-file-object.md)
|
||||
|
||||
## Sources & 3rd party credits
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -277,6 +277,8 @@ MINCRYPT_DIGEST_BLOB, * PMINCRYPT_DIGEST_BLOB,
|
||||
MINCRYPT_DER_BLOB, * PMINCRYPT_DER_BLOB,
|
||||
MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB;
|
||||
|
||||
// Encoded, length-delimited name bytes; not necessarily ANSI or NUL-terminated.
|
||||
// The observed 26100 x64 string tag occupies padding; see the opt-in version header.
|
||||
typedef struct _MINCRYPT_NAME_BLOB
|
||||
{
|
||||
_Field_size_bytes_(Size) PCHAR Data;
|
||||
@@ -287,6 +289,8 @@ typedef struct _MINCRYPT_NAME_BLOB
|
||||
#define MINCRYPT_MAX_HASH_LENGTH (64)
|
||||
#define MINCRYPT_SHA1_LENGTH (160/8)
|
||||
#define MINCRYPT_SHA256_LENGTH (256/8)
|
||||
#define MINCRYPT_SHA384_LENGTH (384/8)
|
||||
#define MINCRYPT_SHA512_LENGTH (512/8)
|
||||
|
||||
typedef struct _MINCRYPT_CHAIN_ELEMENT
|
||||
{
|
||||
@@ -301,9 +305,11 @@ typedef struct _MINCRYPT_CHAIN_ELEMENT
|
||||
|
||||
}MINCRYPT_CHAIN_ELEMENT, * PMINCRYPT_CHAIN_ELEMENT;
|
||||
|
||||
// CI owns one chain allocation; nested pointers borrow that allocation.
|
||||
// Do not free child blobs separately or use memcpy to create a second owner.
|
||||
typedef struct _MINCRYPT_CHAIN_INFO
|
||||
{
|
||||
UINT32 Size;
|
||||
UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data.
|
||||
|
||||
MINCERT_BLOB* PublicKeys;
|
||||
UINT32 NumberOfPublicKeys;
|
||||
@@ -319,7 +325,9 @@ typedef struct _MINCRYPT_CHAIN_INFO
|
||||
// ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc.
|
||||
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
|
||||
|
||||
@@ -338,7 +346,7 @@ typedef struct _MINCRYPT_CHAIN_INFO
|
||||
typedef struct _MINCRYPT_POLICY_INFO
|
||||
{
|
||||
UINT32 Size;
|
||||
UINT32 VerificationStatus;
|
||||
NTSTATUS VerificationStatus; // Check API return status separately.
|
||||
UINT32 PolicyBits;
|
||||
|
||||
MINCRYPT_CHAIN_INFO* ChainInfo;
|
||||
@@ -347,8 +355,8 @@ typedef struct _MINCRYPT_POLICY_INFO
|
||||
|
||||
#if (NTDDI_VERSION >= NTDDI_WIN10)
|
||||
|
||||
LARGE_INTEGER NotBefore; // The certificate is not valid before this time
|
||||
LARGE_INTEGER NotAfter; // The certificate is not valid after this time
|
||||
LARGE_INTEGER NotBefore; // Observed: signer certificate validity start.
|
||||
LARGE_INTEGER NotAfter; // Observed: signer certificate validity end.
|
||||
|
||||
#endif // NTDDI_VERSION >= NTDDI_WIN10
|
||||
|
||||
@@ -358,12 +366,16 @@ typedef struct _MINCRYPT_POLICY_INFO
|
||||
|
||||
|
||||
/**
|
||||
* Resets a PolicyInfo struct - frees the dynamically allocated buffer in PolicyInfo (ChainInfo) if not null.
|
||||
* Zeros the entire PolicyInfo struct.
|
||||
* Release a known initialized policy's owned ChainInfo allocation and reset it.
|
||||
* Zero-initialize policy objects before first use. Release both signer and TSA
|
||||
* policies after use, including failures, and before reusing output storage.
|
||||
* Borrowed child pointers become invalid when the owner is released. Never clear
|
||||
* Size while a chain is still owned. Size does not negotiate the runtime layout.
|
||||
*
|
||||
* @param PolicyInfo - the struct to reset.
|
||||
* @param PolicyInfo - initialized policy, not arbitrary uninitialized storage.
|
||||
*
|
||||
* @return the struct which was reset.
|
||||
* Ignore the return value. The legacy PVOID declaration is retained for source
|
||||
* compatibility; observed 26100 x64 code provides no defined pointer return.
|
||||
*/
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
MINCRYPTAPI
|
||||
@@ -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
|
||||
* the certificates that was used for signing (the entire certificate chain)
|
||||
*
|
||||
* @param Hash - buffer containing the digest
|
||||
* @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
|
||||
*
|
||||
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
|
||||
*
|
||||
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
|
||||
*
|
||||
* @param SecurityDirectory - pointer to the start of the security directory
|
||||
* @param SecurityDirectory - WIN_CERTIFICATE header, not the PKCS#7 payload.
|
||||
* Observed 26100 x64 checks 4-byte pointer alignment, revision 0x200,
|
||||
* type 2, supplied size >=12 and 8 < dwLength <= supplied size.
|
||||
* This wrapper selects the first entry; it does not enumerate the table.
|
||||
*
|
||||
* @param SizeOfSecurityDirectory - size the security directory
|
||||
*
|
||||
@@ -396,9 +413,12 @@ CiFreePolicyInfo(
|
||||
*
|
||||
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain
|
||||
*
|
||||
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified.
|
||||
* @return Overall verification NTSTATUS. A successful policy VerificationStatus
|
||||
* does not replace this return value: the final file-digest comparison
|
||||
* can still fail. Both policy outputs may carry diagnostics on failure.
|
||||
* See docs/mincrypt-contracts.md for ownership and policy limits.
|
||||
* Various error values otherwise, for example:
|
||||
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
|
||||
* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
|
||||
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
||||
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
||||
*/
|
||||
@@ -407,10 +427,10 @@ MINCRYPTAPI
|
||||
NTSTATUS
|
||||
NTAPI
|
||||
CiCheckSignedFile(
|
||||
_In_ PVOID Hash,
|
||||
_In_reads_bytes_(HashSize) PVOID Hash,
|
||||
_In_ UINT32 HashSize,
|
||||
_In_ ALG_ID HashAlgId,
|
||||
_In_ PVOID SecurityDirectory,
|
||||
_In_reads_bytes_(SizeOfSecurityDirectory) PVOID SecurityDirectory,
|
||||
_In_ UINT32 SizeOfSecurityDirectory,
|
||||
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||
_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!
|
||||
*
|
||||
* @param Hash - buffer containing the digest
|
||||
* @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
|
||||
*
|
||||
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
|
||||
*
|
||||
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
|
||||
*
|
||||
* @param IsReloadCatalogs - is reload catalogs cache.
|
||||
* @param ReloadCatalogs - request catalog-store reload; process/silo and cache state matter.
|
||||
*
|
||||
* @param Always0 - this is for IsReloadCatalogs, Always0 != 0 ? 16 : 24;
|
||||
* @param RequireTrustedCatalog - nonzero requires the observed catalog TRUSTED
|
||||
* extension marker. This does not itself establish Microsoft signer trust.
|
||||
*
|
||||
* @param Always2007F - unknown, always 0x2007F, maybe a mask.
|
||||
* @param AcceptedCatalogPolicyBits - candidate catalog policy-bit filter.
|
||||
* Observed 26100 x64 requires ANY overlap with catalog PolicyBits, not
|
||||
* every requested bit. This is not a flags-to-enable input.
|
||||
*
|
||||
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
|
||||
*
|
||||
* @param CatalogName[out option] - catalog file name.
|
||||
* @param CatalogName[out option] - initialize empty; observed 26100 x64 returns
|
||||
* a separate duplicated string allocation. Release a returned Buffer
|
||||
* with RtlFreeUnicodeString, separately from both policy outputs.
|
||||
*
|
||||
* @param SigningTime[out] - when the file was signed (FILETIME format)
|
||||
*
|
||||
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain.
|
||||
*
|
||||
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified.
|
||||
* @return Overall verification NTSTATUS. A successful policy VerificationStatus
|
||||
* does not replace this return value. Catalog selection and the
|
||||
* selected policy still apply. Outputs may carry diagnostics on failure.
|
||||
* See docs/mincrypt-contracts.md for ownership and policy limits.
|
||||
* Various error values otherwise, for example:
|
||||
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
|
||||
* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
|
||||
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
||||
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
||||
*/
|
||||
@@ -456,12 +491,12 @@ MINCRYPTAPI
|
||||
NTSTATUS
|
||||
NTAPI
|
||||
CiVerifyHashInCatalog(
|
||||
_In_ PVOID Hash,
|
||||
_In_reads_bytes_(HashSize) PVOID Hash,
|
||||
_In_ UINT32 HashSize,
|
||||
_In_ ALG_ID HashAlgId,
|
||||
_In_ BOOLEAN IsReloadCatalogs,
|
||||
_In_ UINT32 Always0,
|
||||
_In_ UINT32 Always2007F,
|
||||
_In_ BOOLEAN ReloadCatalogs,
|
||||
_In_ UINT32 RequireTrustedCatalog,
|
||||
_In_ UINT32 AcceptedCatalogPolicyBits,
|
||||
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||
_Out_opt_ UNICODE_STRING* CatalogName,
|
||||
_Out_ LARGE_INTEGER* SigningTime,
|
||||
@@ -552,7 +587,7 @@ CiSetTrustedOriginClaimId(
|
||||
* @return STATUS_SUCCESS when verification succeeds under the selected CI policy.
|
||||
* This does not by itself establish the application's signer trust policy.
|
||||
* Various error values otherwise, for example:
|
||||
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
|
||||
* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
|
||||
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
||||
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
||||
*/
|
||||
@@ -562,12 +597,12 @@ NTSTATUS
|
||||
NTAPI
|
||||
CiValidateFileObject(
|
||||
_In_ FILE_OBJECT* FileObject,
|
||||
_In_opt_ UINT32 ValidationFlags,
|
||||
_In_opt_ UINT32 RequestedSigningLevel,
|
||||
_In_ UINT32 ValidationFlags,
|
||||
_In_ UINT32 RequestedSigningLevel,
|
||||
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
|
||||
_Out_ LARGE_INTEGER* SigningTime,
|
||||
_Out_ UINT8* Hash,
|
||||
_Out_writes_bytes_(*HashSize) UINT8* Hash,
|
||||
_Inout_ UINT32* HashSize,
|
||||
_Out_ ALG_ID* HashAlgId
|
||||
);
|
||||
|
||||
+96
-3
@@ -184,6 +184,43 @@ DomitoGetPortableExecutableDigestKind(
|
||||
_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.
|
||||
// Validates accessed header/certificate ranges and clears outputs on failure
|
||||
@@ -217,8 +254,40 @@ DomitoFreePortableExecutableDigest(
|
||||
_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)
|
||||
_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)
|
||||
_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)
|
||||
_Must_inspect_result_
|
||||
|
||||
+378
-48
@@ -89,11 +89,252 @@ DomitoGetPortableExecutableDigestKind(
|
||||
return CALG_SHA1;
|
||||
}
|
||||
|
||||
// Bounded DER navigation for the outer Authenticode content only. This does not
|
||||
// validate certificates, signerInfos, nested signatures, or cryptographic trust.
|
||||
typedef struct _DOMITO_DER_VIEW
|
||||
{
|
||||
const UCHAR* Bytes;
|
||||
ULONG Length;
|
||||
} DOMITO_DER_VIEW;
|
||||
|
||||
static BOOLEAN DomitoDerTake(DOMITO_DER_VIEW* Input, UCHAR Tag, DOMITO_DER_VIEW* Value)
|
||||
{
|
||||
if (Input->Length < 2 || Input->Bytes[0] != Tag)
|
||||
return FALSE;
|
||||
ULONG length = Input->Bytes[1];
|
||||
ULONG header = 2;
|
||||
if (length & 0x80)
|
||||
{
|
||||
const ULONG count = length & 0x7F;
|
||||
if (!count || count > sizeof(ULONG) || count > Input->Length - header ||
|
||||
Input->Bytes[header] == 0)
|
||||
return FALSE; // No indefinite or nonminimal DER lengths.
|
||||
length = 0;
|
||||
for (ULONG i = 0; i < count; ++i)
|
||||
length = (length << 8) | Input->Bytes[header++];
|
||||
if (length < 0x80)
|
||||
return FALSE;
|
||||
}
|
||||
if (length > Input->Length - header)
|
||||
return FALSE;
|
||||
Value->Bytes = Input->Bytes + header;
|
||||
Value->Length = length;
|
||||
Input->Bytes += header + length;
|
||||
Input->Length -= header + length;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOLEAN DomitoDerOid(DOMITO_DER_VIEW* Input, const UCHAR* Oid, ULONG Size)
|
||||
{
|
||||
DOMITO_DER_VIEW value;
|
||||
return DomitoDerTake(Input, 0x06, &value) && value.Length == Size &&
|
||||
memcmp(value.Bytes, Oid, Size) == 0;
|
||||
}
|
||||
|
||||
_IRQL_requires_max_(DISPATCH_LEVEL)
|
||||
NTSTATUS
|
||||
DomitoGetPortableExecutableDigestKindEx(
|
||||
_In_reads_bytes_(PeSize) const UCHAR* PeBytes,
|
||||
_In_ ULONG PeSize,
|
||||
_In_reads_(IMAGE_DIRECTORY_ENTRY_SECURITY + 1) const IMAGE_DATA_DIRECTORY* Directories,
|
||||
_Out_ PUINT32 DigestCalg)
|
||||
{
|
||||
if (!DigestCalg)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
*DigestCalg = 0;
|
||||
if (!PeBytes || !Directories)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
const IMAGE_DATA_DIRECTORY security = Directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
|
||||
if (!security.VirtualAddress && !security.Size)
|
||||
return STATUS_NOT_FOUND;
|
||||
if (!security.VirtualAddress || (security.VirtualAddress & 7) ||
|
||||
security.VirtualAddress > PeSize || security.Size > PeSize - security.VirtualAddress ||
|
||||
security.Size < 8)
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
ULONG length;
|
||||
USHORT revision, 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(§ion, Bytes + table + i * sizeof(section), sizeof(section));
|
||||
if (!section.SizeOfRawData)
|
||||
continue;
|
||||
if (section.PointerToRawData < headers || section.PointerToRawData > Size ||
|
||||
section.SizeOfRawData > Size - section.PointerToRawData)
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
ULONG at = used;
|
||||
while (at && sections[at - 1].Offset > section.PointerToRawData)
|
||||
{
|
||||
sections[at] = sections[at - 1];
|
||||
--at;
|
||||
}
|
||||
sections[at].Offset = section.PointerToRawData;
|
||||
sections[at].Length = section.SizeOfRawData;
|
||||
++used;
|
||||
}
|
||||
ULONG end = headers;
|
||||
for (ULONG i = 0; i < used; ++i)
|
||||
{
|
||||
if (sections[i].Offset != end)
|
||||
return STATUS_NOT_SUPPORTED;
|
||||
end += sections[i].Length;
|
||||
}
|
||||
if (count > IMAGE_DIRECTORY_ENTRY_SECURITY)
|
||||
{
|
||||
IMAGE_DATA_DIRECTORY security;
|
||||
memcpy(&security, Bytes + optional + directory +
|
||||
IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(security), sizeof(security));
|
||||
if (security.VirtualAddress || security.Size)
|
||||
{
|
||||
if (!security.VirtualAddress || !security.Size || (security.VirtualAddress & 7) ||
|
||||
security.VirtualAddress < end || security.VirtualAddress > Size ||
|
||||
security.Size != Size - security.VirtualAddress)
|
||||
return STATUS_NOT_SUPPORTED;
|
||||
// Validate every aligned certificate entry, not just the first.
|
||||
ULONG offset = security.VirtualAddress;
|
||||
while (offset < Size)
|
||||
{
|
||||
if (Size - offset < 8)
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
ULONG length;
|
||||
USHORT revision, type;
|
||||
memcpy(&length, Bytes + offset, 4);
|
||||
memcpy(&revision, Bytes + offset + 4, 2);
|
||||
memcpy(&type, Bytes + offset + 6, 2);
|
||||
if (length <= 8 || length > Size - offset)
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA)
|
||||
return STATUS_NOT_SUPPORTED;
|
||||
const ULONG padding = (8 - (length & 7)) & 7;
|
||||
if (padding > Size - offset - length)
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
offset += length + padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
_Success_(return == STATUS_SUCCESS)
|
||||
_Must_inspect_result_
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
NTSTATUS
|
||||
DomitoCalculatePortableExecutableDigest(
|
||||
static NTSTATUS
|
||||
DomitoCalculatePortableExecutableDigestInternal(
|
||||
_In_ UINT32 ExplicitCalg,
|
||||
_In_ PUCHAR pPeBytes,
|
||||
_In_ ULONG PeSize,
|
||||
_Out_ PUINT32 pDigestCalgOut,
|
||||
@@ -231,7 +472,8 @@ DomitoCalculatePortableExecutableDigest(
|
||||
BCRYPT_HASH_HANDLE hbHash = nullptr;
|
||||
ULONG hashLength = 0;
|
||||
ULONG resultLength = 0;
|
||||
const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories);
|
||||
const UINT32 peDigestKind = ExplicitCalg ? ExplicitCalg :
|
||||
DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories);
|
||||
NTSTATUS status = BCryptOpenAlgorithmProvider(
|
||||
&hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0);
|
||||
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)
|
||||
void
|
||||
DomitoFreePortableExecutableDigest(
|
||||
@@ -333,8 +616,8 @@ _Success_(return == STATUS_SUCCESS)
|
||||
_Must_inspect_result_
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
#pragma code_seg("PAGED")
|
||||
NTSTATUS
|
||||
DomitoValidateFileLegacyMode(
|
||||
static NTSTATUS
|
||||
DomitoValidateFileLegacyModeInternal(
|
||||
_In_ HANDLE FileHandle,
|
||||
_In_ PVOID Hash,
|
||||
_In_ UINT32 HashSize,
|
||||
@@ -342,11 +625,20 @@ DomitoValidateFileLegacyMode(
|
||||
_In_ const IMAGE_DATA_DIRECTORY * SecurityDirectory,
|
||||
_Inout_ MINCRYPT_POLICY_INFO * PolicyInfo,
|
||||
_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();
|
||||
|
||||
if (!ValidationSource)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
*ValidationSource = 0;
|
||||
if (!Hash || !SecurityDirectory || !PolicyInfo || !SigningTime ||
|
||||
!TimeStampPolicyInfo || PolicyInfo == TimeStampPolicyInfo)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
NTSTATUS status = STATUS_SUCCESS;
|
||||
PVOID certDirectory = nullptr;
|
||||
KAPC_STATE systemContext = {};
|
||||
@@ -358,15 +650,48 @@ DomitoValidateFileLegacyMode(
|
||||
CiFreePolicyInfo(PolicyInfo);
|
||||
CiFreePolicyInfo(TimeStampPolicyInfo);
|
||||
|
||||
if (HashSize != MINCRYPT_SHA1_LENGTH && HashSize != MINCRYPT_SHA256_LENGTH)
|
||||
if (HashAlgId != CALG_SHA1 && HashAlgId != CALG_SHA256)
|
||||
{
|
||||
status = STATUS_INVALID_IMAGE_HASH;
|
||||
status = STATUS_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
if (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;
|
||||
}
|
||||
|
||||
if (SecurityDirectory->Size != 0u &&
|
||||
SecurityDirectory->VirtualAddress != 0u)
|
||||
{
|
||||
if ((SecurityDirectory->VirtualAddress & 7) || SecurityDirectory->Size < 12)
|
||||
{
|
||||
status = STATUS_INVALID_IMAGE_FORMAT;
|
||||
break;
|
||||
}
|
||||
// Bound the allocation/read against the file before trusting its table.
|
||||
PFILE_OBJECT fileObject = nullptr;
|
||||
status = ObReferenceObjectByHandle(FileHandle, FILE_READ_DATA, *IoFileObjectType,
|
||||
KernelMode, (PVOID*)&fileObject, nullptr);
|
||||
if (!NT_SUCCESS(status))
|
||||
break;
|
||||
LARGE_INTEGER fileSize = {};
|
||||
status = FsRtlGetFileSize(fileObject, &fileSize);
|
||||
ObDereferenceObject(fileObject);
|
||||
if (!NT_SUCCESS(status))
|
||||
break;
|
||||
if (fileSize.QuadPart < 0 ||
|
||||
(ULONGLONG)SecurityDirectory->VirtualAddress > (ULONGLONG)fileSize.QuadPart ||
|
||||
SecurityDirectory->Size > (ULONGLONG)fileSize.QuadPart - SecurityDirectory->VirtualAddress)
|
||||
{
|
||||
status = STATUS_INVALID_IMAGE_FORMAT;
|
||||
break;
|
||||
}
|
||||
#pragma warning(disable:4996)
|
||||
certDirectory = ExAllocatePoolWithTag(
|
||||
PagedPool,
|
||||
@@ -381,44 +706,10 @@ DomitoValidateFileLegacyMode(
|
||||
}
|
||||
|
||||
LARGE_INTEGER offset = {};
|
||||
IO_STATUS_BLOCK ioStatusBlock = {};
|
||||
|
||||
offset.LowPart = SecurityDirectory->VirtualAddress;
|
||||
|
||||
status = ZwReadFile(
|
||||
FileHandle,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&ioStatusBlock,
|
||||
certDirectory,
|
||||
SecurityDirectory->Size,
|
||||
&offset,
|
||||
NULL
|
||||
);
|
||||
|
||||
if (status == STATUS_PENDING)
|
||||
{
|
||||
ZwWaitForSingleObject(
|
||||
FileHandle,
|
||||
FALSE,
|
||||
NULL
|
||||
);
|
||||
|
||||
MemoryBarrier();
|
||||
status = ioStatusBlock.Status;
|
||||
}
|
||||
|
||||
offset.QuadPart = SecurityDirectory->VirtualAddress;
|
||||
status = DomitoReadFileAt(FileHandle, certDirectory, SecurityDirectory->Size, &offset);
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (ioStatusBlock.Information != SecurityDirectory->Size)
|
||||
{
|
||||
status = STATUS_END_OF_FILE;
|
||||
break;
|
||||
}
|
||||
|
||||
KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
|
||||
{
|
||||
@@ -437,6 +728,7 @@ DomitoValidateFileLegacyMode(
|
||||
|
||||
if (NT_SUCCESS(status))
|
||||
{
|
||||
*ValidationSource = DOMITO_CI_SOURCE_EMBEDDED;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -446,6 +738,9 @@ DomitoValidateFileLegacyMode(
|
||||
}
|
||||
}
|
||||
|
||||
CiFreePolicyInfo(PolicyInfo);
|
||||
CiFreePolicyInfo(TimeStampPolicyInfo);
|
||||
SigningTime->QuadPart = 0;
|
||||
KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
|
||||
{
|
||||
status = CiVerifyHashInCatalog(
|
||||
@@ -453,8 +748,8 @@ DomitoValidateFileLegacyMode(
|
||||
HashSize,
|
||||
HashAlgId,
|
||||
FALSE,
|
||||
0,
|
||||
0x2007F,
|
||||
RequireTrustedCatalog,
|
||||
AcceptedCatalogPolicyBits,
|
||||
PolicyInfo,
|
||||
NULL,
|
||||
SigningTime,
|
||||
@@ -463,13 +758,16 @@ DomitoValidateFileLegacyMode(
|
||||
|
||||
if (status == STATUS_INVALID_IMAGE_HASH)
|
||||
{
|
||||
CiFreePolicyInfo(PolicyInfo);
|
||||
CiFreePolicyInfo(TimeStampPolicyInfo);
|
||||
SigningTime->QuadPart = 0;
|
||||
status = CiVerifyHashInCatalog(
|
||||
Hash,
|
||||
HashSize,
|
||||
HashAlgId,
|
||||
TRUE,
|
||||
0,
|
||||
0x2007F,
|
||||
RequireTrustedCatalog,
|
||||
AcceptedCatalogPolicyBits,
|
||||
PolicyInfo,
|
||||
NULL,
|
||||
SigningTime,
|
||||
@@ -478,6 +776,8 @@ DomitoValidateFileLegacyMode(
|
||||
}
|
||||
}
|
||||
KeUnstackDetachProcess(&systemContext);
|
||||
if (NT_SUCCESS(status))
|
||||
*ValidationSource = DOMITO_CI_SOURCE_CATALOG;
|
||||
|
||||
} while (FALSE);
|
||||
|
||||
@@ -488,4 +788,34 @@ DomitoValidateFileLegacyMode(
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
_Success_(return == STATUS_SUCCESS)
|
||||
_Must_inspect_result_
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
NTSTATUS
|
||||
DomitoValidateFileLegacyMode(
|
||||
HANDLE FileHandle, PVOID Hash, UINT32 HashSize, ALG_ID HashAlgId,
|
||||
const IMAGE_DATA_DIRECTORY* SecurityDirectory, MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||
LARGE_INTEGER* SigningTime, MINCRYPT_POLICY_INFO* TimeStampPolicyInfo)
|
||||
{
|
||||
ULONG source;
|
||||
return DomitoValidateFileLegacyModeInternal(FileHandle, Hash, HashSize, HashAlgId,
|
||||
SecurityDirectory, PolicyInfo, SigningTime, TimeStampPolicyInfo,
|
||||
DOMITO_CI_LEGACY_CATALOG_POLICY_BITS, FALSE, &source);
|
||||
}
|
||||
|
||||
_Success_(return == STATUS_SUCCESS)
|
||||
_Must_inspect_result_
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
NTSTATUS
|
||||
DomitoValidateFileLegacyModeEx(
|
||||
HANDLE FileHandle, PVOID Hash, UINT32 HashSize, ALG_ID HashAlgId,
|
||||
const IMAGE_DATA_DIRECTORY* SecurityDirectory, MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||
LARGE_INTEGER* SigningTime, MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
|
||||
UINT32 AcceptedCatalogPolicyBits, BOOLEAN RequireTrustedCatalog, PULONG ValidationSource)
|
||||
{
|
||||
return DomitoValidateFileLegacyModeInternal(FileHandle, Hash, HashSize, HashAlgId,
|
||||
SecurityDirectory, PolicyInfo, SigningTime, TimeStampPolicyInfo,
|
||||
AcceptedCatalogPolicyBits, RequireTrustedCatalog, ValidationSource);
|
||||
}
|
||||
#pragma code_seg()
|
||||
+165
-64
@@ -129,6 +129,13 @@ DomitoFindModuleBaseAddress(
|
||||
return status;
|
||||
}
|
||||
|
||||
// This helper handles directory RVAs after the loader-mapped header establishes
|
||||
// SizeOfImage. ModuleBase is not an arbitrary file buffer (there is no size input).
|
||||
static BOOLEAN DomitoImageRvaSpan(ULONG ImageSize, ULONG Rva, ULONG Count, ULONG ElementSize)
|
||||
{
|
||||
return Rva <= ImageSize && ElementSize != 0 && Count <= (ImageSize - Rva) / ElementSize;
|
||||
}
|
||||
|
||||
_Success_(return == STATUS_SUCCESS)
|
||||
_Must_inspect_result_
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
@@ -139,56 +146,113 @@ DomitoFindExportedFunctionAddress(
|
||||
_Inout_opt_ PVOID * FunctionAddress
|
||||
)
|
||||
{
|
||||
NTSTATUS status = STATUS_NOT_FOUND;
|
||||
ULONG exportSize;
|
||||
|
||||
if (G_Common.RtlImageDirectoryEntryToData == NULL)
|
||||
if (FunctionAddress)
|
||||
{
|
||||
*FunctionAddress = nullptr;
|
||||
}
|
||||
if (!ModuleBase || !FunctionName || !FunctionName->Buffer || !FunctionName->Length)
|
||||
{
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
}
|
||||
if (!G_Common.RtlImageDirectoryEntryToData)
|
||||
{
|
||||
return STATUS_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
// Retrieve the export directory information
|
||||
const PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)G_Common.RtlImageDirectoryEntryToData(
|
||||
ModuleBase,
|
||||
TRUE,
|
||||
IMAGE_DIRECTORY_ENTRY_EXPORT,
|
||||
&exportSize
|
||||
);
|
||||
|
||||
if (exportDirectory == NULL)
|
||||
// Initial headers belong to a valid loaded module, as required by this API.
|
||||
const PUCHAR base = (PUCHAR)ModuleBase;
|
||||
IMAGE_DOS_HEADER dos;
|
||||
memcpy(&dos, base, sizeof(dos));
|
||||
if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < (LONG)sizeof(dos))
|
||||
{
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
STRING currentFunctionName;
|
||||
|
||||
const PULONG functionAddresses = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfFunctions);
|
||||
const PULONG functionNames = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNames);
|
||||
const PUSHORT functionOrdinals = (PUSHORT)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNameOrdinals);
|
||||
|
||||
for (ULONG i = 0; i < exportDirectory->NumberOfNames; i++)
|
||||
ULONG exportSize = 0;
|
||||
const PVOID directory = G_Common.RtlImageDirectoryEntryToData(
|
||||
ModuleBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize);
|
||||
const ULONG_PTR directoryOffset = (ULONG_PTR)directory - (ULONG_PTR)ModuleBase;
|
||||
if (!directory || directoryOffset > imageSize || exportSize < sizeof(IMAGE_EXPORT_DIRECTORY) ||
|
||||
exportSize > imageSize - directoryOffset)
|
||||
{
|
||||
const char* functionName = (const char*)((ULONG_PTR)ModuleBase + functionNames[i]);
|
||||
const USHORT functionOrdinal = functionOrdinals[i];
|
||||
UNREFERENCED_PARAMETER(functionOrdinal);
|
||||
|
||||
const ULONG functionRva = functionAddresses[i];
|
||||
const PVOID functionAddress = (PVOID)((ULONG_PTR)ModuleBase + functionRva);
|
||||
|
||||
RtlInitAnsiString(¤tFunctionName, functionName);
|
||||
|
||||
if (0 == RtlCompareString(FunctionName, ¤tFunctionName, TRUE))
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
}
|
||||
IMAGE_EXPORT_DIRECTORY exports;
|
||||
memcpy(&exports, directory, sizeof(exports));
|
||||
if (!DomitoImageRvaSpan(imageSize, exports.AddressOfFunctions, exports.NumberOfFunctions, sizeof(ULONG)) ||
|
||||
!DomitoImageRvaSpan(imageSize, exports.AddressOfNames, exports.NumberOfNames, sizeof(ULONG)) ||
|
||||
!DomitoImageRvaSpan(imageSize, exports.AddressOfNameOrdinals, exports.NumberOfNames, sizeof(USHORT)))
|
||||
{
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
}
|
||||
for (ULONG i = 0; i < exports.NumberOfNames; ++i)
|
||||
{
|
||||
ULONG nameRva;
|
||||
USHORT ordinal;
|
||||
memcpy(&nameRva, base + exports.AddressOfNames + i * sizeof(ULONG), sizeof(nameRva));
|
||||
memcpy(&ordinal, base + exports.AddressOfNameOrdinals + i * sizeof(USHORT), sizeof(ordinal));
|
||||
if (nameRva >= imageSize || ordinal >= exports.NumberOfFunctions)
|
||||
{
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
}
|
||||
ULONG length = 0;
|
||||
while (length < imageSize - nameRva && base[nameRva + length] != 0)
|
||||
{
|
||||
++length;
|
||||
}
|
||||
if (length == imageSize - nameRva || length > MAXUSHORT)
|
||||
{
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
}
|
||||
ANSI_STRING current;
|
||||
current.Buffer = (PCHAR)(base + nameRva);
|
||||
current.Length = (USHORT)length;
|
||||
current.MaximumLength = (USHORT)length;
|
||||
if (RtlCompareString(FunctionName, ¤t, TRUE) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ULONG functionRva;
|
||||
memcpy(&functionRva, base + exports.AddressOfFunctions + ordinal * sizeof(ULONG), sizeof(functionRva));
|
||||
if (!functionRva)
|
||||
{
|
||||
return STATUS_NOT_FOUND; // Empty EAT slot.
|
||||
}
|
||||
if (functionRva >= imageSize)
|
||||
{
|
||||
return STATUS_INVALID_IMAGE_FORMAT;
|
||||
}
|
||||
if (functionRva >= directoryOffset && functionRva - directoryOffset < exportSize)
|
||||
{
|
||||
return STATUS_NOT_SUPPORTED; // Forwarder string, not a callable address.
|
||||
}
|
||||
if (FunctionAddress)
|
||||
{
|
||||
status = STATUS_SUCCESS;
|
||||
*FunctionAddress = functionAddress;
|
||||
*FunctionAddress = base + functionRva;
|
||||
}
|
||||
break;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
return STATUS_NOT_FOUND;
|
||||
}
|
||||
|
||||
_Success_(return == STATUS_SUCCESS)
|
||||
@@ -240,6 +304,68 @@ DomitoMemorySearchPattern(
|
||||
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)
|
||||
_Must_inspect_result_
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
@@ -250,33 +376,8 @@ DomitoReadFile(
|
||||
_In_ ULONG BufferSize
|
||||
)
|
||||
{
|
||||
NTSTATUS status = STATUS_SUCCESS;
|
||||
IO_STATUS_BLOCK ioStatusBlock;
|
||||
|
||||
// Read the file into memory using ZwReadFile
|
||||
if (!NT_SUCCESS(status = ZwReadFile(
|
||||
FileHandle,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
&ioStatusBlock,
|
||||
Buffer,
|
||||
BufferSize,
|
||||
NULL,
|
||||
NULL
|
||||
)))
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
// Check if the file was read successfully
|
||||
if (!NT_SUCCESS(ioStatusBlock.Status))
|
||||
{
|
||||
return ioStatusBlock.Status;
|
||||
}
|
||||
|
||||
// File read successfully
|
||||
return status;
|
||||
const LARGE_INTEGER offset = {};
|
||||
return DomitoReadFileAt(FileHandle, Buffer, BufferSize, &offset);
|
||||
}
|
||||
|
||||
_Success_(return == STATUS_SUCCESS)
|
||||
@@ -306,7 +407,7 @@ DomitoGetProcessImageName(
|
||||
}
|
||||
|
||||
CLIENT_ID cid;
|
||||
cid.UniqueProcess = (HANDLE)ProcessId;
|
||||
cid.UniqueProcess = ULongToHandle(ProcessId);
|
||||
cid.UniqueThread = NULL;
|
||||
|
||||
OBJECT_ATTRIBUTES objAttr;
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\include\Domito.MinCrypt.h" />
|
||||
<ClInclude Include="..\include\Domito.MinCrypt.26100.h" />
|
||||
<ClInclude Include="..\include\Domito.h" />
|
||||
<ClInclude Include="Domito.Internal.h" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
<ClInclude Include="..\include\Domito.h">
|
||||
<Filter>Header Files\Public</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\include\Domito.MinCrypt.26100.h">
|
||||
<Filter>Header Files\Public</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\include\Domito.MinCrypt.h">
|
||||
<Filter>Header Files\Public</Filter>
|
||||
</ClInclude>
|
||||
|
||||
+49
-17
@@ -1,30 +1,62 @@
|
||||
# Code Integrity regression tests
|
||||
# Regression tests
|
||||
|
||||
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
|
||||
|
||||
```powershell
|
||||
python tests\run-digest-tests.py
|
||||
python tests\run-common-tests.py
|
||||
python tests\run-ci-probe-tests.py
|
||||
```
|
||||
|
||||
The runner extracts the production digest, pattern-search, algorithm-mapping,
|
||||
and legacy-validation functions without changing their bodies. The C++ harness
|
||||
uses real user-mode BCrypt hashing and test doubles for pool allocation, file
|
||||
reads, and CI. No driver is installed or loaded. Build outputs are temporary.
|
||||
Build outputs are temporary. No driver is installed or loaded. Test runners
|
||||
extract the actual production functions or include the probe body unchanged;
|
||||
kernel calls, CI calls, and pool allocation are replaced with test doubles.
|
||||
The digest harness uses real BCrypt, Crypt32 and the Windows PE SIP provider.
|
||||
|
||||
Coverage includes PE32/PE32+ digest fixtures, checksum and certificate exclusions,
|
||||
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.
|
||||
## Digest and legacy verification tests
|
||||
|
||||
The tests do not validate CI trust policy, asynchronous kernel I/O behavior, or
|
||||
general Authenticode hashing of unusual section layouts. The digest implementation
|
||||
still uses its existing linear byte-order convention and heuristic signature
|
||||
algorithm detection; replacing those requires a separate compatibility corpus.
|
||||
Coverage includes PE32/PE32+ files, all four supported hash algorithms, checksum
|
||||
and certificate exclusions, overlays, reverse section-header order, multiple
|
||||
certificate entries, certificate padding, unsupported section layouts, bounded
|
||||
DER algorithm discovery, SHA-1 pattern decoys, truncated ASN.1, explicit catalog
|
||||
policy forwarding and cleanup between failed/successful CI attempts.
|
||||
|
||||
The test compares digests against `CryptSIPCreateIndirectData` and reads the
|
||||
installed system `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` as a
|
||||
real-file corpus. For embedded signatures, Crypt32 independently decodes the SPC
|
||||
algorithm and expected file digest. These checks do not verify signature trust.
|
||||
|
||||
Guard pages cover truncated headers and invalid ranges in both compatibility
|
||||
and strict paths. Fault injection covers allocation/hash failures, deterministic
|
||||
outputs, short certificate reads and digest ownership. A DOS stub larger than
|
||||
4 MiB exercises the removed scratch-buffer limit. Older entry points retain
|
||||
legacy selection/layout behavior; strict Ex APIs enforce their documented subset.
|
||||
|
||||
## File and export helper tests
|
||||
|
||||
The common harness checks explicit and zero offsets, exact/short/failed reads,
|
||||
pending completion, event/object cleanup, and error propagation. Export fixtures
|
||||
cover nonidentity name-ordinal mappings, forwarders, empty slots, invalid spans,
|
||||
unbounded strings, and deterministic failure outputs. The export API requires a
|
||||
valid loader-mapped module; it is not an arbitrary file parser.
|
||||
|
||||
## File-object validation flag probe
|
||||
|
||||
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`
|
||||
from the same developer shell to test probe plumbing with mock CI. The kernel
|
||||
probe must still be run in a suitable VM to verify actual policy behavior.
|
||||
for `CiValidateFileObject` flag `0x20`. The host runner tests probe plumbing with
|
||||
mock CI. The kernel probe must still run in a suitable VM to verify actual
|
||||
policy behavior.
|
||||
|
||||
## Kernel compile checks and remaining validation
|
||||
|
||||
`mincrypt-layout-tests.cpp` asserts default modern x64/ARM64 and pre-Windows-10
|
||||
compile layouts. On x64 it also includes the opt-in observed 26100 header's 14
|
||||
layout assertions. Compile it with the same WDK/kernel architecture flags and
|
||||
include paths as the library. This checks structure definitions, not runtime
|
||||
compatibility on those systems. `ci-file-object-probe.cpp` is separately compiled
|
||||
as kernel code for x64/ARM64; its real CI behavior is still untested.
|
||||
|
||||
The host tests do not establish CI trust policy, real asynchronous kernel I/O,
|
||||
real policy ownership under Driver Verifier, revocation/timestamp behavior, or
|
||||
all Windows-version/layout combinations. See the
|
||||
[implementation ledger](../docs/implementation-status.md) for the remaining work.
|
||||
@@ -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
@@ -6,10 +6,12 @@
|
||||
#include <ntstatus.h>
|
||||
#include <bcrypt.h>
|
||||
#include <wintrust.h>
|
||||
#include <mssip.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
||||
#define ASSERT(x) Check(!!(x), #x)
|
||||
@@ -19,11 +21,21 @@
|
||||
#define PAGED_CODE() ((void)0)
|
||||
#define MINCRYPT_SHA1_LENGTH 20
|
||||
#define MINCRYPT_SHA256_LENGTH 32
|
||||
#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
|
||||
#define DOMITO_CI_SOURCE_EMBEDDED 1UL
|
||||
#define DOMITO_CI_SOURCE_CATALOG 2UL
|
||||
typedef const UCHAR* PCUCHAR;
|
||||
|
||||
static unsigned checks, allocations, ciCalls, hashCalls;
|
||||
static int failOperation, failHashCall;
|
||||
static ULONG_PTR bytesRead;
|
||||
static bool allocatePolicies;
|
||||
static NTSTATUS embeddedResult = STATUS_SUCCESS;
|
||||
static NTSTATUS catalogResults[2] = {STATUS_SUCCESS, STATUS_SUCCESS};
|
||||
static unsigned catalogCalls;
|
||||
static ULONG expectedCatalogMask = DOMITO_CI_LEGACY_CATALOG_POLICY_BITS;
|
||||
static BOOLEAN expectedTrustedMarker;
|
||||
static LONGLONG observedFileSize = 1056;
|
||||
static void Check(bool ok, const char* message)
|
||||
{
|
||||
++checks;
|
||||
@@ -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.
|
||||
struct MINCRYPT_POLICY_INFO { ULONG Size; };
|
||||
struct MINCRYPT_POLICY_INFO { ULONG Size; PVOID ChainInfo; };
|
||||
struct KAPC_STATE { ULONG unused; };
|
||||
struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; };
|
||||
static PVOID PsInitialSystemProcess = nullptr;
|
||||
static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* p) { 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 KeUnstackDetachProcess(KAPC_STATE*) {}
|
||||
static NTSTATUS ZwReadFile(HANDLE, PVOID, PVOID, PVOID, IO_STATUS_BLOCK* io,
|
||||
PVOID buffer, ULONG size, LARGE_INTEGER*, PVOID)
|
||||
typedef PVOID PFILE_OBJECT;
|
||||
static PVOID fileObjectType;
|
||||
static PVOID* IoFileObjectType = &fileObjectType;
|
||||
#define KernelMode 0
|
||||
static NTSTATUS ObReferenceObjectByHandle(HANDLE, ULONG, PVOID, int, PVOID* object, PVOID)
|
||||
{
|
||||
io->Status = STATUS_SUCCESS;
|
||||
io->Information = bytesRead;
|
||||
*object = &fileObjectType;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
static void ObDereferenceObject(PVOID) {}
|
||||
static NTSTATUS FsRtlGetFileSize(PFILE_OBJECT, LARGE_INTEGER* size)
|
||||
{
|
||||
size->QuadPart = observedFileSize;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
static NTSTATUS DomitoReadFileAt(HANDLE, PVOID buffer, ULONG size, const LARGE_INTEGER* offset)
|
||||
{
|
||||
Check(offset->QuadPart == 1024, "explicit certificate read offset");
|
||||
std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size));
|
||||
return 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,
|
||||
MINCRYPT_POLICY_INFO*, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*)
|
||||
MINCRYPT_POLICY_INFO* policy, LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
|
||||
{
|
||||
++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,
|
||||
MINCRYPT_POLICY_INFO*, PVOID, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*)
|
||||
static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN reload,
|
||||
UINT32 trusted, UINT32 mask, MINCRYPT_POLICY_INFO* policy, PVOID,
|
||||
LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
|
||||
{
|
||||
++ciCalls;
|
||||
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
|
||||
@@ -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));
|
||||
IMAGE_FILE_HEADER f = {};
|
||||
f.NumberOfSections = 1;
|
||||
f.Machine = pe64 ? IMAGE_FILE_MACHINE_AMD64 : IMAGE_FILE_MACHINE_I386;
|
||||
f.SizeOfOptionalHeader = pe64 ? sizeof(IMAGE_OPTIONAL_HEADER64) : sizeof(IMAGE_OPTIONAL_HEADER32);
|
||||
std::memcpy(b.data() + nt + 4, &f, sizeof(f));
|
||||
IMAGE_OPTIONAL_HEADER64 h64 = {};
|
||||
IMAGE_OPTIONAL_HEADER32 h32 = {};
|
||||
h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
|
||||
h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16;
|
||||
h64.SectionAlignment = h32.SectionAlignment = 4096;
|
||||
h64.FileAlignment = h32.FileAlignment = 512;
|
||||
h64.SizeOfImage = h32.SizeOfImage = 8192;
|
||||
h64.Subsystem = h32.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI;
|
||||
h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384;
|
||||
if (certificate)
|
||||
{
|
||||
h64.DataDirectory[4] = h32.DataDirectory[4] = { nt + 896, 32 };
|
||||
ULONG length = 32;
|
||||
std::memcpy(b.data() + nt + 896, &length, sizeof(length));
|
||||
USHORT revision = 0x0200, type = WIN_CERT_TYPE_PKCS_SIGNED_DATA;
|
||||
std::memcpy(b.data() + nt + 900, &revision, 2);
|
||||
std::memcpy(b.data() + nt + 902, &type, 2);
|
||||
}
|
||||
std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader);
|
||||
IMAGE_SECTION_HEADER section = {};
|
||||
section.VirtualAddress = 4096;
|
||||
section.Misc.VirtualSize = 512;
|
||||
section.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ;
|
||||
section.PointerToRawData = nt + 384; section.SizeOfRawData = 512;
|
||||
std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, §ion, sizeof(section));
|
||||
for (ULONG i = nt + 384; i < nt + 896; ++i) b[i] = (UCHAR)(i * 31);
|
||||
@@ -187,10 +244,295 @@ static void GuardedInvalid(const std::vector<UCHAR>& b, NTSTATUS expected)
|
||||
UCHAR* input = region + span - b.size();
|
||||
std::memcpy(input, b.data(), b.size());
|
||||
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);
|
||||
}
|
||||
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()
|
||||
{
|
||||
SystemImageTests();
|
||||
ExtendedTests();
|
||||
for (bool pe64 : {false, true})
|
||||
{
|
||||
for (bool certificate : {false, true})
|
||||
@@ -255,5 +597,43 @@ int main()
|
||||
Check(NT_SUCCESS(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory,
|
||||
&policy, &signing, ×tamp)), "complete certificate read accepted");
|
||||
Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once");
|
||||
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, ×tamp, expectedCatalogMask, TRUE, &source)), "explicit catalog fallback");
|
||||
Check(source == DOMITO_CI_SOURCE_CATALOG && catalogCalls == 2 && allocations == 2,
|
||||
"final policy ownership and catalog source");
|
||||
CiFreePolicyInfo(&policy); CiFreePolicyInfo(×tamp);
|
||||
Check(allocations == 0, "final results freed once");
|
||||
source = 123;
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA256,
|
||||
&directory, &policy, &signing, ×tamp, 0, FALSE, &source) == STATUS_INVALID_PARAMETER && !source,
|
||||
"algorithm and length mismatch rejected");
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA384,
|
||||
&directory, &policy, &signing, ×tamp, 0, FALSE, &source) == STATUS_NOT_SUPPORTED,
|
||||
"legacy catalog algorithm capability explicit");
|
||||
observedFileSize = 1040;
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA1,
|
||||
&directory, &policy, &signing, ×tamp, 0, FALSE, &source) == STATUS_INVALID_IMAGE_FORMAT,
|
||||
"certificate file range checked before allocation");
|
||||
Check(allocations == 0, "invalid file range never allocated");
|
||||
observedFileSize = 1056;
|
||||
expectedCatalogMask = 0x8000;
|
||||
expectedTrustedMarker = TRUE;
|
||||
catalogCalls = 0;
|
||||
catalogResults[1] = STATUS_INVALID_IMAGE_HASH;
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA1,
|
||||
&directory, &policy, &signing, ×tamp, expectedCatalogMask, TRUE, &source) == STATUS_INVALID_IMAGE_HASH,
|
||||
"final catalog failure retained");
|
||||
Check(!source && allocations == 2, "final failed-call policies remain caller-owned");
|
||||
CiFreePolicyInfo(&policy); CiFreePolicyInfo(×tamp);
|
||||
Check(allocations == 0, "failed final policies freed once");
|
||||
std::printf("PASS: %u digest/legacy checks (Windows SIP, Crypt32, guard pages, failures, ownership).\n", checks);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -30,5 +30,5 @@ with tempfile.TemporaryDirectory(prefix='domito-digest-tests-') as temp:
|
||||
test_source = Path(__file__).with_name('digest-tests.cpp')
|
||||
subprocess.run(['cl', '/nologo', '/EHsc', '/W4', '/WX', '/std:c++17',
|
||||
'/I' + str(build), str(test_source), '/Fe' + str(build/'digest-tests.exe'),
|
||||
'/Fo' + str(build/'digest-tests.obj'), '/link', 'bcrypt.lib'], 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)
|
||||
Reference in new issue
Block a user