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
|
# CI APIs
|
||||||
|
|
||||||
|
These historical availability notes are unverified. The analyzed ci.dll
|
||||||
|
10.0.26100.8328 x64 still exports the digest-based APIs listed below. Exact
|
||||||
|
first-available builds and ABI changes need matching old binaries. See
|
||||||
|
[the current contracts and remaining work](docs/mincrypt-contracts.md).
|
||||||
|
|
||||||
## Available starting with 1511
|
## Available starting with 1511
|
||||||
|
|
||||||
- CiCheckSignedFile
|
- CiCheckSignedFile
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Built for and tested on **Windows 10 version 1507 (or newer) x64/ARM64**. 32-Bit
|
|||||||
- I do not want to force any consumer of the library to drag C++ paradigms into their project. I do expect the user to utilize a modern compiler though, so the library sources themselves may have some 'C++-ish touches' here and there, for my own convenience 😉
|
- I do not want to force any consumer of the library to drag C++ paradigms into their project. I do expect the user to utilize a modern compiler though, so the library sources themselves may have some 'C++-ish touches' here and there, for my own convenience 😉
|
||||||
- Compatibility with every Windows 10 version.
|
- Compatibility with every Windows 10 version.
|
||||||
- APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞
|
- APIs not available on older builds will give you a `STATUS_NOT_IMPLEMENTED` instead of hard-linking and therefore making your driver fail to load 🤞
|
||||||
- **Caution:** this claim comes with an asterisk though; due to PatchGuard (or some other security mechanism) I couldn't get run-time dynamic linking for `ci.dll` exports to work, so you need to actively avoid implementing code depending on exports that do not exist on earlier versions of Windows. Once I have a compatibility matrix for all of them I'll update the documentation accordingly.
|
- CI entry points are currently static imports from `ci.lib`; consumers must avoid imports unavailable on their target Windows builds. Export lookup now uses the correct name-ordinal mapping and rejects forwarders, but this does not establish a complete CI availability or runtime ABI matrix. See the remaining-work ledger below.
|
||||||
- No conflicts with WDF or DMF
|
- No conflicts with WDF or DMF
|
||||||
- The consuming driver *may* (but doesn't have to) utilize Microsoft WDF or DMF in addition without having to fear any incompatibilities 💪
|
- The consuming driver *may* (but doesn't have to) utilize Microsoft WDF or DMF in addition without having to fear any incompatibilities 💪
|
||||||
- Reliable SAL annotations.
|
- Reliable SAL annotations.
|
||||||
@@ -73,6 +73,10 @@ Copy the provided `Domito.props` file into the directory of your `.vcxproj` file
|
|||||||
|
|
||||||
## Code Integrity API notes
|
## Code Integrity API notes
|
||||||
|
|
||||||
|
- [Algorithm-explicit Authenticode hashing and legacy catalog policy](docs/authenticode-digests.md)
|
||||||
|
- [Mincrypt contracts, lifetimes, flags and opt-in x64 26100 layouts](docs/mincrypt-contracts.md)
|
||||||
|
- [Implemented corrections and remaining VM/binary work](docs/implementation-status.md)
|
||||||
|
|
||||||
- [CiValidateFileObject flag 0x20: evidence, limitations, and kernel test procedure](docs/ci-validate-file-object.md)
|
- [CiValidateFileObject flag 0x20: evidence, limitations, and kernel test procedure](docs/ci-validate-file-object.md)
|
||||||
|
|
||||||
## Sources & 3rd party credits
|
## Sources & 3rd party credits
|
||||||
|
|||||||
@@ -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_DER_BLOB, * PMINCRYPT_DER_BLOB,
|
||||||
MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB;
|
MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB;
|
||||||
|
|
||||||
|
// Encoded, length-delimited name bytes; not necessarily ANSI or NUL-terminated.
|
||||||
|
// The observed 26100 x64 string tag occupies padding; see the opt-in version header.
|
||||||
typedef struct _MINCRYPT_NAME_BLOB
|
typedef struct _MINCRYPT_NAME_BLOB
|
||||||
{
|
{
|
||||||
_Field_size_bytes_(Size) PCHAR Data;
|
_Field_size_bytes_(Size) PCHAR Data;
|
||||||
@@ -287,6 +289,8 @@ typedef struct _MINCRYPT_NAME_BLOB
|
|||||||
#define MINCRYPT_MAX_HASH_LENGTH (64)
|
#define MINCRYPT_MAX_HASH_LENGTH (64)
|
||||||
#define MINCRYPT_SHA1_LENGTH (160/8)
|
#define MINCRYPT_SHA1_LENGTH (160/8)
|
||||||
#define MINCRYPT_SHA256_LENGTH (256/8)
|
#define MINCRYPT_SHA256_LENGTH (256/8)
|
||||||
|
#define MINCRYPT_SHA384_LENGTH (384/8)
|
||||||
|
#define MINCRYPT_SHA512_LENGTH (512/8)
|
||||||
|
|
||||||
typedef struct _MINCRYPT_CHAIN_ELEMENT
|
typedef struct _MINCRYPT_CHAIN_ELEMENT
|
||||||
{
|
{
|
||||||
@@ -301,9 +305,11 @@ typedef struct _MINCRYPT_CHAIN_ELEMENT
|
|||||||
|
|
||||||
}MINCRYPT_CHAIN_ELEMENT, * PMINCRYPT_CHAIN_ELEMENT;
|
}MINCRYPT_CHAIN_ELEMENT, * PMINCRYPT_CHAIN_ELEMENT;
|
||||||
|
|
||||||
|
// CI owns one chain allocation; nested pointers borrow that allocation.
|
||||||
|
// Do not free child blobs separately or use memcpy to create a second owner.
|
||||||
typedef struct _MINCRYPT_CHAIN_INFO
|
typedef struct _MINCRYPT_CHAIN_INFO
|
||||||
{
|
{
|
||||||
UINT32 Size;
|
UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data.
|
||||||
|
|
||||||
MINCERT_BLOB* PublicKeys;
|
MINCERT_BLOB* PublicKeys;
|
||||||
UINT32 NumberOfPublicKeys;
|
UINT32 NumberOfPublicKeys;
|
||||||
@@ -319,7 +325,9 @@ typedef struct _MINCRYPT_CHAIN_INFO
|
|||||||
// ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc.
|
// ASN.1 blob of authenticated attributes - spcSpOpusInfo, contentType, etc.
|
||||||
MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
|
MINCRYPT_ATTR_BLOB AuthenticodeAttributes;
|
||||||
|
|
||||||
// UINT8 Hash[32];
|
// Observed 26100 x64 adds RootId at +0x34 and a 32-byte platform-manifest
|
||||||
|
// binary ID at +0x48. These are exposed only by Domito.MinCrypt.26100.h.
|
||||||
|
// Do not infer their availability solely from NTDDI_VERSION or Size.
|
||||||
|
|
||||||
#endif // NTDDI_VERSION >= NTDDI_WIN10
|
#endif // NTDDI_VERSION >= NTDDI_WIN10
|
||||||
|
|
||||||
@@ -338,7 +346,7 @@ typedef struct _MINCRYPT_CHAIN_INFO
|
|||||||
typedef struct _MINCRYPT_POLICY_INFO
|
typedef struct _MINCRYPT_POLICY_INFO
|
||||||
{
|
{
|
||||||
UINT32 Size;
|
UINT32 Size;
|
||||||
UINT32 VerificationStatus;
|
NTSTATUS VerificationStatus; // Check API return status separately.
|
||||||
UINT32 PolicyBits;
|
UINT32 PolicyBits;
|
||||||
|
|
||||||
MINCRYPT_CHAIN_INFO* ChainInfo;
|
MINCRYPT_CHAIN_INFO* ChainInfo;
|
||||||
@@ -347,8 +355,8 @@ typedef struct _MINCRYPT_POLICY_INFO
|
|||||||
|
|
||||||
#if (NTDDI_VERSION >= NTDDI_WIN10)
|
#if (NTDDI_VERSION >= NTDDI_WIN10)
|
||||||
|
|
||||||
LARGE_INTEGER NotBefore; // The certificate is not valid before this time
|
LARGE_INTEGER NotBefore; // Observed: signer certificate validity start.
|
||||||
LARGE_INTEGER NotAfter; // The certificate is not valid after this time
|
LARGE_INTEGER NotAfter; // Observed: signer certificate validity end.
|
||||||
|
|
||||||
#endif // NTDDI_VERSION >= NTDDI_WIN10
|
#endif // NTDDI_VERSION >= NTDDI_WIN10
|
||||||
|
|
||||||
@@ -358,12 +366,16 @@ typedef struct _MINCRYPT_POLICY_INFO
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets a PolicyInfo struct - frees the dynamically allocated buffer in PolicyInfo (ChainInfo) if not null.
|
* Release a known initialized policy's owned ChainInfo allocation and reset it.
|
||||||
* Zeros the entire PolicyInfo struct.
|
* Zero-initialize policy objects before first use. Release both signer and TSA
|
||||||
|
* policies after use, including failures, and before reusing output storage.
|
||||||
|
* Borrowed child pointers become invalid when the owner is released. Never clear
|
||||||
|
* Size while a chain is still owned. Size does not negotiate the runtime layout.
|
||||||
*
|
*
|
||||||
* @param PolicyInfo - the struct to reset.
|
* @param PolicyInfo - initialized policy, not arbitrary uninitialized storage.
|
||||||
*
|
*
|
||||||
* @return the struct which was reset.
|
* Ignore the return value. The legacy PVOID declaration is retained for source
|
||||||
|
* compatibility; observed 26100 x64 code provides no defined pointer return.
|
||||||
*/
|
*/
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
MINCRYPTAPI
|
MINCRYPTAPI
|
||||||
@@ -375,18 +387,23 @@ CiFreePolicyInfo(
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10!
|
* Digest-based workflow, also implemented on observed Windows 11 build 26100.
|
||||||
|
* Prefer the file-object workflow when its policy fits. Older-build ABI and
|
||||||
|
* KB3033929 requirements need a separately verified compatibility matrix.
|
||||||
*
|
*
|
||||||
* Given a file digest and signature of a file, verify the signature and provide information regarding
|
* Given a file digest and signature of a file, verify the signature and provide information regarding
|
||||||
* the certificates that was used for signing (the entire certificate chain)
|
* the certificates that was used for signing (the entire certificate chain)
|
||||||
*
|
*
|
||||||
* @param Hash - buffer containing the digest
|
* @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
|
||||||
*
|
*
|
||||||
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
|
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
|
||||||
*
|
*
|
||||||
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
|
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
|
||||||
*
|
*
|
||||||
* @param SecurityDirectory - pointer to the start of the security directory
|
* @param SecurityDirectory - WIN_CERTIFICATE header, not the PKCS#7 payload.
|
||||||
|
* Observed 26100 x64 checks 4-byte pointer alignment, revision 0x200,
|
||||||
|
* type 2, supplied size >=12 and 8 < dwLength <= supplied size.
|
||||||
|
* This wrapper selects the first entry; it does not enumerate the table.
|
||||||
*
|
*
|
||||||
* @param SizeOfSecurityDirectory - size the security directory
|
* @param SizeOfSecurityDirectory - size the security directory
|
||||||
*
|
*
|
||||||
@@ -396,9 +413,12 @@ CiFreePolicyInfo(
|
|||||||
*
|
*
|
||||||
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain
|
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain
|
||||||
*
|
*
|
||||||
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified.
|
* @return Overall verification NTSTATUS. A successful policy VerificationStatus
|
||||||
|
* does not replace this return value: the final file-digest comparison
|
||||||
|
* can still fail. Both policy outputs may carry diagnostics on failure.
|
||||||
|
* See docs/mincrypt-contracts.md for ownership and policy limits.
|
||||||
* Various error values otherwise, for example:
|
* Various error values otherwise, for example:
|
||||||
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
|
* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
|
||||||
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
||||||
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
||||||
*/
|
*/
|
||||||
@@ -407,10 +427,10 @@ MINCRYPTAPI
|
|||||||
NTSTATUS
|
NTSTATUS
|
||||||
NTAPI
|
NTAPI
|
||||||
CiCheckSignedFile(
|
CiCheckSignedFile(
|
||||||
_In_ PVOID Hash,
|
_In_reads_bytes_(HashSize) PVOID Hash,
|
||||||
_In_ UINT32 HashSize,
|
_In_ UINT32 HashSize,
|
||||||
_In_ ALG_ID HashAlgId,
|
_In_ ALG_ID HashAlgId,
|
||||||
_In_ PVOID SecurityDirectory,
|
_In_reads_bytes_(SizeOfSecurityDirectory) PVOID SecurityDirectory,
|
||||||
_In_ UINT32 SizeOfSecurityDirectory,
|
_In_ UINT32 SizeOfSecurityDirectory,
|
||||||
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||||
_Out_ LARGE_INTEGER* SigningTime,
|
_Out_ LARGE_INTEGER* SigningTime,
|
||||||
@@ -418,36 +438,51 @@ CiCheckSignedFile(
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
// Existing Domito compatibility filter, not a universal trusted-root policy.
|
||||||
|
// Includes test/DMD categories and a diagnostic bit; select policy deliberately.
|
||||||
|
#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Win7SP1-Win8.1 only (KB3033929 installed). Use CiValidateFileObject on Win10!
|
* Digest-based workflow, also implemented on observed Windows 11 build 26100.
|
||||||
|
* Prefer the file-object workflow when its policy fits. Older-build ABI and
|
||||||
|
* KB3033929 requirements need a separately verified compatibility matrix.
|
||||||
*
|
*
|
||||||
* Checks if the SHA-1 message digest is contained within a verified system catalog
|
* Finds a caller-computed file digest in an accepted installed catalog.
|
||||||
|
* Observed 26100 x64 file-hash lookup supports SHA-1 and SHA-256.
|
||||||
*
|
*
|
||||||
* @note must be attached to the PsInitialSystemProcess first!
|
* @note must be attached to the PsInitialSystemProcess first!
|
||||||
*
|
*
|
||||||
* @param Hash - buffer containing the digest
|
* @param Hash[in] - caller-computed file digest; CI does not recompute the file bytes
|
||||||
*
|
*
|
||||||
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
|
* @param HashSize - size of the digest, e.g. 0x14(160bit) for SHA1, 0x20(256bit) for SHA256
|
||||||
*
|
*
|
||||||
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
|
* @param HashAlgId - digest algorithm identifier, e.g. CALG_SHA1(0x8004), CALG_SHA_256(0x800C)
|
||||||
*
|
*
|
||||||
* @param IsReloadCatalogs - is reload catalogs cache.
|
* @param ReloadCatalogs - request catalog-store reload; process/silo and cache state matter.
|
||||||
*
|
*
|
||||||
* @param Always0 - this is for IsReloadCatalogs, Always0 != 0 ? 16 : 24;
|
* @param RequireTrustedCatalog - nonzero requires the observed catalog TRUSTED
|
||||||
|
* extension marker. This does not itself establish Microsoft signer trust.
|
||||||
*
|
*
|
||||||
* @param Always2007F - unknown, always 0x2007F, maybe a mask.
|
* @param AcceptedCatalogPolicyBits - candidate catalog policy-bit filter.
|
||||||
|
* Observed 26100 x64 requires ANY overlap with catalog PolicyBits, not
|
||||||
|
* every requested bit. This is not a flags-to-enable input.
|
||||||
*
|
*
|
||||||
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
|
* @param PolicyInfo[out] - PolicyInfo containing information about the signer certificate chain.
|
||||||
*
|
*
|
||||||
* @param CatalogName[out option] - catalog file name.
|
* @param CatalogName[out option] - initialize empty; observed 26100 x64 returns
|
||||||
|
* a separate duplicated string allocation. Release a returned Buffer
|
||||||
|
* with RtlFreeUnicodeString, separately from both policy outputs.
|
||||||
*
|
*
|
||||||
* @param SigningTime[out] - when the file was signed (FILETIME format)
|
* @param SigningTime[out] - when the file was signed (FILETIME format)
|
||||||
*
|
*
|
||||||
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain.
|
* @param TimeStampPolicyInfo[out] - PolicyInfo containing information about the timestamping authority (TSA) certificate chain.
|
||||||
*
|
*
|
||||||
* @return STATUS_SUCCESS if the file digest in the signature matches the given digest and the signer cetificate is verified.
|
* @return Overall verification NTSTATUS. A successful policy VerificationStatus
|
||||||
|
* does not replace this return value. Catalog selection and the
|
||||||
|
* selected policy still apply. Outputs may carry diagnostics on failure.
|
||||||
|
* See docs/mincrypt-contracts.md for ownership and policy limits.
|
||||||
* Various error values otherwise, for example:
|
* Various error values otherwise, for example:
|
||||||
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
|
* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
|
||||||
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
||||||
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
||||||
*/
|
*/
|
||||||
@@ -456,12 +491,12 @@ MINCRYPTAPI
|
|||||||
NTSTATUS
|
NTSTATUS
|
||||||
NTAPI
|
NTAPI
|
||||||
CiVerifyHashInCatalog(
|
CiVerifyHashInCatalog(
|
||||||
_In_ PVOID Hash,
|
_In_reads_bytes_(HashSize) PVOID Hash,
|
||||||
_In_ UINT32 HashSize,
|
_In_ UINT32 HashSize,
|
||||||
_In_ ALG_ID HashAlgId,
|
_In_ ALG_ID HashAlgId,
|
||||||
_In_ BOOLEAN IsReloadCatalogs,
|
_In_ BOOLEAN ReloadCatalogs,
|
||||||
_In_ UINT32 Always0,
|
_In_ UINT32 RequireTrustedCatalog,
|
||||||
_In_ UINT32 Always2007F,
|
_In_ UINT32 AcceptedCatalogPolicyBits,
|
||||||
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||||
_Out_opt_ UNICODE_STRING* CatalogName,
|
_Out_opt_ UNICODE_STRING* CatalogName,
|
||||||
_Out_ LARGE_INTEGER* SigningTime,
|
_Out_ LARGE_INTEGER* SigningTime,
|
||||||
@@ -552,7 +587,7 @@ CiSetTrustedOriginClaimId(
|
|||||||
* @return STATUS_SUCCESS when verification succeeds under the selected CI policy.
|
* @return STATUS_SUCCESS when verification succeeds under the selected CI policy.
|
||||||
* This does not by itself establish the application's signer trust policy.
|
* This does not by itself establish the application's signer trust policy.
|
||||||
* Various error values otherwise, for example:
|
* Various error values otherwise, for example:
|
||||||
* STATUS_INVALID_IMAGE_HASH - the digest does not match the digest in the signature
|
* STATUS_INVALID_IMAGE_HASH - digest/signature or selected CI policy rejected
|
||||||
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
* STATUS_IMAGE_CERT_REVOKED - the certificate used for signing the file is revoked
|
||||||
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
* STATUS_IMAGE_CERT_EXPIRED - the certificate used for signing the file has expired
|
||||||
*/
|
*/
|
||||||
@@ -562,12 +597,12 @@ NTSTATUS
|
|||||||
NTAPI
|
NTAPI
|
||||||
CiValidateFileObject(
|
CiValidateFileObject(
|
||||||
_In_ FILE_OBJECT* FileObject,
|
_In_ FILE_OBJECT* FileObject,
|
||||||
_In_opt_ UINT32 ValidationFlags,
|
_In_ UINT32 ValidationFlags,
|
||||||
_In_opt_ UINT32 RequestedSigningLevel,
|
_In_ UINT32 RequestedSigningLevel,
|
||||||
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
_Out_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||||
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
|
_Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
|
||||||
_Out_ LARGE_INTEGER* SigningTime,
|
_Out_ LARGE_INTEGER* SigningTime,
|
||||||
_Out_ UINT8* Hash,
|
_Out_writes_bytes_(*HashSize) UINT8* Hash,
|
||||||
_Inout_ UINT32* HashSize,
|
_Inout_ UINT32* HashSize,
|
||||||
_Out_ ALG_ID* HashAlgId
|
_Out_ ALG_ID* HashAlgId
|
||||||
);
|
);
|
||||||
|
|||||||
+96
-3
@@ -184,6 +184,43 @@ DomitoGetPortableExecutableDigestKind(
|
|||||||
_In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory
|
_In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reads the first PKCS#7 entry's SpcIndirectDataContent DigestInfo algorithm
|
||||||
|
// using bounded DER navigation. Does not validate the signature or select among
|
||||||
|
// multiple/nested signatures. Absent table => STATUS_NOT_FOUND; unsupported OID
|
||||||
|
// or certificate type => STATUS_NOT_SUPPORTED; malformed DER => format error.
|
||||||
|
// Unlike the compatibility helper above, it never silently falls back to SHA-1.
|
||||||
|
_IRQL_requires_max_(DISPATCH_LEVEL)
|
||||||
|
EXTERN_C
|
||||||
|
NTSTATUS
|
||||||
|
DomitoGetPortableExecutableDigestKindEx(
|
||||||
|
_In_reads_bytes_(PeSize) const UCHAR* PeBytes,
|
||||||
|
_In_ ULONG PeSize,
|
||||||
|
_In_reads_(IMAGE_DIRECTORY_ENTRY_SECURITY + 1) const IMAGE_DATA_DIRECTORY* Directories,
|
||||||
|
_Out_ PUINT32 DigestCalg
|
||||||
|
);
|
||||||
|
|
||||||
|
// Algorithm-explicit Authenticode digest for the supported layout subset:
|
||||||
|
// bounded PE32/PE32+ headers, contiguous nonoverlapping raw sections in file
|
||||||
|
// offset order, optional overlay before a complete EOF certificate table.
|
||||||
|
// SHA-1/256/384/512 supported. Gaps/overlaps/nonterminal certificates are rejected.
|
||||||
|
// No signature trust decision is made. Outputs are cleared on failure; the
|
||||||
|
// certificate borrows stable input memory, and only the digest must be freed.
|
||||||
|
// See docs/authenticode-digests.md for policy and Windows SIP comparisons.
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
EXTERN_C
|
||||||
|
NTSTATUS
|
||||||
|
DomitoCalculatePortableExecutableDigestEx(
|
||||||
|
_In_reads_bytes_(PeSize) const UCHAR* PeBytes,
|
||||||
|
_In_ ULONG PeSize,
|
||||||
|
_In_ UINT32 HashAlgId,
|
||||||
|
_Out_ PULONG DigestSize,
|
||||||
|
_Outptr_result_bytebuffer_(*DigestSize) PVOID* Digest,
|
||||||
|
_Outptr_result_maybenull_ LPWIN_CERTIFICATE* Certificate,
|
||||||
|
_Out_ PULONG SecuritySize
|
||||||
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Calculates a PE digest using the existing linear hashing convention.
|
// Calculates a PE digest using the existing linear hashing convention.
|
||||||
// Validates accessed header/certificate ranges and clears outputs on failure
|
// Validates accessed header/certificate ranges and clears outputs on failure
|
||||||
@@ -217,8 +254,40 @@ DomitoFreePortableExecutableDigest(
|
|||||||
_In_ PVOID pDigestOut
|
_In_ PVOID pDigestOut
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#define DOMITO_CI_SOURCE_EMBEDDED 1UL
|
||||||
|
#define DOMITO_CI_SOURCE_CATALOG 2UL
|
||||||
|
|
||||||
|
// Policy-explicit legacy helper. SHA-1/256 only; hash size must match algorithm.
|
||||||
|
// Policy objects must be zero-initialized or own valid CI results; both are freed
|
||||||
|
// before each attempt. Caller owns both final policy outputs, including failure.
|
||||||
|
// Embedded validation is tried first; only INVALID_IMAGE_HASH permits catalog
|
||||||
|
// fallback/reload. AcceptedCatalogPolicyBits uses ANY-overlap catalog filtering;
|
||||||
|
// RequireTrustedCatalog gates the observed marker, not publisher identity.
|
||||||
|
// Source is 0 on failure, EMBEDDED/CATALOG on success. Stable file bytes and a
|
||||||
|
// handle valid in the caller's current process context are required during I/O.
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
EXTERN_C
|
||||||
|
NTSTATUS
|
||||||
|
DomitoValidateFileLegacyModeEx(
|
||||||
|
_In_ HANDLE FileHandle,
|
||||||
|
_In_reads_bytes_(HashSize) PVOID Hash,
|
||||||
|
_In_ UINT32 HashSize,
|
||||||
|
_In_ ALG_ID HashAlgId,
|
||||||
|
_In_ const IMAGE_DATA_DIRECTORY* SecurityDirectory,
|
||||||
|
_Inout_ MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||||
|
_Out_ LARGE_INTEGER* SigningTime,
|
||||||
|
_Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
|
||||||
|
_In_ UINT32 AcceptedCatalogPolicyBits,
|
||||||
|
_In_ BOOLEAN RequireTrustedCatalog,
|
||||||
|
_Out_ PULONG ValidationSource
|
||||||
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Verifies if the Authenticode signature of a give PE file matches the provided (e.g. SHA1) file digest.
|
// Compatibility wrapper around DomitoValidateFileLegacyModeEx, with catalog
|
||||||
|
// mask DOMITO_CI_LEGACY_CATALOG_POLICY_BITS and RequireTrustedCatalog = FALSE.
|
||||||
|
// The same initialization, ownership, hash, handle, and stable-file rules apply.
|
||||||
//
|
//
|
||||||
_Success_(return == STATUS_SUCCESS)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
@@ -258,7 +327,10 @@ DomitoFindModuleBaseAddress(
|
|||||||
);
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Finds the address of an exported function by name.
|
// Finds an export by name (case-insensitive) in a valid loader-mapped module.
|
||||||
|
// ModuleBase must remain mapped; arbitrary file buffers are not supported.
|
||||||
|
// Export table spans are checked against SizeOfImage. Forwarded exports return
|
||||||
|
// STATUS_NOT_SUPPORTED; the optional output is cleared before lookup.
|
||||||
//
|
//
|
||||||
_Success_(return == STATUS_SUCCESS)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
@@ -289,7 +361,28 @@ DomitoMemorySearchPattern(
|
|||||||
);
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Reads from the beginning of a file until the end or the buffer size is reached.
|
// Reads exactly BufferSize bytes at a nonnegative absolute file offset.
|
||||||
|
// Same completion, buffer, handle, APC, and short-read contract as DomitoReadFile.
|
||||||
|
//
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
EXTERN_C
|
||||||
|
NTSTATUS
|
||||||
|
DomitoReadFileAt(
|
||||||
|
_In_ HANDLE FileHandle,
|
||||||
|
_Out_writes_bytes_(BufferSize) PVOID Buffer,
|
||||||
|
_In_ ULONG BufferSize,
|
||||||
|
_In_ const LARGE_INTEGER* ByteOffset
|
||||||
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reads exactly BufferSize bytes from file offset zero. Short reads return
|
||||||
|
// STATUS_END_OF_FILE; Buffer may then contain a partial read. Zero length succeeds.
|
||||||
|
// Waits for pending I/O on a private event before returning. Requires PASSIVE_LEVEL
|
||||||
|
// with special kernel APCs enabled, a readable handle valid in the current process
|
||||||
|
// (or a kernel handle), and a buffer that remains valid for the entire call.
|
||||||
|
// For synchronous handles, the explicit read also updates the file position.
|
||||||
//
|
//
|
||||||
_Success_(return == STATUS_SUCCESS)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
|
|||||||
+378
-48
@@ -89,11 +89,252 @@ DomitoGetPortableExecutableDigestKind(
|
|||||||
return CALG_SHA1;
|
return CALG_SHA1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bounded DER navigation for the outer Authenticode content only. This does not
|
||||||
|
// validate certificates, signerInfos, nested signatures, or cryptographic trust.
|
||||||
|
typedef struct _DOMITO_DER_VIEW
|
||||||
|
{
|
||||||
|
const UCHAR* Bytes;
|
||||||
|
ULONG Length;
|
||||||
|
} DOMITO_DER_VIEW;
|
||||||
|
|
||||||
|
static BOOLEAN DomitoDerTake(DOMITO_DER_VIEW* Input, UCHAR Tag, DOMITO_DER_VIEW* Value)
|
||||||
|
{
|
||||||
|
if (Input->Length < 2 || Input->Bytes[0] != Tag)
|
||||||
|
return FALSE;
|
||||||
|
ULONG length = Input->Bytes[1];
|
||||||
|
ULONG header = 2;
|
||||||
|
if (length & 0x80)
|
||||||
|
{
|
||||||
|
const ULONG count = length & 0x7F;
|
||||||
|
if (!count || count > sizeof(ULONG) || count > Input->Length - header ||
|
||||||
|
Input->Bytes[header] == 0)
|
||||||
|
return FALSE; // No indefinite or nonminimal DER lengths.
|
||||||
|
length = 0;
|
||||||
|
for (ULONG i = 0; i < count; ++i)
|
||||||
|
length = (length << 8) | Input->Bytes[header++];
|
||||||
|
if (length < 0x80)
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
if (length > Input->Length - header)
|
||||||
|
return FALSE;
|
||||||
|
Value->Bytes = Input->Bytes + header;
|
||||||
|
Value->Length = length;
|
||||||
|
Input->Bytes += header + length;
|
||||||
|
Input->Length -= header + length;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOLEAN DomitoDerOid(DOMITO_DER_VIEW* Input, const UCHAR* Oid, ULONG Size)
|
||||||
|
{
|
||||||
|
DOMITO_DER_VIEW value;
|
||||||
|
return DomitoDerTake(Input, 0x06, &value) && value.Length == Size &&
|
||||||
|
memcmp(value.Bytes, Oid, Size) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_IRQL_requires_max_(DISPATCH_LEVEL)
|
||||||
|
NTSTATUS
|
||||||
|
DomitoGetPortableExecutableDigestKindEx(
|
||||||
|
_In_reads_bytes_(PeSize) const UCHAR* PeBytes,
|
||||||
|
_In_ ULONG PeSize,
|
||||||
|
_In_reads_(IMAGE_DIRECTORY_ENTRY_SECURITY + 1) const IMAGE_DATA_DIRECTORY* Directories,
|
||||||
|
_Out_ PUINT32 DigestCalg)
|
||||||
|
{
|
||||||
|
if (!DigestCalg)
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
*DigestCalg = 0;
|
||||||
|
if (!PeBytes || !Directories)
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
const IMAGE_DATA_DIRECTORY security = Directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
|
||||||
|
if (!security.VirtualAddress && !security.Size)
|
||||||
|
return STATUS_NOT_FOUND;
|
||||||
|
if (!security.VirtualAddress || (security.VirtualAddress & 7) ||
|
||||||
|
security.VirtualAddress > PeSize || security.Size > PeSize - security.VirtualAddress ||
|
||||||
|
security.Size < 8)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
ULONG length;
|
||||||
|
USHORT revision, type;
|
||||||
|
const UCHAR* certificate = PeBytes + security.VirtualAddress;
|
||||||
|
memcpy(&length, certificate, 4);
|
||||||
|
memcpy(&revision, certificate + 4, 2);
|
||||||
|
memcpy(&type, certificate + 6, 2);
|
||||||
|
if (length <= 8 || length > security.Size)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA)
|
||||||
|
return STATUS_NOT_SUPPORTED;
|
||||||
|
|
||||||
|
const UCHAR signedDataOid[] = {0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x07,0x02};
|
||||||
|
const UCHAR indirectOid[] = {0x2B,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x01,0x04};
|
||||||
|
const UCHAR sha1Oid[] = {0x2B,0x0E,0x03,0x02,0x1A};
|
||||||
|
const UCHAR sha2Prefix[] = {0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02};
|
||||||
|
DOMITO_DER_VIEW payload = {certificate + 8, length - 8};
|
||||||
|
DOMITO_DER_VIEW content, wrapped, signedData, ignored, contentInfo, indirect, digest, algorithm, oid;
|
||||||
|
if (!DomitoDerTake(&payload, 0x30, &content) ||
|
||||||
|
!DomitoDerOid(&content, signedDataOid, sizeof(signedDataOid)) ||
|
||||||
|
!DomitoDerTake(&content, 0xA0, &wrapped) || content.Length ||
|
||||||
|
!DomitoDerTake(&wrapped, 0x30, &signedData) || wrapped.Length ||
|
||||||
|
!DomitoDerTake(&signedData, 0x02, &ignored) ||
|
||||||
|
!DomitoDerTake(&signedData, 0x31, &ignored) ||
|
||||||
|
!DomitoDerTake(&signedData, 0x30, &contentInfo) ||
|
||||||
|
!DomitoDerOid(&contentInfo, indirectOid, sizeof(indirectOid)) ||
|
||||||
|
!DomitoDerTake(&contentInfo, 0xA0, &wrapped) || contentInfo.Length ||
|
||||||
|
!DomitoDerTake(&wrapped, 0x30, &indirect) || wrapped.Length ||
|
||||||
|
!DomitoDerTake(&indirect, 0x30, &ignored) ||
|
||||||
|
!DomitoDerTake(&indirect, 0x30, &digest) || indirect.Length ||
|
||||||
|
!DomitoDerTake(&digest, 0x30, &algorithm) ||
|
||||||
|
!DomitoDerTake(&algorithm, 0x06, &oid))
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
// Windows-signed entries can include up to seven zero alignment bytes in
|
||||||
|
// dwLength itself. Accept only that bounded, aligned padding after the DER.
|
||||||
|
if (payload.Length > 7 || (payload.Length && (length & 7)))
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
for (ULONG i = 0; i < payload.Length; ++i)
|
||||||
|
if (payload.Bytes[i])
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
if (algorithm.Length && (!DomitoDerTake(&algorithm, 0x05, &ignored) ||
|
||||||
|
ignored.Length || algorithm.Length))
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
UINT32 calg = 0;
|
||||||
|
ULONG expectedLength = 0;
|
||||||
|
if (oid.Length == sizeof(sha1Oid) && !memcmp(oid.Bytes, sha1Oid, sizeof(sha1Oid)))
|
||||||
|
{
|
||||||
|
calg = CALG_SHA1;
|
||||||
|
expectedLength = 20;
|
||||||
|
}
|
||||||
|
else if (oid.Length == sizeof(sha2Prefix) + 1 &&
|
||||||
|
!memcmp(oid.Bytes, sha2Prefix, sizeof(sha2Prefix)))
|
||||||
|
{
|
||||||
|
switch (oid.Bytes[sizeof(sha2Prefix)])
|
||||||
|
{
|
||||||
|
case 1: calg = CALG_SHA256; expectedLength = 32; break;
|
||||||
|
case 2: calg = CALG_SHA384; expectedLength = 48; break;
|
||||||
|
case 3: calg = CALG_SHA512; expectedLength = 64; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!calg)
|
||||||
|
return STATUS_NOT_SUPPORTED;
|
||||||
|
if (!DomitoDerTake(&digest, 0x04, &ignored) || digest.Length || ignored.Length != expectedLength)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
*DigestCalg = calg;
|
||||||
|
return STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conservative section-layout boundary: contiguous raw sections (any header
|
||||||
|
// order), optional overlay before an EOF certificate table. Gaps, overlaps, and
|
||||||
|
// certificates in the middle are rejected rather than assigned an ambiguous hash.
|
||||||
|
static NTSTATUS DomitoValidateAuthenticodeLayout(const UCHAR* Bytes, ULONG Size)
|
||||||
|
{
|
||||||
|
if (!Bytes || Size < sizeof(IMAGE_DOS_HEADER))
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
IMAGE_DOS_HEADER dos;
|
||||||
|
memcpy(&dos, Bytes, sizeof(dos));
|
||||||
|
if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < 0)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
const ULONG nt = (ULONG)dos.e_lfanew;
|
||||||
|
const ULONG prefix = sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER);
|
||||||
|
if (nt > Size || prefix > Size - nt)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
ULONG signature;
|
||||||
|
IMAGE_FILE_HEADER file;
|
||||||
|
memcpy(&signature, Bytes + nt, sizeof(signature));
|
||||||
|
memcpy(&file, Bytes + nt + sizeof(signature), sizeof(file));
|
||||||
|
if (signature != IMAGE_NT_SIGNATURE || !file.NumberOfSections || file.NumberOfSections > 96)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
const ULONG optional = nt + prefix;
|
||||||
|
if (file.SizeOfOptionalHeader < 2 || file.SizeOfOptionalHeader > Size - optional)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
USHORT magic;
|
||||||
|
memcpy(&magic, Bytes + optional, sizeof(magic));
|
||||||
|
ULONG directory;
|
||||||
|
if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC)
|
||||||
|
directory = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory);
|
||||||
|
else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
|
||||||
|
directory = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory);
|
||||||
|
else
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
if (file.SizeOfOptionalHeader < directory)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
ULONG count, headers;
|
||||||
|
memcpy(&count, Bytes + optional + directory - 4, sizeof(count));
|
||||||
|
memcpy(&headers, Bytes + optional + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfHeaders), sizeof(headers));
|
||||||
|
if (count > (file.SizeOfOptionalHeader - directory) / sizeof(IMAGE_DATA_DIRECTORY))
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
if (count <= IMAGE_DIRECTORY_ENTRY_SECURITY)
|
||||||
|
return STATUS_NOT_SUPPORTED; // SIP exclusion behavior differs for absent slots.
|
||||||
|
const ULONG table = optional + file.SizeOfOptionalHeader;
|
||||||
|
const ULONG tableSize = file.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);
|
||||||
|
if (tableSize > Size - table || headers < table + tableSize || headers > Size)
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
struct { ULONG Offset; ULONG Length; } sections[96];
|
||||||
|
ULONG used = 0;
|
||||||
|
for (ULONG i = 0; i < file.NumberOfSections; ++i)
|
||||||
|
{
|
||||||
|
IMAGE_SECTION_HEADER section;
|
||||||
|
memcpy(§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)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
NTSTATUS
|
static NTSTATUS
|
||||||
DomitoCalculatePortableExecutableDigest(
|
DomitoCalculatePortableExecutableDigestInternal(
|
||||||
|
_In_ UINT32 ExplicitCalg,
|
||||||
_In_ PUCHAR pPeBytes,
|
_In_ PUCHAR pPeBytes,
|
||||||
_In_ ULONG PeSize,
|
_In_ ULONG PeSize,
|
||||||
_Out_ PUINT32 pDigestCalgOut,
|
_Out_ PUINT32 pDigestCalgOut,
|
||||||
@@ -231,7 +472,8 @@ DomitoCalculatePortableExecutableDigest(
|
|||||||
BCRYPT_HASH_HANDLE hbHash = nullptr;
|
BCRYPT_HASH_HANDLE hbHash = nullptr;
|
||||||
ULONG hashLength = 0;
|
ULONG hashLength = 0;
|
||||||
ULONG resultLength = 0;
|
ULONG resultLength = 0;
|
||||||
const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories);
|
const UINT32 peDigestKind = ExplicitCalg ? ExplicitCalg :
|
||||||
|
DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories);
|
||||||
NTSTATUS status = BCryptOpenAlgorithmProvider(
|
NTSTATUS status = BCryptOpenAlgorithmProvider(
|
||||||
&hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0);
|
&hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0);
|
||||||
if (!NT_SUCCESS(status))
|
if (!NT_SUCCESS(status))
|
||||||
@@ -315,6 +557,47 @@ cleanup:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
NTSTATUS
|
||||||
|
DomitoCalculatePortableExecutableDigest(
|
||||||
|
PUCHAR PeBytes, ULONG PeSize, PUINT32 DigestCalg, PULONG DigestSize,
|
||||||
|
PVOID* Digest, LPWIN_CERTIFICATE* Certificate, PULONG SecuritySize)
|
||||||
|
{
|
||||||
|
return DomitoCalculatePortableExecutableDigestInternal(0, PeBytes, PeSize,
|
||||||
|
DigestCalg, DigestSize, Digest, Certificate, SecuritySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
NTSTATUS
|
||||||
|
DomitoCalculatePortableExecutableDigestEx(
|
||||||
|
const UCHAR* PeBytes, ULONG PeSize, UINT32 HashAlgId, PULONG DigestSize,
|
||||||
|
PVOID* Digest, LPWIN_CERTIFICATE* Certificate, PULONG SecuritySize)
|
||||||
|
{
|
||||||
|
if (!DigestSize || !Digest || !Certificate || !SecuritySize)
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
*DigestSize = 0;
|
||||||
|
*Digest = nullptr;
|
||||||
|
*Certificate = nullptr;
|
||||||
|
*SecuritySize = 0;
|
||||||
|
if (!PeBytes)
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
if (HashAlgId != CALG_SHA1 && HashAlgId != CALG_SHA256 &&
|
||||||
|
HashAlgId != CALG_SHA384 && HashAlgId != CALG_SHA512)
|
||||||
|
return STATUS_NOT_SUPPORTED;
|
||||||
|
NTSTATUS status = DomitoValidateAuthenticodeLayout(PeBytes, PeSize);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
return status;
|
||||||
|
UINT32 actualCalg = 0;
|
||||||
|
// Contiguous sorted sections make the validated section sequence identical
|
||||||
|
// to linear byte order; overlay precedes an EOF certificate table.
|
||||||
|
return DomitoCalculatePortableExecutableDigestInternal(HashAlgId, (PUCHAR)PeBytes,
|
||||||
|
PeSize, &actualCalg, DigestSize, Digest, Certificate, SecuritySize);
|
||||||
|
}
|
||||||
|
|
||||||
_IRQL_requires_max_(DISPATCH_LEVEL)
|
_IRQL_requires_max_(DISPATCH_LEVEL)
|
||||||
void
|
void
|
||||||
DomitoFreePortableExecutableDigest(
|
DomitoFreePortableExecutableDigest(
|
||||||
@@ -333,8 +616,8 @@ _Success_(return == STATUS_SUCCESS)
|
|||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
#pragma code_seg("PAGED")
|
#pragma code_seg("PAGED")
|
||||||
NTSTATUS
|
static NTSTATUS
|
||||||
DomitoValidateFileLegacyMode(
|
DomitoValidateFileLegacyModeInternal(
|
||||||
_In_ HANDLE FileHandle,
|
_In_ HANDLE FileHandle,
|
||||||
_In_ PVOID Hash,
|
_In_ PVOID Hash,
|
||||||
_In_ UINT32 HashSize,
|
_In_ UINT32 HashSize,
|
||||||
@@ -342,11 +625,20 @@ DomitoValidateFileLegacyMode(
|
|||||||
_In_ const IMAGE_DATA_DIRECTORY * SecurityDirectory,
|
_In_ const IMAGE_DATA_DIRECTORY * SecurityDirectory,
|
||||||
_Inout_ MINCRYPT_POLICY_INFO * PolicyInfo,
|
_Inout_ MINCRYPT_POLICY_INFO * PolicyInfo,
|
||||||
_Out_ LARGE_INTEGER * SigningTime,
|
_Out_ LARGE_INTEGER * SigningTime,
|
||||||
_Inout_ MINCRYPT_POLICY_INFO * TimeStampPolicyInfo
|
_Inout_ MINCRYPT_POLICY_INFO * TimeStampPolicyInfo,
|
||||||
|
_In_ UINT32 AcceptedCatalogPolicyBits,
|
||||||
|
_In_ BOOLEAN RequireTrustedCatalog,
|
||||||
|
_Out_ PULONG ValidationSource
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
PAGED_CODE();
|
PAGED_CODE();
|
||||||
|
|
||||||
|
if (!ValidationSource)
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
*ValidationSource = 0;
|
||||||
|
if (!Hash || !SecurityDirectory || !PolicyInfo || !SigningTime ||
|
||||||
|
!TimeStampPolicyInfo || PolicyInfo == TimeStampPolicyInfo)
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
NTSTATUS status = STATUS_SUCCESS;
|
NTSTATUS status = STATUS_SUCCESS;
|
||||||
PVOID certDirectory = nullptr;
|
PVOID certDirectory = nullptr;
|
||||||
KAPC_STATE systemContext = {};
|
KAPC_STATE systemContext = {};
|
||||||
@@ -358,15 +650,48 @@ DomitoValidateFileLegacyMode(
|
|||||||
CiFreePolicyInfo(PolicyInfo);
|
CiFreePolicyInfo(PolicyInfo);
|
||||||
CiFreePolicyInfo(TimeStampPolicyInfo);
|
CiFreePolicyInfo(TimeStampPolicyInfo);
|
||||||
|
|
||||||
if (HashSize != MINCRYPT_SHA1_LENGTH && HashSize != MINCRYPT_SHA256_LENGTH)
|
if (HashAlgId != CALG_SHA1 && HashAlgId != CALG_SHA256)
|
||||||
{
|
{
|
||||||
status = STATUS_INVALID_IMAGE_HASH;
|
status = STATUS_NOT_SUPPORTED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (HashSize != (UINT32)(HashAlgId == CALG_SHA1 ? MINCRYPT_SHA1_LENGTH : MINCRYPT_SHA256_LENGTH))
|
||||||
|
{
|
||||||
|
status = STATUS_INVALID_PARAMETER;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((SecurityDirectory->Size != 0) != (SecurityDirectory->VirtualAddress != 0))
|
||||||
|
{
|
||||||
|
status = STATUS_INVALID_IMAGE_FORMAT;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SecurityDirectory->Size != 0u &&
|
if (SecurityDirectory->Size != 0u &&
|
||||||
SecurityDirectory->VirtualAddress != 0u)
|
SecurityDirectory->VirtualAddress != 0u)
|
||||||
{
|
{
|
||||||
|
if ((SecurityDirectory->VirtualAddress & 7) || SecurityDirectory->Size < 12)
|
||||||
|
{
|
||||||
|
status = STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Bound the allocation/read against the file before trusting its table.
|
||||||
|
PFILE_OBJECT fileObject = nullptr;
|
||||||
|
status = ObReferenceObjectByHandle(FileHandle, FILE_READ_DATA, *IoFileObjectType,
|
||||||
|
KernelMode, (PVOID*)&fileObject, nullptr);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
break;
|
||||||
|
LARGE_INTEGER fileSize = {};
|
||||||
|
status = FsRtlGetFileSize(fileObject, &fileSize);
|
||||||
|
ObDereferenceObject(fileObject);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
break;
|
||||||
|
if (fileSize.QuadPart < 0 ||
|
||||||
|
(ULONGLONG)SecurityDirectory->VirtualAddress > (ULONGLONG)fileSize.QuadPart ||
|
||||||
|
SecurityDirectory->Size > (ULONGLONG)fileSize.QuadPart - SecurityDirectory->VirtualAddress)
|
||||||
|
{
|
||||||
|
status = STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
#pragma warning(disable:4996)
|
#pragma warning(disable:4996)
|
||||||
certDirectory = ExAllocatePoolWithTag(
|
certDirectory = ExAllocatePoolWithTag(
|
||||||
PagedPool,
|
PagedPool,
|
||||||
@@ -381,44 +706,10 @@ DomitoValidateFileLegacyMode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LARGE_INTEGER offset = {};
|
LARGE_INTEGER offset = {};
|
||||||
IO_STATUS_BLOCK ioStatusBlock = {};
|
offset.QuadPart = SecurityDirectory->VirtualAddress;
|
||||||
|
status = DomitoReadFileAt(FileHandle, certDirectory, SecurityDirectory->Size, &offset);
|
||||||
offset.LowPart = SecurityDirectory->VirtualAddress;
|
|
||||||
|
|
||||||
status = ZwReadFile(
|
|
||||||
FileHandle,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
&ioStatusBlock,
|
|
||||||
certDirectory,
|
|
||||||
SecurityDirectory->Size,
|
|
||||||
&offset,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status == STATUS_PENDING)
|
|
||||||
{
|
|
||||||
ZwWaitForSingleObject(
|
|
||||||
FileHandle,
|
|
||||||
FALSE,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
MemoryBarrier();
|
|
||||||
status = ioStatusBlock.Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NT_SUCCESS(status))
|
if (!NT_SUCCESS(status))
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
if (ioStatusBlock.Information != SecurityDirectory->Size)
|
|
||||||
{
|
|
||||||
status = STATUS_END_OF_FILE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
|
KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
|
||||||
{
|
{
|
||||||
@@ -437,6 +728,7 @@ DomitoValidateFileLegacyMode(
|
|||||||
|
|
||||||
if (NT_SUCCESS(status))
|
if (NT_SUCCESS(status))
|
||||||
{
|
{
|
||||||
|
*ValidationSource = DOMITO_CI_SOURCE_EMBEDDED;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +738,9 @@ DomitoValidateFileLegacyMode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CiFreePolicyInfo(PolicyInfo);
|
||||||
|
CiFreePolicyInfo(TimeStampPolicyInfo);
|
||||||
|
SigningTime->QuadPart = 0;
|
||||||
KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
|
KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
|
||||||
{
|
{
|
||||||
status = CiVerifyHashInCatalog(
|
status = CiVerifyHashInCatalog(
|
||||||
@@ -453,8 +748,8 @@ DomitoValidateFileLegacyMode(
|
|||||||
HashSize,
|
HashSize,
|
||||||
HashAlgId,
|
HashAlgId,
|
||||||
FALSE,
|
FALSE,
|
||||||
0,
|
RequireTrustedCatalog,
|
||||||
0x2007F,
|
AcceptedCatalogPolicyBits,
|
||||||
PolicyInfo,
|
PolicyInfo,
|
||||||
NULL,
|
NULL,
|
||||||
SigningTime,
|
SigningTime,
|
||||||
@@ -463,13 +758,16 @@ DomitoValidateFileLegacyMode(
|
|||||||
|
|
||||||
if (status == STATUS_INVALID_IMAGE_HASH)
|
if (status == STATUS_INVALID_IMAGE_HASH)
|
||||||
{
|
{
|
||||||
|
CiFreePolicyInfo(PolicyInfo);
|
||||||
|
CiFreePolicyInfo(TimeStampPolicyInfo);
|
||||||
|
SigningTime->QuadPart = 0;
|
||||||
status = CiVerifyHashInCatalog(
|
status = CiVerifyHashInCatalog(
|
||||||
Hash,
|
Hash,
|
||||||
HashSize,
|
HashSize,
|
||||||
HashAlgId,
|
HashAlgId,
|
||||||
TRUE,
|
TRUE,
|
||||||
0,
|
RequireTrustedCatalog,
|
||||||
0x2007F,
|
AcceptedCatalogPolicyBits,
|
||||||
PolicyInfo,
|
PolicyInfo,
|
||||||
NULL,
|
NULL,
|
||||||
SigningTime,
|
SigningTime,
|
||||||
@@ -478,6 +776,8 @@ DomitoValidateFileLegacyMode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeUnstackDetachProcess(&systemContext);
|
KeUnstackDetachProcess(&systemContext);
|
||||||
|
if (NT_SUCCESS(status))
|
||||||
|
*ValidationSource = DOMITO_CI_SOURCE_CATALOG;
|
||||||
|
|
||||||
} while (FALSE);
|
} while (FALSE);
|
||||||
|
|
||||||
@@ -488,4 +788,34 @@ DomitoValidateFileLegacyMode(
|
|||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
NTSTATUS
|
||||||
|
DomitoValidateFileLegacyMode(
|
||||||
|
HANDLE FileHandle, PVOID Hash, UINT32 HashSize, ALG_ID HashAlgId,
|
||||||
|
const IMAGE_DATA_DIRECTORY* SecurityDirectory, MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||||
|
LARGE_INTEGER* SigningTime, MINCRYPT_POLICY_INFO* TimeStampPolicyInfo)
|
||||||
|
{
|
||||||
|
ULONG source;
|
||||||
|
return DomitoValidateFileLegacyModeInternal(FileHandle, Hash, HashSize, HashAlgId,
|
||||||
|
SecurityDirectory, PolicyInfo, SigningTime, TimeStampPolicyInfo,
|
||||||
|
DOMITO_CI_LEGACY_CATALOG_POLICY_BITS, FALSE, &source);
|
||||||
|
}
|
||||||
|
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
NTSTATUS
|
||||||
|
DomitoValidateFileLegacyModeEx(
|
||||||
|
HANDLE FileHandle, PVOID Hash, UINT32 HashSize, ALG_ID HashAlgId,
|
||||||
|
const IMAGE_DATA_DIRECTORY* SecurityDirectory, MINCRYPT_POLICY_INFO* PolicyInfo,
|
||||||
|
LARGE_INTEGER* SigningTime, MINCRYPT_POLICY_INFO* TimeStampPolicyInfo,
|
||||||
|
UINT32 AcceptedCatalogPolicyBits, BOOLEAN RequireTrustedCatalog, PULONG ValidationSource)
|
||||||
|
{
|
||||||
|
return DomitoValidateFileLegacyModeInternal(FileHandle, Hash, HashSize, HashAlgId,
|
||||||
|
SecurityDirectory, PolicyInfo, SigningTime, TimeStampPolicyInfo,
|
||||||
|
AcceptedCatalogPolicyBits, RequireTrustedCatalog, ValidationSource);
|
||||||
|
}
|
||||||
#pragma code_seg()
|
#pragma code_seg()
|
||||||
+165
-64
@@ -129,6 +129,13 @@ DomitoFindModuleBaseAddress(
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This helper handles directory RVAs after the loader-mapped header establishes
|
||||||
|
// SizeOfImage. ModuleBase is not an arbitrary file buffer (there is no size input).
|
||||||
|
static BOOLEAN DomitoImageRvaSpan(ULONG ImageSize, ULONG Rva, ULONG Count, ULONG ElementSize)
|
||||||
|
{
|
||||||
|
return Rva <= ImageSize && ElementSize != 0 && Count <= (ImageSize - Rva) / ElementSize;
|
||||||
|
}
|
||||||
|
|
||||||
_Success_(return == STATUS_SUCCESS)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
@@ -139,56 +146,113 @@ DomitoFindExportedFunctionAddress(
|
|||||||
_Inout_opt_ PVOID * FunctionAddress
|
_Inout_opt_ PVOID * FunctionAddress
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
NTSTATUS status = STATUS_NOT_FOUND;
|
if (FunctionAddress)
|
||||||
ULONG exportSize;
|
{
|
||||||
|
*FunctionAddress = nullptr;
|
||||||
if (G_Common.RtlImageDirectoryEntryToData == NULL)
|
}
|
||||||
|
if (!ModuleBase || !FunctionName || !FunctionName->Buffer || !FunctionName->Length)
|
||||||
|
{
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
if (!G_Common.RtlImageDirectoryEntryToData)
|
||||||
{
|
{
|
||||||
return STATUS_NOT_IMPLEMENTED;
|
return STATUS_NOT_IMPLEMENTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the export directory information
|
// Initial headers belong to a valid loaded module, as required by this API.
|
||||||
const PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)G_Common.RtlImageDirectoryEntryToData(
|
const PUCHAR base = (PUCHAR)ModuleBase;
|
||||||
ModuleBase,
|
IMAGE_DOS_HEADER dos;
|
||||||
TRUE,
|
memcpy(&dos, base, sizeof(dos));
|
||||||
IMAGE_DIRECTORY_ENTRY_EXPORT,
|
if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < (LONG)sizeof(dos))
|
||||||
&exportSize
|
{
|
||||||
);
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
}
|
||||||
if (exportDirectory == NULL)
|
const PUCHAR nt = base + dos.e_lfanew;
|
||||||
|
ULONG signature;
|
||||||
|
memcpy(&signature, nt, sizeof(signature));
|
||||||
|
USHORT magic;
|
||||||
|
memcpy(&magic, nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER), sizeof(magic));
|
||||||
|
if (signature != IMAGE_NT_SIGNATURE ||
|
||||||
|
(magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC && magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC))
|
||||||
|
{
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
}
|
||||||
|
// SizeOfImage is at the same offset in PE32 and PE32+ optional headers.
|
||||||
|
ULONG imageSize;
|
||||||
|
memcpy(&imageSize, nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) +
|
||||||
|
FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage), sizeof(imageSize));
|
||||||
|
if (!DomitoImageRvaSpan(imageSize, (ULONG)dos.e_lfanew,
|
||||||
|
sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) +
|
||||||
|
FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage) + sizeof(imageSize), 1))
|
||||||
{
|
{
|
||||||
return STATUS_INVALID_IMAGE_FORMAT;
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
STRING currentFunctionName;
|
ULONG exportSize = 0;
|
||||||
|
const PVOID directory = G_Common.RtlImageDirectoryEntryToData(
|
||||||
const PULONG functionAddresses = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfFunctions);
|
ModuleBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize);
|
||||||
const PULONG functionNames = (PULONG)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNames);
|
const ULONG_PTR directoryOffset = (ULONG_PTR)directory - (ULONG_PTR)ModuleBase;
|
||||||
const PUSHORT functionOrdinals = (PUSHORT)((ULONG_PTR)ModuleBase + exportDirectory->AddressOfNameOrdinals);
|
if (!directory || directoryOffset > imageSize || exportSize < sizeof(IMAGE_EXPORT_DIRECTORY) ||
|
||||||
|
exportSize > imageSize - directoryOffset)
|
||||||
for (ULONG i = 0; i < exportDirectory->NumberOfNames; i++)
|
|
||||||
{
|
{
|
||||||
const char* functionName = (const char*)((ULONG_PTR)ModuleBase + functionNames[i]);
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
const USHORT functionOrdinal = functionOrdinals[i];
|
}
|
||||||
UNREFERENCED_PARAMETER(functionOrdinal);
|
IMAGE_EXPORT_DIRECTORY exports;
|
||||||
|
memcpy(&exports, directory, sizeof(exports));
|
||||||
const ULONG functionRva = functionAddresses[i];
|
if (!DomitoImageRvaSpan(imageSize, exports.AddressOfFunctions, exports.NumberOfFunctions, sizeof(ULONG)) ||
|
||||||
const PVOID functionAddress = (PVOID)((ULONG_PTR)ModuleBase + functionRva);
|
!DomitoImageRvaSpan(imageSize, exports.AddressOfNames, exports.NumberOfNames, sizeof(ULONG)) ||
|
||||||
|
!DomitoImageRvaSpan(imageSize, exports.AddressOfNameOrdinals, exports.NumberOfNames, sizeof(USHORT)))
|
||||||
RtlInitAnsiString(¤tFunctionName, functionName);
|
|
||||||
|
|
||||||
if (0 == RtlCompareString(FunctionName, ¤tFunctionName, TRUE))
|
|
||||||
{
|
{
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
}
|
||||||
|
for (ULONG i = 0; i < exports.NumberOfNames; ++i)
|
||||||
|
{
|
||||||
|
ULONG nameRva;
|
||||||
|
USHORT ordinal;
|
||||||
|
memcpy(&nameRva, base + exports.AddressOfNames + i * sizeof(ULONG), sizeof(nameRva));
|
||||||
|
memcpy(&ordinal, base + exports.AddressOfNameOrdinals + i * sizeof(USHORT), sizeof(ordinal));
|
||||||
|
if (nameRva >= imageSize || ordinal >= exports.NumberOfFunctions)
|
||||||
|
{
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
}
|
||||||
|
ULONG length = 0;
|
||||||
|
while (length < imageSize - nameRva && base[nameRva + length] != 0)
|
||||||
|
{
|
||||||
|
++length;
|
||||||
|
}
|
||||||
|
if (length == imageSize - nameRva || length > MAXUSHORT)
|
||||||
|
{
|
||||||
|
return STATUS_INVALID_IMAGE_FORMAT;
|
||||||
|
}
|
||||||
|
ANSI_STRING current;
|
||||||
|
current.Buffer = (PCHAR)(base + nameRva);
|
||||||
|
current.Length = (USHORT)length;
|
||||||
|
current.MaximumLength = (USHORT)length;
|
||||||
|
if (RtlCompareString(FunctionName, ¤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)
|
if (FunctionAddress)
|
||||||
{
|
{
|
||||||
status = STATUS_SUCCESS;
|
*FunctionAddress = base + functionRva;
|
||||||
*FunctionAddress = functionAddress;
|
|
||||||
}
|
}
|
||||||
break;
|
return STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
return STATUS_NOT_FOUND;
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_Success_(return == STATUS_SUCCESS)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
@@ -240,6 +304,68 @@ DomitoMemorySearchPattern(
|
|||||||
return STATUS_NOT_FOUND;
|
return STATUS_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_Success_(return == STATUS_SUCCESS)
|
||||||
|
_Must_inspect_result_
|
||||||
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
|
NTSTATUS
|
||||||
|
DomitoReadFileAt(
|
||||||
|
_In_ HANDLE FileHandle,
|
||||||
|
_Out_ PVOID Buffer,
|
||||||
|
_In_ ULONG BufferSize,
|
||||||
|
_In_ const LARGE_INTEGER* ByteOffset
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!FileHandle || !ByteOffset || ByteOffset->QuadPart < 0 || (!Buffer && BufferSize))
|
||||||
|
{
|
||||||
|
return STATUS_INVALID_PARAMETER;
|
||||||
|
}
|
||||||
|
if (!BufferSize)
|
||||||
|
{
|
||||||
|
return STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE eventHandle = nullptr;
|
||||||
|
OBJECT_ATTRIBUTES attributes;
|
||||||
|
InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr);
|
||||||
|
NTSTATUS status = ZwCreateEvent(&eventHandle, EVENT_ALL_ACCESS, &attributes,
|
||||||
|
NotificationEvent, FALSE);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
PVOID eventObject = nullptr;
|
||||||
|
status = ObReferenceObjectByHandle(eventHandle, SYNCHRONIZE, *ExEventObjectType,
|
||||||
|
KernelMode, &eventObject, nullptr);
|
||||||
|
if (!NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
ZwClose(eventHandle);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
IO_STATUS_BLOCK ioStatusBlock = {};
|
||||||
|
LARGE_INTEGER offset = *ByteOffset;
|
||||||
|
status = ZwReadFile(FileHandle, eventHandle, nullptr, nullptr, &ioStatusBlock,
|
||||||
|
Buffer, BufferSize, &offset, nullptr);
|
||||||
|
if (status == STATUS_PENDING)
|
||||||
|
{
|
||||||
|
// A private event identifies this request even if the file has other I/O.
|
||||||
|
// KernelMode + nonalertable + no timeout cannot return before completion.
|
||||||
|
KeWaitForSingleObject(eventObject, Executive, KernelMode, FALSE, nullptr);
|
||||||
|
status = ioStatusBlock.Status;
|
||||||
|
}
|
||||||
|
if (NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
status = ioStatusBlock.Status;
|
||||||
|
if (NT_SUCCESS(status) && ioStatusBlock.Information != BufferSize)
|
||||||
|
{
|
||||||
|
status = STATUS_END_OF_FILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObDereferenceObject(eventObject);
|
||||||
|
ZwClose(eventHandle);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
_Success_(return == STATUS_SUCCESS)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
_Must_inspect_result_
|
_Must_inspect_result_
|
||||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||||
@@ -250,33 +376,8 @@ DomitoReadFile(
|
|||||||
_In_ ULONG BufferSize
|
_In_ ULONG BufferSize
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
NTSTATUS status = STATUS_SUCCESS;
|
const LARGE_INTEGER offset = {};
|
||||||
IO_STATUS_BLOCK ioStatusBlock;
|
return DomitoReadFileAt(FileHandle, Buffer, BufferSize, &offset);
|
||||||
|
|
||||||
// Read the file into memory using ZwReadFile
|
|
||||||
if (!NT_SUCCESS(status = ZwReadFile(
|
|
||||||
FileHandle,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
NULL,
|
|
||||||
&ioStatusBlock,
|
|
||||||
Buffer,
|
|
||||||
BufferSize,
|
|
||||||
NULL,
|
|
||||||
NULL
|
|
||||||
)))
|
|
||||||
{
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the file was read successfully
|
|
||||||
if (!NT_SUCCESS(ioStatusBlock.Status))
|
|
||||||
{
|
|
||||||
return ioStatusBlock.Status;
|
|
||||||
}
|
|
||||||
|
|
||||||
// File read successfully
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_Success_(return == STATUS_SUCCESS)
|
_Success_(return == STATUS_SUCCESS)
|
||||||
@@ -306,7 +407,7 @@ DomitoGetProcessImageName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
CLIENT_ID cid;
|
CLIENT_ID cid;
|
||||||
cid.UniqueProcess = (HANDLE)ProcessId;
|
cid.UniqueProcess = ULongToHandle(ProcessId);
|
||||||
cid.UniqueThread = NULL;
|
cid.UniqueThread = NULL;
|
||||||
|
|
||||||
OBJECT_ATTRIBUTES objAttr;
|
OBJECT_ATTRIBUTES objAttr;
|
||||||
|
|||||||
@@ -127,6 +127,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ClInclude Include="..\include\Domito.MinCrypt.h" />
|
<ClInclude Include="..\include\Domito.MinCrypt.h" />
|
||||||
|
<ClInclude Include="..\include\Domito.MinCrypt.26100.h" />
|
||||||
<ClInclude Include="..\include\Domito.h" />
|
<ClInclude Include="..\include\Domito.h" />
|
||||||
<ClInclude Include="Domito.Internal.h" />
|
<ClInclude Include="Domito.Internal.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
<ClInclude Include="..\include\Domito.h">
|
<ClInclude Include="..\include\Domito.h">
|
||||||
<Filter>Header Files\Public</Filter>
|
<Filter>Header Files\Public</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="..\include\Domito.MinCrypt.26100.h">
|
||||||
|
<Filter>Header Files\Public</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="..\include\Domito.MinCrypt.h">
|
<ClInclude Include="..\include\Domito.MinCrypt.h">
|
||||||
<Filter>Header Files\Public</Filter>
|
<Filter>Header Files\Public</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
|||||||
+49
-17
@@ -1,30 +1,62 @@
|
|||||||
# Code Integrity regression tests
|
# Regression tests
|
||||||
|
|
||||||
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
|
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python tests\run-digest-tests.py
|
python tests\run-digest-tests.py
|
||||||
|
python tests\run-common-tests.py
|
||||||
|
python tests\run-ci-probe-tests.py
|
||||||
```
|
```
|
||||||
|
|
||||||
The runner extracts the production digest, pattern-search, algorithm-mapping,
|
Build outputs are temporary. No driver is installed or loaded. Test runners
|
||||||
and legacy-validation functions without changing their bodies. The C++ harness
|
extract the actual production functions or include the probe body unchanged;
|
||||||
uses real user-mode BCrypt hashing and test doubles for pool allocation, file
|
kernel calls, CI calls, and pool allocation are replaced with test doubles.
|
||||||
reads, and CI. No driver is installed or loaded. Build outputs are temporary.
|
The digest harness uses real BCrypt, Crypt32 and the Windows PE SIP provider.
|
||||||
|
|
||||||
Coverage includes PE32/PE32+ digest fixtures, checksum and certificate exclusions,
|
## Digest and legacy verification tests
|
||||||
a DOS stub larger than 4 MiB, truncated headers next to inaccessible guard pages,
|
|
||||||
invalid offsets and lengths, absent security-directory entries, injected hashing
|
|
||||||
and allocation failures, deterministic failure outputs, allocation ownership,
|
|
||||||
and rejection of successful-but-short certificate reads before invoking CI.
|
|
||||||
|
|
||||||
The tests do not validate CI trust policy, asynchronous kernel I/O behavior, or
|
Coverage includes PE32/PE32+ files, all four supported hash algorithms, checksum
|
||||||
general Authenticode hashing of unusual section layouts. The digest implementation
|
and certificate exclusions, overlays, reverse section-header order, multiple
|
||||||
still uses its existing linear byte-order convention and heuristic signature
|
certificate entries, certificate padding, unsupported section layouts, bounded
|
||||||
algorithm detection; replacing those requires a separate compatibility corpus.
|
DER algorithm discovery, SHA-1 pattern decoys, truncated ASN.1, explicit catalog
|
||||||
|
policy forwarding and cleanup between failed/successful CI attempts.
|
||||||
|
|
||||||
|
The test compares digests against `CryptSIPCreateIndirectData` and reads the
|
||||||
|
installed system `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` as a
|
||||||
|
real-file corpus. For embedded signatures, Crypt32 independently decodes the SPC
|
||||||
|
algorithm and expected file digest. These checks do not verify signature trust.
|
||||||
|
|
||||||
|
Guard pages cover truncated headers and invalid ranges in both compatibility
|
||||||
|
and strict paths. Fault injection covers allocation/hash failures, deterministic
|
||||||
|
outputs, short certificate reads and digest ownership. A DOS stub larger than
|
||||||
|
4 MiB exercises the removed scratch-buffer limit. Older entry points retain
|
||||||
|
legacy selection/layout behavior; strict Ex APIs enforce their documented subset.
|
||||||
|
|
||||||
|
## File and export helper tests
|
||||||
|
|
||||||
|
The common harness checks explicit and zero offsets, exact/short/failed reads,
|
||||||
|
pending completion, event/object cleanup, and error propagation. Export fixtures
|
||||||
|
cover nonidentity name-ordinal mappings, forwarders, empty slots, invalid spans,
|
||||||
|
unbounded strings, and deterministic failure outputs. The export API requires a
|
||||||
|
valid loader-mapped module; it is not an arbitrary file parser.
|
||||||
|
|
||||||
## File-object validation flag probe
|
## File-object validation flag probe
|
||||||
|
|
||||||
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
|
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
|
||||||
for `CiValidateFileObject` flag `0x20`. Run `python tests\run-ci-probe-tests.py`
|
for `CiValidateFileObject` flag `0x20`. The host runner tests probe plumbing with
|
||||||
from the same developer shell to test probe plumbing with mock CI. The kernel
|
mock CI. The kernel probe must still run in a suitable VM to verify actual
|
||||||
probe must still be run in a suitable VM to verify actual policy behavior.
|
policy behavior.
|
||||||
|
|
||||||
|
## Kernel compile checks and remaining validation
|
||||||
|
|
||||||
|
`mincrypt-layout-tests.cpp` asserts default modern x64/ARM64 and pre-Windows-10
|
||||||
|
compile layouts. On x64 it also includes the opt-in observed 26100 header's 14
|
||||||
|
layout assertions. Compile it with the same WDK/kernel architecture flags and
|
||||||
|
include paths as the library. This checks structure definitions, not runtime
|
||||||
|
compatibility on those systems. `ci-file-object-probe.cpp` is separately compiled
|
||||||
|
as kernel code for x64/ARM64; its real CI behavior is still untested.
|
||||||
|
|
||||||
|
The host tests do not establish CI trust policy, real asynchronous kernel I/O,
|
||||||
|
real policy ownership under Driver Verifier, revocation/timestamp behavior, or
|
||||||
|
all Windows-version/layout combinations. See the
|
||||||
|
[implementation ledger](../docs/implementation-status.md) for the remaining work.
|
||||||
@@ -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 <ntstatus.h>
|
||||||
#include <bcrypt.h>
|
#include <bcrypt.h>
|
||||||
#include <wintrust.h>
|
#include <wintrust.h>
|
||||||
|
#include <mssip.h>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
||||||
#define ASSERT(x) Check(!!(x), #x)
|
#define ASSERT(x) Check(!!(x), #x)
|
||||||
@@ -19,11 +21,21 @@
|
|||||||
#define PAGED_CODE() ((void)0)
|
#define PAGED_CODE() ((void)0)
|
||||||
#define MINCRYPT_SHA1_LENGTH 20
|
#define MINCRYPT_SHA1_LENGTH 20
|
||||||
#define MINCRYPT_SHA256_LENGTH 32
|
#define MINCRYPT_SHA256_LENGTH 32
|
||||||
|
#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
|
||||||
|
#define DOMITO_CI_SOURCE_EMBEDDED 1UL
|
||||||
|
#define DOMITO_CI_SOURCE_CATALOG 2UL
|
||||||
typedef const UCHAR* PCUCHAR;
|
typedef const UCHAR* PCUCHAR;
|
||||||
|
|
||||||
static unsigned checks, allocations, ciCalls, hashCalls;
|
static unsigned checks, allocations, ciCalls, hashCalls;
|
||||||
static int failOperation, failHashCall;
|
static int failOperation, failHashCall;
|
||||||
static ULONG_PTR bytesRead;
|
static ULONG_PTR bytesRead;
|
||||||
|
static bool allocatePolicies;
|
||||||
|
static NTSTATUS embeddedResult = STATUS_SUCCESS;
|
||||||
|
static NTSTATUS catalogResults[2] = {STATUS_SUCCESS, STATUS_SUCCESS};
|
||||||
|
static unsigned catalogCalls;
|
||||||
|
static ULONG expectedCatalogMask = DOMITO_CI_LEGACY_CATALOG_POLICY_BITS;
|
||||||
|
static BOOLEAN expectedTrustedMarker;
|
||||||
|
static LONGLONG observedFileSize = 1056;
|
||||||
static void Check(bool ok, const char* message)
|
static void Check(bool ok, const char* message)
|
||||||
{
|
{
|
||||||
++checks;
|
++checks;
|
||||||
@@ -61,33 +73,67 @@ static NTSTATUS TestFinishHash(BCRYPT_HASH_HANDLE h, PUCHAR b, ULONG n, ULONG f)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Minimal dependencies for the legacy wrapper; no actual CI code is invoked.
|
// Minimal dependencies for the legacy wrapper; no actual CI code is invoked.
|
||||||
struct MINCRYPT_POLICY_INFO { ULONG Size; };
|
struct MINCRYPT_POLICY_INFO { ULONG Size; PVOID ChainInfo; };
|
||||||
struct KAPC_STATE { ULONG unused; };
|
struct KAPC_STATE { ULONG unused; };
|
||||||
struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; };
|
struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; };
|
||||||
static PVOID PsInitialSystemProcess = nullptr;
|
static PVOID PsInitialSystemProcess = nullptr;
|
||||||
static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* p) { p->Size = 0; }
|
static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* p) {
|
||||||
|
if (p->ChainInfo) ExFreePoolWithTag(p->ChainInfo, DOMITO_POOL_TAG);
|
||||||
|
*p = {};
|
||||||
|
}
|
||||||
|
static void PopulatePolicies(MINCRYPT_POLICY_INFO* policy, MINCRYPT_POLICY_INFO* timestamp)
|
||||||
|
{
|
||||||
|
Check(!policy->ChainInfo && !timestamp->ChainInfo, "previous CI attempt released before reuse");
|
||||||
|
if (allocatePolicies) {
|
||||||
|
policy->Size = timestamp->Size = sizeof(*policy);
|
||||||
|
policy->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG);
|
||||||
|
timestamp->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG);
|
||||||
|
}
|
||||||
|
}
|
||||||
static void KeStackAttachProcess(PVOID, KAPC_STATE*) {}
|
static void KeStackAttachProcess(PVOID, KAPC_STATE*) {}
|
||||||
static void KeUnstackDetachProcess(KAPC_STATE*) {}
|
static void KeUnstackDetachProcess(KAPC_STATE*) {}
|
||||||
static NTSTATUS ZwReadFile(HANDLE, PVOID, PVOID, PVOID, IO_STATUS_BLOCK* io,
|
typedef PVOID PFILE_OBJECT;
|
||||||
PVOID buffer, ULONG size, LARGE_INTEGER*, PVOID)
|
static PVOID fileObjectType;
|
||||||
|
static PVOID* IoFileObjectType = &fileObjectType;
|
||||||
|
#define KernelMode 0
|
||||||
|
static NTSTATUS ObReferenceObjectByHandle(HANDLE, ULONG, PVOID, int, PVOID* object, PVOID)
|
||||||
{
|
{
|
||||||
io->Status = STATUS_SUCCESS;
|
*object = &fileObjectType;
|
||||||
io->Information = bytesRead;
|
return STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
static void ObDereferenceObject(PVOID) {}
|
||||||
|
static NTSTATUS FsRtlGetFileSize(PFILE_OBJECT, LARGE_INTEGER* size)
|
||||||
|
{
|
||||||
|
size->QuadPart = observedFileSize;
|
||||||
|
return STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
static NTSTATUS DomitoReadFileAt(HANDLE, PVOID buffer, ULONG size, const LARGE_INTEGER* offset)
|
||||||
|
{
|
||||||
|
Check(offset->QuadPart == 1024, "explicit certificate read offset");
|
||||||
std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size));
|
std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size));
|
||||||
return STATUS_SUCCESS;
|
return bytesRead == size ? STATUS_SUCCESS : STATUS_END_OF_FILE;
|
||||||
}
|
}
|
||||||
static NTSTATUS ZwWaitForSingleObject(HANDLE, BOOLEAN, PVOID) { return STATUS_SUCCESS; }
|
|
||||||
static NTSTATUS CiCheckSignedFile(PVOID, UINT32, ALG_ID, PVOID, UINT32,
|
static NTSTATUS CiCheckSignedFile(PVOID, UINT32, ALG_ID, PVOID, UINT32,
|
||||||
MINCRYPT_POLICY_INFO*, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*)
|
MINCRYPT_POLICY_INFO* policy, LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
|
||||||
{
|
{
|
||||||
++ciCalls;
|
++ciCalls;
|
||||||
return STATUS_SUCCESS;
|
Check(time->QuadPart == 0, "fresh signing time for embedded validation");
|
||||||
|
PopulatePolicies(policy, timestamp);
|
||||||
|
time->QuadPart = 123;
|
||||||
|
return embeddedResult;
|
||||||
}
|
}
|
||||||
static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN, UINT32, UINT32,
|
static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN reload,
|
||||||
MINCRYPT_POLICY_INFO*, PVOID, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*)
|
UINT32 trusted, UINT32 mask, MINCRYPT_POLICY_INFO* policy, PVOID,
|
||||||
|
LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
|
||||||
{
|
{
|
||||||
++ciCalls;
|
++ciCalls;
|
||||||
return STATUS_SUCCESS;
|
Check(mask == expectedCatalogMask && trusted == expectedTrustedMarker, "explicit catalog policy forwarded");
|
||||||
|
Check(reload == (catalogCalls != 0), "catalog reload sequence");
|
||||||
|
Check(time->QuadPart == 0, "fresh signing time for catalog attempt");
|
||||||
|
PopulatePolicies(policy, timestamp);
|
||||||
|
time->QuadPart = 456;
|
||||||
|
Check(catalogCalls < 2, "bounded catalog retries");
|
||||||
|
return catalogResults[catalogCalls++];
|
||||||
}
|
}
|
||||||
|
|
||||||
#define BCryptCreateHash TestCreateHash
|
#define BCryptCreateHash TestCreateHash
|
||||||
@@ -108,21 +154,32 @@ static std::vector<UCHAR> Image(bool pe64, ULONG nt = 128, bool certificate = fa
|
|||||||
std::memcpy(b.data() + nt, &signature, sizeof(signature));
|
std::memcpy(b.data() + nt, &signature, sizeof(signature));
|
||||||
IMAGE_FILE_HEADER f = {};
|
IMAGE_FILE_HEADER f = {};
|
||||||
f.NumberOfSections = 1;
|
f.NumberOfSections = 1;
|
||||||
|
f.Machine = pe64 ? IMAGE_FILE_MACHINE_AMD64 : IMAGE_FILE_MACHINE_I386;
|
||||||
f.SizeOfOptionalHeader = pe64 ? sizeof(IMAGE_OPTIONAL_HEADER64) : sizeof(IMAGE_OPTIONAL_HEADER32);
|
f.SizeOfOptionalHeader = pe64 ? sizeof(IMAGE_OPTIONAL_HEADER64) : sizeof(IMAGE_OPTIONAL_HEADER32);
|
||||||
std::memcpy(b.data() + nt + 4, &f, sizeof(f));
|
std::memcpy(b.data() + nt + 4, &f, sizeof(f));
|
||||||
IMAGE_OPTIONAL_HEADER64 h64 = {};
|
IMAGE_OPTIONAL_HEADER64 h64 = {};
|
||||||
IMAGE_OPTIONAL_HEADER32 h32 = {};
|
IMAGE_OPTIONAL_HEADER32 h32 = {};
|
||||||
h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
|
h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
|
||||||
h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16;
|
h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16;
|
||||||
|
h64.SectionAlignment = h32.SectionAlignment = 4096;
|
||||||
|
h64.FileAlignment = h32.FileAlignment = 512;
|
||||||
|
h64.SizeOfImage = h32.SizeOfImage = 8192;
|
||||||
|
h64.Subsystem = h32.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI;
|
||||||
h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384;
|
h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384;
|
||||||
if (certificate)
|
if (certificate)
|
||||||
{
|
{
|
||||||
h64.DataDirectory[4] = h32.DataDirectory[4] = { nt + 896, 32 };
|
h64.DataDirectory[4] = h32.DataDirectory[4] = { nt + 896, 32 };
|
||||||
ULONG length = 32;
|
ULONG length = 32;
|
||||||
std::memcpy(b.data() + nt + 896, &length, sizeof(length));
|
std::memcpy(b.data() + nt + 896, &length, sizeof(length));
|
||||||
|
USHORT revision = 0x0200, type = WIN_CERT_TYPE_PKCS_SIGNED_DATA;
|
||||||
|
std::memcpy(b.data() + nt + 900, &revision, 2);
|
||||||
|
std::memcpy(b.data() + nt + 902, &type, 2);
|
||||||
}
|
}
|
||||||
std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader);
|
std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader);
|
||||||
IMAGE_SECTION_HEADER section = {};
|
IMAGE_SECTION_HEADER section = {};
|
||||||
|
section.VirtualAddress = 4096;
|
||||||
|
section.Misc.VirtualSize = 512;
|
||||||
|
section.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ;
|
||||||
section.PointerToRawData = nt + 384; section.SizeOfRawData = 512;
|
section.PointerToRawData = nt + 384; section.SizeOfRawData = 512;
|
||||||
std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, §ion, sizeof(section));
|
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);
|
for (ULONG i = nt + 384; i < nt + 896; ++i) b[i] = (UCHAR)(i * 31);
|
||||||
@@ -187,10 +244,295 @@ static void GuardedInvalid(const std::vector<UCHAR>& b, NTSTATUS expected)
|
|||||||
UCHAR* input = region + span - b.size();
|
UCHAR* input = region + span - b.size();
|
||||||
std::memcpy(input, b.data(), b.size());
|
std::memcpy(input, b.data(), b.size());
|
||||||
Digest(input, (ULONG)b.size(), expected);
|
Digest(input, (ULONG)b.size(), expected);
|
||||||
|
ULONG strictSize = 0, securitySize = 0;
|
||||||
|
PVOID strictDigest = nullptr;
|
||||||
|
LPWIN_CERTIFICATE certificate = nullptr;
|
||||||
|
Check(!NT_SUCCESS(DomitoCalculatePortableExecutableDigestEx(input, (ULONG)b.size(), CALG_SHA256,
|
||||||
|
&strictSize, &strictDigest, &certificate, &securitySize)), "strict guard-page malformed input rejected");
|
||||||
|
Check(!strictSize && !strictDigest && !certificate && !securitySize, "strict malformed outputs cleared");
|
||||||
VirtualFree(region, 0, MEM_RELEASE);
|
VirtualFree(region, 0, MEM_RELEASE);
|
||||||
}
|
}
|
||||||
|
static std::vector<UCHAR> ExplicitDigest(const std::vector<UCHAR>& b, UINT32 calg,
|
||||||
|
NTSTATUS expected = STATUS_SUCCESS)
|
||||||
|
{
|
||||||
|
ULONG length = 0xdead, certSize = 0xdead;
|
||||||
|
PVOID digest = (PVOID)1;
|
||||||
|
LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1;
|
||||||
|
NTSTATUS status = DomitoCalculatePortableExecutableDigestEx(b.data(), (ULONG)b.size(),
|
||||||
|
calg, &length, &digest, &cert, &certSize);
|
||||||
|
Check(status == expected, "explicit digest status");
|
||||||
|
std::vector<UCHAR> result;
|
||||||
|
if (NT_SUCCESS(status))
|
||||||
|
{
|
||||||
|
Check(length == (calg == CALG_SHA1 ? 20u : calg == CALG_SHA256 ? 32u :
|
||||||
|
calg == CALG_SHA384 ? 48u : 64u), "explicit algorithm digest length");
|
||||||
|
result.assign((UCHAR*)digest, (UCHAR*)digest + length);
|
||||||
|
DomitoFreePortableExecutableDigest(digest);
|
||||||
|
}
|
||||||
|
else Check(!length && !digest && !cert && !certSize, "explicit failure outputs cleared");
|
||||||
|
Check(allocations == 0, "explicit digest allocation cleanup");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<UCHAR> SipDigest(const std::vector<UCHAR>& b, UINT32 calg)
|
||||||
|
{
|
||||||
|
WCHAR tempPath[MAX_PATH], tempName[MAX_PATH];
|
||||||
|
Check(GetTempPathW(MAX_PATH, tempPath) != 0, "temp path");
|
||||||
|
Check(GetTempFileNameW(tempPath, L"dci", 0, tempName) != 0, "temp name");
|
||||||
|
HANDLE file = CreateFileW(tempName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
|
||||||
|
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
|
||||||
|
Check(file != INVALID_HANDLE_VALUE, "SIP fixture file");
|
||||||
|
DWORD written = 0;
|
||||||
|
Check(WriteFile(file, b.data(), (DWORD)b.size(), &written, nullptr) && written == b.size(), "write SIP fixture");
|
||||||
|
GUID subject;
|
||||||
|
Check(CryptSIPRetrieveSubjectGuid(tempName, file, &subject) != FALSE, "PE SIP subject");
|
||||||
|
SIP_SUBJECTINFO info = {};
|
||||||
|
info.cbSize = sizeof(info);
|
||||||
|
info.pgSubjectType = &subject;
|
||||||
|
info.hFile = file;
|
||||||
|
info.pwsFileName = tempName;
|
||||||
|
info.dwEncodingType = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
|
||||||
|
info.DigestAlgorithm.pszObjId = const_cast<char*>(calg == CALG_SHA1 ? szOID_OIWSEC_sha1 :
|
||||||
|
calg == CALG_SHA256 ? szOID_NIST_sha256 : calg == CALG_SHA384 ? szOID_NIST_sha384 : szOID_NIST_sha512);
|
||||||
|
DWORD size = 0;
|
||||||
|
Check(CryptSIPCreateIndirectData(&info, &size, nullptr) != FALSE, "SIP size query");
|
||||||
|
std::vector<UCHAR> buffer(size);
|
||||||
|
auto indirect = (SIP_INDIRECT_DATA*)buffer.data();
|
||||||
|
Check(CryptSIPCreateIndirectData(&info, &size, indirect) != FALSE, "Windows SIP digest");
|
||||||
|
std::vector<UCHAR> result(indirect->Digest.pbData, indirect->Digest.pbData + indirect->Digest.cbData);
|
||||||
|
CloseHandle(file);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<UCHAR> Der(UCHAR tag, const std::vector<UCHAR>& value)
|
||||||
|
{
|
||||||
|
std::vector<UCHAR> out = {tag};
|
||||||
|
if (value.size() < 128) out.push_back((UCHAR)value.size());
|
||||||
|
else if (value.size() < 256) { out.push_back(0x81); out.push_back((UCHAR)value.size()); }
|
||||||
|
else { out.push_back(0x82); out.push_back((UCHAR)(value.size() >> 8)); out.push_back((UCHAR)value.size()); }
|
||||||
|
out.insert(out.end(), value.begin(), value.end());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
static std::vector<UCHAR> Join(std::initializer_list<std::vector<UCHAR>> values)
|
||||||
|
{
|
||||||
|
std::vector<UCHAR> out;
|
||||||
|
for (auto& value : values) out.insert(out.end(), value.begin(), value.end());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
static std::vector<UCHAR> SignedContent(UINT32 calg, bool nullParameters = true)
|
||||||
|
{
|
||||||
|
std::vector<UCHAR> oid = calg == CALG_SHA1 ? std::vector<UCHAR>{0x2B,0x0E,0x03,0x02,0x1A} :
|
||||||
|
std::vector<UCHAR>{0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,
|
||||||
|
(UCHAR)(calg == CALG_SHA256 ? 1 : calg == CALG_SHA384 ? 2 : 3)};
|
||||||
|
auto algorithm = Der(0x30, Join({Der(0x06, oid), nullParameters ? Der(0x05, {}) : std::vector<UCHAR>{}}));
|
||||||
|
auto digest = Der(0x30, Join({algorithm, Der(0x04, std::vector<UCHAR>(
|
||||||
|
calg == CALG_SHA1 ? 20 : calg == CALG_SHA256 ? 32 : calg == CALG_SHA384 ? 48 : 64, 0xAA))}));
|
||||||
|
// A complete SHA-1 DigestInfo decoy in the ignored data attribute must not
|
||||||
|
// override the selected SpcIndirectDataContent digest AlgorithmIdentifier.
|
||||||
|
auto data = Der(0x30, Der(0x04, {0x30,0x21,0x30,0x09,0x06,0x05,0x2B,0x0E,0x03,0x02,0x1A,0x05,0,0x04,0x14}));
|
||||||
|
auto indirect = Der(0x30, Join({data, digest}));
|
||||||
|
auto content = Der(0x30, Join({Der(0x06, {0x2B,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x01,0x04}), Der(0xA0, indirect)}));
|
||||||
|
auto signedData = Der(0x30, Join({Der(0x02, {1}), Der(0x31, algorithm), content, Der(0x31, {})}));
|
||||||
|
return Der(0x30, Join({Der(0x06, {0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x07,0x02}), Der(0xA0, signedData)}));
|
||||||
|
}
|
||||||
|
static void ExtendedTests()
|
||||||
|
{
|
||||||
|
for (bool pe64 : {false, true})
|
||||||
|
for (bool cert : {false, true})
|
||||||
|
for (UINT32 calg : std::initializer_list<UINT32>{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
|
||||||
|
{
|
||||||
|
auto b = Image(pe64, 128, cert);
|
||||||
|
Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP differential fixture");
|
||||||
|
if (!cert)
|
||||||
|
{
|
||||||
|
b.insert(b.end(), 48, 0x42); // Non-section overlay.
|
||||||
|
Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP overlay differential");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (bool pe64 : {false, true})
|
||||||
|
for (ULONG count : {0UL, 4UL})
|
||||||
|
{
|
||||||
|
auto absent = Image(pe64);
|
||||||
|
Set32(absent, pe64 ? 260 : 244, count);
|
||||||
|
ExplicitDigest(absent, CALG_SHA256, STATUS_NOT_SUPPORTED);
|
||||||
|
}
|
||||||
|
auto b = Image(true);
|
||||||
|
// Two contiguous sections whose headers are in reverse raw-offset order.
|
||||||
|
Set16(b, 134, 2);
|
||||||
|
IMAGE_SECTION_HEADER first = {}, second = {};
|
||||||
|
first.PointerToRawData = 768; first.SizeOfRawData = 256;
|
||||||
|
second.PointerToRawData = 512; second.SizeOfRawData = 256;
|
||||||
|
std::memcpy(b.data() + 392, &first, sizeof(first));
|
||||||
|
std::memcpy(b.data() + 432, &second, sizeof(second));
|
||||||
|
Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "section header order differential");
|
||||||
|
Set32(b, 392 + 20, 800);
|
||||||
|
ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT); // extends beyond EOF
|
||||||
|
Set32(b, 392 + 20, 640);
|
||||||
|
ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // overlap
|
||||||
|
b = Image(true);
|
||||||
|
Set32(b, 392 + 20, 528); Set32(b, 392 + 16, 496);
|
||||||
|
ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // gap
|
||||||
|
b = Image(true, 128, true);
|
||||||
|
b.insert(b.end(), 8, 0);
|
||||||
|
ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // nonterminal certificate
|
||||||
|
b = Image(true);
|
||||||
|
Set32(b, 392 + 20, 0xFFFFFFFF);
|
||||||
|
ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT);
|
||||||
|
ExplicitDigest(Image(true), 0x1234, STATUS_NOT_SUPPORTED);
|
||||||
|
b = Image(true, 128, true);
|
||||||
|
b.resize(1088);
|
||||||
|
Set32(b, 300, 64);
|
||||||
|
Set32(b, 1056, 32); Set16(b, 1060, 0x0200); Set16(b, 1062, 2);
|
||||||
|
Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "multiple certificate entries excluded");
|
||||||
|
Set32(b, 1056, 0);
|
||||||
|
ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT);
|
||||||
|
b = Image(true, 128, true);
|
||||||
|
Set32(b, 1024, 31); // Last entry's alignment padding is outside dwLength.
|
||||||
|
Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "certificate alignment padding");
|
||||||
|
for (int failure : {1, 2, 3, 4})
|
||||||
|
{
|
||||||
|
failOperation = failure;
|
||||||
|
failHashCall = 1;
|
||||||
|
hashCalls = 0;
|
||||||
|
ExplicitDigest(Image(true), CALG_SHA256,
|
||||||
|
failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL);
|
||||||
|
}
|
||||||
|
failOperation = 0;
|
||||||
|
for (UINT32 calg : std::initializer_list<UINT32>{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
|
||||||
|
{
|
||||||
|
auto content = SignedContent(calg);
|
||||||
|
auto signedImage = Image(true);
|
||||||
|
ULONG certLength = 8 + (ULONG)content.size();
|
||||||
|
ULONG tableSize = (certLength + 7) & ~7u;
|
||||||
|
signedImage.resize(1024 + tableSize);
|
||||||
|
Set32(signedImage, 296, 1024); Set32(signedImage, 300, tableSize);
|
||||||
|
Set32(signedImage, 1024, certLength);
|
||||||
|
Set16(signedImage, 1028, 0x0200); Set16(signedImage, 1030, 2);
|
||||||
|
std::memcpy(signedImage.data() + 1032, content.data(), content.size());
|
||||||
|
IMAGE_DATA_DIRECTORY directories[5] = {};
|
||||||
|
directories[4] = {1024, tableSize};
|
||||||
|
UINT32 actual = 0;
|
||||||
|
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||||
|
directories, &actual) == STATUS_SUCCESS && actual == calg, "bounded SPC digest algorithm, decoy ignored");
|
||||||
|
for (ULONG cut = 0; cut < content.size(); ++cut)
|
||||||
|
{
|
||||||
|
Set32(signedImage, 1024, cut + 8);
|
||||||
|
actual = 0xDEAD;
|
||||||
|
Check(!NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx(signedImage.data(),
|
||||||
|
(ULONG)signedImage.size(), directories, &actual)) && actual == 0, "truncated DER rejected");
|
||||||
|
}
|
||||||
|
Set32(signedImage, 1024, tableSize);
|
||||||
|
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||||
|
directories, &actual) == STATUS_SUCCESS && actual == calg, "bounded zero padding inside dwLength");
|
||||||
|
if (tableSize > certLength)
|
||||||
|
{
|
||||||
|
signedImage.back() = 0x7F;
|
||||||
|
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||||
|
directories, &actual) == STATUS_INVALID_IMAGE_FORMAT && actual == 0, "nonzero DER trailing bytes rejected");
|
||||||
|
signedImage.back() = 0;
|
||||||
|
}
|
||||||
|
Set32(signedImage, 1024, certLength);
|
||||||
|
signedImage[1033] = 0x80; // Indefinite length forbidden in this DER subset.
|
||||||
|
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||||
|
directories, &actual) == STATUS_INVALID_IMAGE_FORMAT, "indefinite DER rejected");
|
||||||
|
}
|
||||||
|
for (bool nullParameters : {false, true})
|
||||||
|
{
|
||||||
|
auto content = SignedContent(CALG_SHA256, nullParameters);
|
||||||
|
auto pe = Image(true);
|
||||||
|
const ULONG length = 8 + (ULONG)content.size();
|
||||||
|
const ULONG tableSize = (length + 7) & ~7u;
|
||||||
|
pe.resize(1024 + tableSize);
|
||||||
|
Set32(pe, 1024, length); Set16(pe, 1028, 0x0200); Set16(pe, 1030, 2);
|
||||||
|
std::memcpy(pe.data() + 1032, content.data(), content.size());
|
||||||
|
IMAGE_DATA_DIRECTORY dirs[5] = {};
|
||||||
|
dirs[4] = {1024, tableSize};
|
||||||
|
UINT32 algorithm = 0;
|
||||||
|
Check(DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) == STATUS_SUCCESS &&
|
||||||
|
algorithm == CALG_SHA256, "AlgorithmIdentifier NULL optional");
|
||||||
|
// Change both copies of the SHA-256 OID to an unsupported family member.
|
||||||
|
const UCHAR oid[] = {0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01};
|
||||||
|
for (size_t i = 1032; i + sizeof(oid) <= pe.size(); ++i)
|
||||||
|
if (!std::memcmp(pe.data() + i, oid, sizeof(oid))) pe[i + sizeof(oid) - 1] = 0x7F;
|
||||||
|
Check(DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) == STATUS_NOT_SUPPORTED &&
|
||||||
|
!algorithm, "unsupported SPC algorithm does not default to SHA1");
|
||||||
|
}
|
||||||
|
IMAGE_DATA_DIRECTORY dirs[5] = {};
|
||||||
|
UINT32 actual = 123;
|
||||||
|
Check(DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual) == STATUS_NOT_FOUND &&
|
||||||
|
actual == 0, "absent DER does not silently select SHA1");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SystemImageTests()
|
||||||
|
{
|
||||||
|
WCHAR system[MAX_PATH];
|
||||||
|
Check(GetSystemDirectoryW(system, MAX_PATH) != 0, "system directory");
|
||||||
|
for (const WCHAR* name : {L"ci.dll", L"ntdll.dll", L"kernel32.dll", L"notepad.exe"})
|
||||||
|
{
|
||||||
|
std::wstring path = std::wstring(system) + L"\\" + name;
|
||||||
|
HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE,
|
||||||
|
nullptr, OPEN_EXISTING, 0, nullptr);
|
||||||
|
Check(file != INVALID_HANDLE_VALUE, "system PE corpus file");
|
||||||
|
LARGE_INTEGER size;
|
||||||
|
Check(GetFileSizeEx(file, &size) && size.QuadPart > 0 && size.QuadPart < 32 * 1024 * 1024, "bounded system PE size");
|
||||||
|
std::vector<UCHAR> b((size_t)size.QuadPart);
|
||||||
|
DWORD read;
|
||||||
|
Check(ReadFile(file, b.data(), (DWORD)b.size(), &read, nullptr) && read == b.size(), "read system PE fixture");
|
||||||
|
CloseHandle(file);
|
||||||
|
for (UINT32 calg : std::initializer_list<UINT32>{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
|
||||||
|
Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "real system PE Windows SIP differential");
|
||||||
|
IMAGE_DOS_HEADER dos;
|
||||||
|
std::memcpy(&dos, b.data(), sizeof(dos));
|
||||||
|
USHORT magic;
|
||||||
|
const ULONG optional = (ULONG)dos.e_lfanew + 24;
|
||||||
|
std::memcpy(&magic, b.data() + optional, 2);
|
||||||
|
const ULONG directory = magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC ?
|
||||||
|
FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory) : FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory);
|
||||||
|
IMAGE_DATA_DIRECTORY dirs[5];
|
||||||
|
std::memcpy(dirs, b.data() + optional + directory, sizeof(dirs));
|
||||||
|
if (dirs[4].Size)
|
||||||
|
{
|
||||||
|
UINT32 actual;
|
||||||
|
Check(NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual)),
|
||||||
|
"real embedded SPC digest algorithm");
|
||||||
|
// Independent Windows ASN.1 decoder: extract signed content with
|
||||||
|
// CryptQueryObject/CryptMsg, then decode SPC_INDIRECT_DATA_OBJID.
|
||||||
|
ULONG certLength;
|
||||||
|
std::memcpy(&certLength, b.data() + dirs[4].VirtualAddress, 4);
|
||||||
|
CRYPT_DATA_BLOB blob = {certLength - 8, b.data() + dirs[4].VirtualAddress + 8};
|
||||||
|
HCRYPTMSG message = nullptr;
|
||||||
|
Check(CryptQueryObject(CERT_QUERY_OBJECT_BLOB, &blob, CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED,
|
||||||
|
CERT_QUERY_FORMAT_FLAG_BINARY, 0, nullptr, nullptr, nullptr, nullptr, &message, nullptr) != FALSE,
|
||||||
|
"Windows decode PKCS7");
|
||||||
|
DWORD contentSize = 0;
|
||||||
|
Check(CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, nullptr, &contentSize) != FALSE,
|
||||||
|
"Windows SPC content size");
|
||||||
|
std::vector<UCHAR> content(contentSize);
|
||||||
|
Check(CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, content.data(), &contentSize) != FALSE,
|
||||||
|
"Windows SPC content");
|
||||||
|
SIP_INDIRECT_DATA* indirect = nullptr;
|
||||||
|
DWORD decodedSize = 0;
|
||||||
|
Check(CryptDecodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID,
|
||||||
|
content.data(), contentSize, CRYPT_DECODE_ALLOC_FLAG, nullptr, &indirect, &decodedSize) != FALSE,
|
||||||
|
"Windows decode indirect data");
|
||||||
|
const char* oid = indirect->DigestAlgorithm.pszObjId;
|
||||||
|
const UINT32 expected = !std::strcmp(oid, szOID_OIWSEC_sha1) ? CALG_SHA1 :
|
||||||
|
!std::strcmp(oid, szOID_NIST_sha256) ? CALG_SHA256 :
|
||||||
|
!std::strcmp(oid, szOID_NIST_sha384) ? CALG_SHA384 : CALG_SHA512;
|
||||||
|
Check(actual == expected, "SPC digest algorithm matches Windows ASN1 decoder");
|
||||||
|
auto digest = ExplicitDigest(b, actual);
|
||||||
|
Check(digest.size() == indirect->Digest.cbData &&
|
||||||
|
!std::memcmp(digest.data(), indirect->Digest.pbData, digest.size()), "computed digest matches real embedded SPC digest");
|
||||||
|
LocalFree(indirect);
|
||||||
|
CryptMsgClose(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
|
SystemImageTests();
|
||||||
|
ExtendedTests();
|
||||||
for (bool pe64 : {false, true})
|
for (bool pe64 : {false, true})
|
||||||
{
|
{
|
||||||
for (bool certificate : {false, true})
|
for (bool certificate : {false, true})
|
||||||
@@ -255,5 +597,43 @@ int main()
|
|||||||
Check(NT_SUCCESS(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory,
|
Check(NT_SUCCESS(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory,
|
||||||
&policy, &signing, ×tamp)), "complete certificate read accepted");
|
&policy, &signing, ×tamp)), "complete certificate read accepted");
|
||||||
Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once");
|
Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once");
|
||||||
std::printf("PASS: %u checks (guarded malformed inputs, PE32/PE32+, CNG digests, failure cleanup, short reads).\n", checks);
|
// Failure paths must release previous attempts before embedded/catalog/reload reuse.
|
||||||
|
allocatePolicies = true;
|
||||||
|
embeddedResult = STATUS_INVALID_IMAGE_HASH;
|
||||||
|
catalogResults[0] = STATUS_INVALID_IMAGE_HASH;
|
||||||
|
catalogResults[1] = STATUS_SUCCESS;
|
||||||
|
catalogCalls = 0;
|
||||||
|
expectedCatalogMask = 0x8000;
|
||||||
|
expectedTrustedMarker = TRUE;
|
||||||
|
ULONG source = 0xDEAD;
|
||||||
|
Check(NT_SUCCESS(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA1,
|
||||||
|
&directory, &policy, &signing, ×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')
|
test_source = Path(__file__).with_name('digest-tests.cpp')
|
||||||
subprocess.run(['cl', '/nologo', '/EHsc', '/W4', '/WX', '/std:c++17',
|
subprocess.run(['cl', '/nologo', '/EHsc', '/W4', '/WX', '/std:c++17',
|
||||||
'/I' + str(build), str(test_source), '/Fe' + str(build/'digest-tests.exe'),
|
'/I' + str(build), str(test_source), '/Fe' + str(build/'digest-tests.exe'),
|
||||||
'/Fo' + str(build/'digest-tests.obj'), '/link', 'bcrypt.lib'], cwd=build, check=True)
|
'/Fo' + str(build/'digest-tests.obj'), '/link', 'bcrypt.lib', 'crypt32.lib'], cwd=build, check=True)
|
||||||
subprocess.run([str(build/'digest-tests.exe')], cwd=build, check=True)
|
subprocess.run([str(build/'digest-tests.exe')], cwd=build, check=True)
|
||||||
Reference in new issue
Block a user