From a491bbd3f8d8ceba8d95d26a90fe968cfcc667fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=B6glinger-Stelzer?= Date: Tue, 27 Oct 2026 00:16:42 +0100 Subject: [PATCH] Add reusable certificate and file validation helpers Fix process-name ownership and module lookup outputs, add explicit SPKI hashing and bounded chain access, and improve WDM integration. Deprecate superseded entry points with migration guidance and align branch code and tests with the library style. --- Domito.props | 5 +- README.md | 14 +- docs/ci-validation-helpers.md | 12 +- docs/consumer-helpers.md | 247 ++++++ docs/implementation-status.md | 21 +- include/Domito.Consumer.h | 200 +++++ include/Domito.MinCrypt.26100.h | 189 +++-- include/Domito.MinCrypt.Historical.h | 65 +- include/Domito.MinCrypt.h | 422 +++++------ include/Domito.h | 107 +-- src/Domito.Certificate.cpp | 396 ++++++++++ src/Domito.CodeIntegrity.Validation.cpp | 173 ++++- src/Domito.CodeIntegrity.cpp | 559 ++++++++++---- src/Domito.Consumer.cpp | 395 ++++++++++ src/Domito.cpp | 265 ++++--- src/Domito.vcxproj | 3 + src/Domito.vcxproj.filters | 9 + tests/README.md | 27 + tests/ci-file-object-probe-tests.cpp | 131 +++- tests/ci-file-object-probe.cpp | 38 +- tests/ci-file-object-probe.h | 6 +- tests/ci-validation-tests.cpp | 538 ++++++++++--- tests/common-tests.cpp | 437 ++++++++--- tests/consumer-helper-tests.cpp | 967 ++++++++++++++++++++++++ tests/digest-tests.cpp | 911 +++++++++++++++++----- tests/mincrypt-layout-tests.cpp | 42 +- tests/run-ci-probe-tests.py | 36 +- tests/run-ci-validation-tests.py | 64 +- tests/run-common-tests.py | 39 +- tests/run-consumer-helper-tests.py | 71 ++ tests/run-deprecation-tests.py | 94 +++ tests/run-digest-tests.py | 57 +- 32 files changed, 5404 insertions(+), 1136 deletions(-) create mode 100644 docs/consumer-helpers.md create mode 100644 include/Domito.Consumer.h create mode 100644 src/Domito.Certificate.cpp create mode 100644 src/Domito.Consumer.cpp create mode 100644 tests/consumer-helper-tests.cpp create mode 100644 tests/run-consumer-helper-tests.py create mode 100644 tests/run-deprecation-tests.py diff --git a/Domito.props b/Domito.props index 40705ee..251f70f 100644 --- a/Domito.props +++ b/Domito.props @@ -4,14 +4,15 @@ <_PropertySheetDisplayName>Domito Kernel Library - $(SolutionDir)..\Domito + $(MSBuildThisFileDirectory) + $(Platform) $(DomitoRootPath)\include;$(IntDir);%(AdditionalIncludeDirectories) - $(DomitoRootPath)\lib\$(DDKPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DDKPlatform)\ci.lib;$(DDK_LIB_PATH)cng.lib;%(AdditionalDependencies); + $(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\ci.lib;cng.lib;%(AdditionalDependencies); diff --git a/README.md b/README.md index 348eea1..bf5e369 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,14 @@ algorithm-explicit embedded verification using CI. First-success selection is an opt-in x64 experiment; existing APIs retain their behavior. Always enforce the expected signer/trust policy separately from a successful CI return status. +## Consumer helpers + +[Migration and contracts](docs/consumer-helpers.md) cover file-handle/path CI +validation, bounded x64 chain views, optional publisher-name extraction, +certificate/SPKI hashing (including explicit legacy content-only pins), strict +hex conversion, process-name cleanup and combined module/export lookup. +Application authorization and trust policies remain caller-provided. + ## How to use - Add the `include` directory to your project's headers search path. @@ -63,7 +71,11 @@ the expected signer/trust policy separately from a successful CI return status. ### Using the property sheet -Copy the provided `Domito.props` file into the directory of your `.vcxproj` file and customize it like below: +Import `Domito.props` from the Domito package, or set `DomitoRootPath` explicitly +if copying the sheet elsewhere. Its default root is its own directory. The sheet +links both WDM and KMDF final drivers; static-library consumers must also import +it into the final driver project. `DomitoLibraryPlatform` defaults to `$(Platform)`. +For example: ```xml diff --git a/docs/ci-validation-helpers.md b/docs/ci-validation-helpers.md index 22c3e91..9727193 100644 --- a/docs/ci-validation-helpers.md +++ b/docs/ci-validation-helpers.md @@ -55,8 +55,10 @@ NTSTATUS status = DomitoValidateFileObjectEx( DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE, 0, DomitoCiSignatureSelectionFirstSuccessfulExperimental, - &result); -if (NT_SUCCESS(status)) { + &result +); +if (NT_SUCCESS(status)) +{ // Apply your expected-certificate/trust policy to result.PolicyInfo. // result.SigningTime belongs to that selected verification result. } @@ -83,9 +85,9 @@ successful catalog verification. ```c DOMITO_CI_VALIDATION_RESULT result = {0}; -NTSTATUS status = DomitoValidatePortableExecutableEmbeddedSignature( - stablePeBytes, peSize, CALG_SHA_256, &result); -if (NT_SUCCESS(status)) { +NTSTATUS status = DomitoValidatePortableExecutableEmbeddedSignature(stablePeBytes, peSize, CALG_SHA_256, &result); +if (NT_SUCCESS(status)) +{ // Enforce the expected certificate and intended trust policy here. } DomitoFreeCiValidationResult(&result); diff --git a/docs/consumer-helpers.md b/docs/consumer-helpers.md new file mode 100644 index 0000000..4fa4333 --- /dev/null +++ b/docs/consumer-helpers.md @@ -0,0 +1,247 @@ +# Migrating consumer Authenticode helpers + +These helpers centralize file validation, certificate inspection, hashing and +allocation cleanup. The contracts keep application identity and trust policy +explicit. Host tests and kernel compilation do not establish new CI runtime +guarantees. + +## Verified fixes and existing coverage + +- **Implementation defect fixed:** `DomitoGetProcessImageName` freed its allocation + on a failed second query but left that freed address in the caller's output. + It also left old caller output untouched on earlier failures. It now clears + the output first, publishes only a successful bounded result, and frees its + local allocation on all other paths. The process handle uses limited-query + access (`0x1000`) instead of `PROCESS_ALL_ACCESS`. A PID is resolved once to a + process handle; no later query re-resolves that PID. +- **Output-contract defect fixed:** `DomitoFindModuleBaseAddress` could preserve + an old address when lookup failed. It now clears the optional output first and + validates the counted module name. The export-by-module wrapper preserves the + underlying status, including missing exports and unsupported forwarders. +- **Annotation defect fixed:** the `DomitoReadFileAt` definition's single-object + output annotation disagreed with its public byte-count annotation. Both now + describe `BufferSize` writable bytes. +- **Already addressed before this review:** exact file reads return + `STATUS_END_OF_FILE` on short reads and wait for their own pending request. + Explicit digest helpers bound PE32/PE32+ headers, sections, certificate spans + and DER navigation. Certificate pointers borrow the stable input buffer. + [CI validation results](ci-validation-helpers.md) initialize outputs and own + both policies, including diagnostic allocations on failure. The current raw + CI declaration names the flags and byte-sized signing level; `0x20` broadens + root acceptance in examined builds, rather than declaring a file to be PE. + +Applications should avoid truncating file lengths to `EndOfFile.LowPart`, +reparsing fixed-kind PE headers, consuming uninitialized CI policies, skipping +cleanup on failure, or accepting invalid hexadecimal digits. The helpers below +provide reusable primitives without defining application authorization policy. + +## File object, handle and path validation + +`DomitoValidateFileObjectEx`, `DomitoValidateFileHandleEx` and +`DomitoValidateFilePathEx` expose the same explicit flags, requested signing +level, selection mode and owning `DOMITO_CI_VALIDATION_RESULT`. None adds flags +or falls back to a different validation policy. They require PASSIVE_LEVEL and +Windows 10 compile targets; the CI imports must exist on the target OS. +They do not require `DomitoInit`. + +The handle wrapper requires a trusted **kernel handle** with `FILE_READ_DATA`. +It references its FILE_OBJECT, releases that reference on every exit and leaves +the caller's handle open. Never pass an unchecked user handle: the reference is +made using KernelMode. The caller must keep the handle valid during validation. + +The path wrapper accepts a counted absolute native path, for example +`\??\C:\Program Files\Example\app.exe`. It creates a read-only kernel handle, +shares only reads, references that same opened file for CI, and closes its own +handle. Existing incompatible writers cause an open failure. Special kernel APCs +must be enabled for synchronous I/O. Reparse points may redirect the open; +validation describes the opened object. It does not establish the identity of +a previously looked-up process image or certify in-memory process contents. +File-object/handle callers must independently provide stable file bytes. + +```c +DOMITO_CI_VALIDATION_RESULT result = {0}; +NTSTATUS status = DomitoValidateFilePathEx(&nativePath, selectedCiFlags, 0, DomitoCiSignatureSelectionNative, &result); +if (NT_SUCCESS(status) && NT_SUCCESS(result.PolicyInfo.VerificationStatus)) +{ + // Apply caller-provided certificate/publisher/pin policy here. + // A nonzero SigningTime is a separate consumer requirement, if desired. +} +DomitoFreeCiValidationResult(&result); // Also on CI or application-policy failure. +``` + +Modern file-object verification can select a preferred Microsoft nested signer +where direct embedded verification selected the company signer. Changing paths +therefore requires an explicit migration decision. Keep +`DomitoValidatePortableExecutableEmbeddedSignature` for the stable PE-buffer, +explicit-algorithm route. Its supported PE subset and nested-selection limitations +still apply. It verifies the original first WIN_CERTIFICATE through CI, with no +algorithm retry or catalog fallback. Existing legacy/catalog APIs remain +available with explicit catalog policy; new convenience wrappers do not invoke +them implicitly. An absent optional API is not permission to relax trust policy. + +## Certificate fingerprints and SPKI pins + +`DomitoGetCertificateSpki` extracts two borrowed views from stable certificate +DER: `Encoded` contains the complete SPKI SEQUENCE TLV; `Content` omits that +sequence's tag and length. It handles optional X.509 version fields and bounded, +minimal definite DER lengths. It validates navigation/framing and the SPKI field +shape, rather than every X.509 field, extension, name, algorithm parameter or +signature. It does not verify trust. Failed extraction clears both views. + +`DomitoHashBuffer` hashes exact bytes using SHA-1/256/384/512 and Windows CNG. +`HashSize` is capacity on entry, actual length on success, required length on +`STATUS_BUFFER_TOO_SMALL`, and zero on other failures. A NULL hash buffer queries +the required size. Operational CNG failures erase the selected digest bytes. +Input/output buffers must not overlap. Only consume a digest after success. + +`DomitoHashCertificate` takes an explicit `DOMITO_CERTIFICATE_HASH_KIND`: + +- `DomitoCertificateHashDer`: exact complete certificate bytes, suitable for a + DER thumbprint. This mode hashes bytes; it does not parse or validate them. +- `DomitoCertificateHashSpkiDer`: complete SPKI encoding, including outer TLV. +- `DomitoCertificateHashSpkiContentLegacy`: **preserves the consumers' existing + content-only convention**. Existing pins must use this mode. Switching to the + complete encoding requires recomputing pins from the intended certificates; + do not silently treat hashes from the two representations as interchangeable. + +```c +UCHAR pin[MINCRYPT_SHA256_LENGTH]; +ULONG pinSize = sizeof(pin); +status = + DomitoHashCertificate(cert.Data, cert.Size, DomitoCertificateHashSpkiContentLegacy, CALG_SHA_256, pin, &pinSize); +``` + +`DomitoDecodeHex` accepts an exact counted Unicode hex string for a caller-selected +binary length, rejects invalid digits/odd or mismatched lengths, and zeroes its +destination on malformed input. `DomitoEncodeHex` writes uppercase Unicode hex +with a NUL terminator; its capacity is in WCHARs including the terminator. +Decode configuration pins once into binary and compare like representations. +The library embeds no publisher names, root pins, or certificate allowlists. +Hashing does not prove that a certificate is a root or trusted issuer. + +## Chain inspection and publisher ownership + +`DomitoGetCiChainView` requires a known, readable, initialized CI-owned policy +and an explicit `DomitoCiChainLayoutObservedX64` argument. This asserts the +sampled x64 common prefix (+0x28 elements, +0x30 count, +0x34 opaque RootId, +minimum 56-byte header, 120-byte elements). It is **not** automatic OS/layout +detection. Non-x64 builds return `STATUS_NOT_SUPPORTED`; allocation size and +compile-target NTDDI cannot establish runtime layout compatibility. + +The accessor bounds the element array and every exposed certificate/name blob +within CI's owned allocation before returning a view. It rejects oversized hash +lengths and absent certificate bytes. It exposes neither trailing attributes nor +EKU/public-key arrays. It cannot make a forged or freed kernel pointer safe; +allocation extent/readability and the asserted layout remain caller preconditions. +All returned pointers expire when the policy is freed or its result reused. + +Sampled CI results are leaf-first. Inspect `Elements[0]` for that selected leaf. +`Elements[Count-1]` is the last represented certificate; this API does **not** +promise it is self-signed, the configured trust anchor, or that the chain includes +every certificate through a root. `RootId` is opaque and does not establish those +properties. A consumer requiring a specific root must establish its intended CI +policy and represented-anchor assumptions instead of inferring trust from position. + +The accessor deliberately imposes no bytewise issuer/next-subject equality. +CI name blobs are not an independent chain verifier; +their encoding and representation are version-sensitive. Current evidence does +not establish universal byte equality as a CI API contract. Preserve such a check +as an explicit downstream restriction if compatibility demands it, and assess its +false-rejection risk. CI return status, policy verification status and application +identity/pin requirements remain separate checks. + +`DomitoGetCertificatePublisherName` dynamically resolves the optional CI export +instead of adding a static import. It requires `DomitoInit`; missing modules, +exports and unsupported export forwarders propagate their lookup status. +The caller passes an empty `UNICODE_STRING`, and uses +`DomitoFreeCertificatePublisherName` to release/reset it. Partial allocations on +failure are cleaned internally; the destructor is safe again afterward. Its +private pool allocator is independent of configurable Domito callbacks. Never +free this string through `G_Memory`, RtlFreeUnicodeString or the process-name free +API. Display-name comparison remains caller policy, not certificate identity. + +## Process names, allocators and optional exports + +`DomitoGetProcessImageName` returns one Domito allocation containing the +`UNICODE_STRING` and its buffer. `DomitoFreeProcessImageName` frees the entire +allocation once, without querying callbacks in consumer code. Its output is +NULL on failure. The existing ULONG PID interface remains; a PID lookup can race +process exit/reuse before the initial open and is not a durable process identity. + +`DomitoGetProcessImageNameEx` instead accepts a caller-referenced PEPROCESS. +It uses `SeLocateProcessImageName`, copies the name into the same Domito-owned +representation and frees the OS allocation internally. The caller must keep the +process referenced for the call. No Domito initialization is needed for this Ex +API. The PID API still requires `DomitoInit` for its query routine. +Neither name API proves that a subsequent path open addresses the original +image file. WDF/requestor-process lifetime and authorization remain downstream. + +**Allocator contract:** the existing callback API does not attach allocator +provenance to each result and is not synchronized for concurrent replacement. +Configure a compatible allocate/free pair during initialization, before concurrent +calls, and keep it unchanged until all callback-backed allocations and in-flight +calls are released. A dedicated free name does not make changing callback pairs +with outstanding allocations safe. Do not retrofit a hidden prefix onto existing +allocations: callers may already free the public base address with their callback. +CI policies and the publisher helper use their dedicated free APIs instead. + +`DomitoFindModuleExportedFunctionAddress` combines the existing full-module-path +and export-name lookups, clears its output and preserves useful failure status. +It requires `DomitoInit`, and does not pin the module. The caller must guarantee +that the module remains mapped throughout lookup and use of its returned address. + +## Integration and consumer boundary + +Import `Domito.props` from the Domito package location or override +`DomitoRootPath`. The default root is the sheet's own directory. x64/ARM64 and +Debug/Release remain selected by the project; `DomitoLibraryPlatform` can override +the library platform directory. Both WDM and KMDF final driver projects receive +Domito, CI and CNG dependencies. Importing a sheet into a static library does not +make link dependencies transitive: import it into the final driver as well. + +WDF caches, PID caches, administrator checks, developer bypasses, request allowlists, +build expiry and selected-signing-time requirements stay in consumer code. +No default trust evaluator is added: that would silently choose policy absent +from the generic SDK. WPP diagnostics can enumerate the bounded chain view without +fixed-depth indentation arrays. Report the returned NTSTATUS and local policy +failure reason separately rather than mapping all failures inside the SDK. + +The new host suite compiles the production code unchanged apart from its kernel +include, uses deterministic kernel doubles for ownership/failure paths, compares +SPKI bytes and fingerprints with Crypt32, and uses real CNG for hashing. It tests +both architecture conditional branches, which is not ARM64 runtime emulation. +Real WDK compilation separately covers x64 and ARM64. These checks do not certify +kernel CI behavior, every chain layout, or full X.509 validation. + +References: [SeLocateProcessImageName ownership](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-selocateprocessimagename), +[BCryptCreateHash memory management and kernel requirements](https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptcreatehash). + +## Deprecated compatibility entry points + +The public header uses MSVC `__declspec(deprecated("migration guidance"))` for +four older functions. Calling or taking their address produces C4996 in C and +C++; declarations and binary entry points remain available. Projects treating +warnings as errors must migrate, or suppress C4996 locally around a deliberate +compatibility call. Do not disable the warning globally to conceal accidental use. + +- `DomitoGetPortableExecutableDigestKind` → + `DomitoGetPortableExecutableDigestKindEx`: check NTSTATUS; unrecognized input + no longer silently chooses SHA-1. Neither function validates signature trust. +- `DomitoCalculatePortableExecutableDigest` → + `DomitoCalculatePortableExecutableDigestEx`: select the algorithm explicitly + and review the supported layout/rejection rules. This is not a drop-in semantic + change for arbitrary files previously accepted by linear hashing. +- `DomitoValidateFileLegacyMode` → `DomitoValidateFileLegacyModeEx`: select catalog + policy and trusted-catalog requirements explicitly, and consume the validation + source. The Ex legacy path remains available where catalog/older-OS behavior + is required; migration does not implicitly switch to modern signer selection. +- `DomitoGetProcessImageName` → `DomitoGetProcessImageNameEx`: acquire and hold the + intended PEPROCESS reference, then use the dedicated name destructor. Looking + up a PID immediately before the Ex call cannot eliminate an earlier PID-reuse + race; obtain the process reference from the operation's intended identity. + This migration does not solve subsequent path-reopen identity races. + +No old type has an unconditional replacement. Raw CI/mincrypt types still +describe the native ABI; the owning validation result and borrowed chain view +serve different purposes. Raw Windows CI exports, allocator callbacks, module +lookup primitives, exact file reads and digest cleanup remain supported. diff --git a/docs/implementation-status.md b/docs/implementation-status.md index ec170d4..30d865f 100644 --- a/docs/implementation-status.md +++ b/docs/implementation-status.md @@ -102,7 +102,26 @@ and compilation do not close the remaining work: ## Checks -Run the four host runners in `tests/README.md`. Kernel compilation also covers +Run the six host runners in `tests/README.md`. Kernel compilation also covers the default modern x64/ARM64 structures, the pre-Windows-10 x64 compile variant, and 14 assertions in the 26100 opt-in header plus 12 historical-view assertions. Those are build checks, not a claim that historical or ARM64 CI runtime ABI has been verified. + +## Consumer-driven helpers + +The [consumer migration review](consumer-helpers.md) separates verified defects, +existing corrections, new helpers and downstream policy. Additions include +handle/path native CI wrappers, explicit SPKI representation/hash helpers, +strict hex conversion, an explicit-layout x64 chain view, optional publisher +resolution with matching cleanup, a referenced-process name API and dedicated +process-name cleanup. The PID path fixes dangling failure output and excessive +process access; module-base lookup clears stale output. WDM/KMDF linking is +covered by the package-relative property sheet. No default publisher/root trust +policy, automatic legacy fallback, or process authorization/cache is introduced. + +Four compatibility functions carry MSVC deprecation annotations with explicit +migration messages: heuristic digest-kind discovery, compatibility PE hashing, +implicit-policy legacy validation and PID-based process-name lookup. Native CI +types/functions remain available for low-level callers. The compile-only +deprecation suite checks actual public headers in C/C++, including modern API +controls and expected C4996 diagnostics for each older entry point. diff --git a/include/Domito.Consumer.h b/include/Domito.Consumer.h new file mode 100644 index 0000000..f2d9b79 --- /dev/null +++ b/include/Domito.Consumer.h @@ -0,0 +1,200 @@ +#pragma once +#include "Domito.h" + +// Consumer helpers: see docs/consumer-helpers.md for ownership and trust limits. +typedef struct _DOMITO_BYTE_VIEW +{ + const UCHAR* Data; + ULONG Size; +} DOMITO_BYTE_VIEW; + +typedef struct _DOMITO_CERTIFICATE_SPKI +{ + DOMITO_BYTE_VIEW Encoded; // Complete SPKI SEQUENCE, including tag and length. + DOMITO_BYTE_VIEW Content; // Legacy consumer convention: excludes that header. +} DOMITO_CERTIFICATE_SPKI; + +typedef enum _DOMITO_CERTIFICATE_HASH_KIND +{ + DomitoCertificateHashDer = 0, + DomitoCertificateHashSpkiDer = 1, + DomitoCertificateHashSpkiContentLegacy = 2 +} DOMITO_CERTIFICATE_HASH_KIND; + +// Bounded positional DER extraction, not X.509 validation. Views borrow CertData. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoGetCertificateSpki( + _In_reads_bytes_(CertSize) const UCHAR* CertData, + _In_ ULONG CertSize, + _Out_ DOMITO_CERTIFICATE_SPKI* Spki +); + +// SHA-1/256/384/512. HashSize is capacity on entry, bytes on success, required +// size on BUFFER_TOO_SMALL, zero on other failures. Hash may be NULL to size. +// Input/output must not overlap. Hash contains zero bytes on operational failure. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoHashBuffer( + _In_reads_bytes_opt_(Size) const UCHAR* Bytes, + _In_ ULONG Size, + _In_ ALG_ID Algorithm, + _Out_writes_bytes_to_opt_(*HashSize, *HashSize) UCHAR* Hash, + _Inout_ ULONG* HashSize +); + +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoHashCertificate( + _In_reads_bytes_(CertSize) const UCHAR* CertData, + _In_ ULONG CertSize, + _In_ DOMITO_CERTIFICATE_HASH_KIND Kind, + _In_ ALG_ID Algorithm, + _Out_writes_bytes_to_opt_(*HashSize, *HashSize) UCHAR* Hash, + _Inout_ ULONG* HashSize +); + +// Strict length-delimited hex: no spaces, prefixes, separators or invalid digits. +// Nonoverlapping buffers. Decode clears its output on malformed input. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoDecodeHex( + _In_ const UNICODE_STRING* Hex, + _Out_writes_bytes_(ByteCount) UCHAR* Bytes, + _In_ ULONG ByteCount +); + +// Writes uppercase hex plus NUL; HexChars includes terminator capacity. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoEncodeHex( + _In_reads_bytes_opt_(ByteCount) const UCHAR* Bytes, + _In_ ULONG ByteCount, + _Out_writes_(HexChars) WCHAR* Hex, + _In_ ULONG HexChars +); + +// Resolve optional exports without creating a static import. Requires DomitoInit. +// ModuleName is the full loaded-module path used by FindModuleBaseAddress. +// Caller must keep that module mapped; this does not acquire a loader reference. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoFindModuleExportedFunctionAddress( + _In_ PANSI_STRING ModuleName, + _In_ PANSI_STRING FunctionName, + _Outptr_result_maybenull_ PVOID* FunctionAddress +); + +// Results from either process-name API use one Domito allocation. The memory +// callback pair must remain unchanged until all allocations are released. +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +void +DomitoFreeProcessImageName( + _In_opt_ PUNICODE_STRING ImageName +); + +// Caller holds a referenced PEPROCESS throughout the call. No PID re-lookup. +// Returns a copy owned by Domito; use DomitoFreeProcessImageName once. +// Name lookup does not establish the identity of a later reopened file. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoGetProcessImageNameEx( + _In_ PEPROCESS Process, + _Outptr_result_maybenull_ PUNICODE_STRING* ImageName +); + +#if (NTDDI_VERSION >= NTDDI_WIN10) +// These wrappers do not require DomitoInit. Handle must be a trusted kernel +// handle with FILE_READ_DATA; it is borrowed, referenced for the call, not closed. +// Same initialized-result/cleanup rules and explicit flags as FileObjectEx. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoValidateFileHandleEx( + _In_ HANDLE FileHandle, + _In_ UINT32 ValidationFlags, + _In_ UINT8 RequestedSigningLevel, + _In_ DOMITO_CI_SIGNATURE_SELECTION Selection, + _Inout_ DOMITO_CI_VALIDATION_RESULT* Result +); + +// Absolute native path (e.g. \??\C:\...). Opens read-only, shares only reads, +// closes its own handle. The path can be reparsed; this validates the opened +// file, not process memory or a previous path identity. No fallback is implicit. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoValidateFilePathEx( + _In_ const UNICODE_STRING* Path, + _In_ UINT32 ValidationFlags, + _In_ UINT8 RequestedSigningLevel, + _In_ DOMITO_CI_SIGNATURE_SELECTION Selection, + _Inout_ DOMITO_CI_VALIDATION_RESULT* Result +); + +typedef enum _DOMITO_CI_CHAIN_LAYOUT +{ + // Explicit caller assertion: observed x64 +0x28 array/+0x30 count, 120-byte + // elements and a minimum 56-byte header. NOT selected by allocation Size. + DomitoCiChainLayoutObservedX64 = 1 +} DOMITO_CI_CHAIN_LAYOUT; + +typedef struct _DOMITO_CI_CHAIN_VIEW +{ + const MINCRYPT_CHAIN_ELEMENT* Elements; + ULONG Count; + ULONG RootId; // Opaque CI identifier, not proof the last element is a root. +} DOMITO_CI_CHAIN_VIEW; + +// Only accepts a readable, initialized CI-owned policy, never arbitrary pointers. +// Checks every exposed name/certificate span against CI's owned allocation. +// Leaf-first order is observed in sampled layouts. Last element is not promised +// to be a self-signed/trusted root. No issuer/subject byte equality is imposed. +// View borrows policy storage until CiFreePolicyInfo/result reuse. x64 only. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoGetCiChainView( + _In_ const MINCRYPT_POLICY_INFO* Policy, + _In_ DOMITO_CI_CHAIN_LAYOUT Layout, + _Out_ DOMITO_CI_CHAIN_VIEW* View +); + +// Optional export, resolved at call time; requires DomitoInit. No static import +// of CiGetCertPublisherName. Extracts a display name, not certificate trust. +// Start with {0}; release even after failure using the dedicated free function. +// Never pass an owning previous string: free it before making another call. +_Must_inspect_result_ +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +NTSTATUS +DomitoGetCertificatePublisherName( + _In_ const MINCERT_BLOB* Certificate, + _Out_ PUNICODE_STRING PublisherName +); + +_IRQL_requires_max_(PASSIVE_LEVEL) +EXTERN_C +void +DomitoFreeCertificatePublisherName( + _Inout_ PUNICODE_STRING PublisherName +); +#endif diff --git a/include/Domito.MinCrypt.26100.h b/include/Domito.MinCrypt.26100.h index bf7a123..ec5c7cf 100644 --- a/include/Domito.MinCrypt.26100.h +++ b/include/Domito.MinCrypt.26100.h @@ -13,18 +13,22 @@ #endif #pragma pack(push, 8) -typedef struct _DOMITO_CI26100_BLOB { + +typedef struct _DOMITO_CI26100_BLOB +{ ULONG Size; - UCHAR *Data; + 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. */ +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 { +typedef struct _DOMITO_CI26100_CHAIN_ELEMENT +{ ULONG HashAlgId; ULONG HashSize; UCHAR Hash[64]; @@ -33,72 +37,121 @@ typedef struct _DOMITO_CI26100_CHAIN_ELEMENT { 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; +typedef struct _DOMITO_CI26100_CHAIN_INFO +{ + ULONG Size; /* Entire allocation, including trailing data. */ + DOMITO_CI26100_BLOB* PublicKeys; ULONG NumberOfPublicKeys; - DOMITO_CI26100_BLOB *EKUs; + DOMITO_CI26100_BLOB* EKUs; ULONG NumberOfEKUs; - DOMITO_CI26100_CHAIN_ELEMENT *ChainElements; + DOMITO_CI26100_CHAIN_ELEMENT* ChainElements; ULONG NumberOfChainElements; - ULONG RootId; /* Opaque classification, NOT policy bits. */ + 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 { +typedef struct _DOMITO_CI26100_POLICY_INFO +{ ULONG Size; NTSTATUS VerificationStatus; /* Does not replace the API return status. */ ULONG PolicyBits; - DOMITO_CI26100_CHAIN_INFO *ChainInfo; + 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( + 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( + 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( + 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 VOID(NTAPI* DOMITO_CI26100_FREE_POLICY_INFO)(DOMITO_CI26100_POLICY_INFO* PolicyInfo); -typedef NTSTATUS (NTAPI *DOMITO_CI26100_VALIDATE_FILE_OBJECT)( +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); + 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)( +typedef NTSTATUS(NTAPI* DOMITO_CI26100_CHECK_SIGNED_FILE)( PVOID Hash, ULONG HashSize, ULONG HashAlgId, - PVOID WinCertificate, /* WIN_CERTIFICATE header, not bCertificate. */ + PVOID WinCertificate, /* WIN_CERTIFICATE header, not bCertificate. */ ULONG SizeOfSecurityDirectory, - DOMITO_CI26100_POLICY_INFO *PolicyInfo, - LARGE_INTEGER *SigningTime, - DOMITO_CI26100_POLICY_INFO *TimeStampPolicyInfo); + 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 @@ -106,49 +159,51 @@ typedef NTSTATUS (NTAPI *DOMITO_CI26100_CHECK_SIGNED_FILE)( * 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)( +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); + 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, +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. */ + 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 +#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_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; diff --git a/include/Domito.MinCrypt.Historical.h b/include/Domito.MinCrypt.Historical.h index 0d0aa42..3939172 100644 --- a/include/Domito.MinCrypt.Historical.h +++ b/include/Domito.MinCrypt.Historical.h @@ -13,6 +13,7 @@ #endif #pragma pack(push, 8) + // Observed ci.dll 10.0.10240.16463. The variable payload begins at +0x38; // AuthenticodeAttributes does not exist at that offset in this header. typedef struct _DOMITO_CI10240_CHAIN_INFO @@ -57,17 +58,63 @@ typedef struct _DOMITO_CI14393_CHAIN_INFO MINCRYPT_ATTR_BLOB AuthenticodeAttributes; UINT8 ReservedExtension[32]; } DOMITO_CI14393_CHAIN_INFO, *PDOMITO_CI14393_CHAIN_INFO; + #pragma pack(pop) C_ASSERT(sizeof(DOMITO_CI10240_CHAIN_INFO) == 56); -C_ASSERT(FIELD_OFFSET(DOMITO_CI10240_CHAIN_INFO, PublicKeys) == 8); -C_ASSERT(FIELD_OFFSET(DOMITO_CI10240_CHAIN_INFO, EKUs) == 24); -C_ASSERT(FIELD_OFFSET(DOMITO_CI10240_CHAIN_INFO, ChainElements) == 40); -C_ASSERT(FIELD_OFFSET(DOMITO_CI10240_CHAIN_INFO, RootId) == 52); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI10240_CHAIN_INFO, + PublicKeys + ) == 8 +); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI10240_CHAIN_INFO, + EKUs + ) == 24 +); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI10240_CHAIN_INFO, + ChainElements + ) == 40 +); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI10240_CHAIN_INFO, + RootId + ) == 52 +); C_ASSERT(sizeof(DOMITO_CI10586_CHAIN_INFO) == 72); -C_ASSERT(FIELD_OFFSET(DOMITO_CI10586_CHAIN_INFO, RootId) == 52); -C_ASSERT(FIELD_OFFSET(DOMITO_CI10586_CHAIN_INFO, AuthenticodeAttributes) == 56); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI10586_CHAIN_INFO, + RootId + ) == 52 +); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI10586_CHAIN_INFO, + AuthenticodeAttributes + ) == 56 +); C_ASSERT(sizeof(DOMITO_CI14393_CHAIN_INFO) == 104); -C_ASSERT(FIELD_OFFSET(DOMITO_CI14393_CHAIN_INFO, RootId) == 52); -C_ASSERT(FIELD_OFFSET(DOMITO_CI14393_CHAIN_INFO, AuthenticodeAttributes) == 56); -C_ASSERT(FIELD_OFFSET(DOMITO_CI14393_CHAIN_INFO, ReservedExtension) == 72); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI14393_CHAIN_INFO, + RootId + ) == 52 +); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI14393_CHAIN_INFO, + AuthenticodeAttributes + ) == 56 +); +C_ASSERT( + FIELD_OFFSET( + DOMITO_CI14393_CHAIN_INFO, + ReservedExtension + ) == 72 +); diff --git a/include/Domito.MinCrypt.h b/include/Domito.MinCrypt.h index 5e0c37c..48a5dbb 100644 --- a/include/Domito.MinCrypt.h +++ b/include/Domito.MinCrypt.h @@ -22,16 +22,16 @@ #pragma once - // Warnings which disabled for compiling +// Warnings which disabled for compiling #if _MSC_VER >= 1200 #pragma warning(push) // nonstandard extension used : nameless struct/union -#pragma warning(disable:4201) +#pragma warning(disable : 4201) // 'struct_name' : structure was padded due to __declspec(align()) -#pragma warning(disable:4324) +#pragma warning(disable : 4324) // 'enumeration': a forward declaration of an unscoped enumeration must have an // underlying type (int assumed) -#pragma warning(disable:4471) +#pragma warning(disable : 4471) #endif EXTERN_C_START @@ -47,144 +47,144 @@ EXTERN_C_START // // ALG_ID crackers -#define GET_ALG_CLASS(x) (x & (7 << 13)) -#define GET_ALG_TYPE(x) (x & (15 << 9)) -#define GET_ALG_SID(x) (x & (511)) +#define GET_ALG_CLASS(x) (x & (7 << 13)) +#define GET_ALG_TYPE(x) (x & (15 << 9)) +#define GET_ALG_SID(x) (x & (511)) // Algorithm classes // certenrolld_begin -- ALG_CLASS_* -#define ALG_CLASS_ANY (0) -#define ALG_CLASS_SIGNATURE (1 << 13) -#define ALG_CLASS_MSG_ENCRYPT (2 << 13) -#define ALG_CLASS_DATA_ENCRYPT (3 << 13) -#define ALG_CLASS_HASH (4 << 13) -#define ALG_CLASS_KEY_EXCHANGE (5 << 13) -#define ALG_CLASS_ALL (7 << 13) +#define ALG_CLASS_ANY (0) +#define ALG_CLASS_SIGNATURE (1 << 13) +#define ALG_CLASS_MSG_ENCRYPT (2 << 13) +#define ALG_CLASS_DATA_ENCRYPT (3 << 13) +#define ALG_CLASS_HASH (4 << 13) +#define ALG_CLASS_KEY_EXCHANGE (5 << 13) +#define ALG_CLASS_ALL (7 << 13) // certenrolld_end // Algorithm types -#define ALG_TYPE_ANY (0) -#define ALG_TYPE_DSS (1 << 9) -#define ALG_TYPE_RSA (2 << 9) -#define ALG_TYPE_BLOCK (3 << 9) -#define ALG_TYPE_STREAM (4 << 9) -#define ALG_TYPE_DH (5 << 9) -#define ALG_TYPE_SECURECHANNEL (6 << 9) +#define ALG_TYPE_ANY (0) +#define ALG_TYPE_DSS (1 << 9) +#define ALG_TYPE_RSA (2 << 9) +#define ALG_TYPE_BLOCK (3 << 9) +#define ALG_TYPE_STREAM (4 << 9) +#define ALG_TYPE_DH (5 << 9) +#define ALG_TYPE_SECURECHANNEL (6 << 9) #if (NTDDI_VERSION >= NTDDI_VISTA) -#define ALG_TYPE_ECDH (7 << 9) +#define ALG_TYPE_ECDH (7 << 9) #endif //(NTDDI_VERSION >= NTDDI_VISTA) #if (NTDDI_VERSION >= NTDDI_WIN10_RS1) -#define ALG_TYPE_THIRDPARTY (8 << 9) +#define ALG_TYPE_THIRDPARTY (8 << 9) #endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1) // Generic sub-ids -#define ALG_SID_ANY (0) +#define ALG_SID_ANY (0) // Generic ThirdParty sub-ids #if (NTDDI_VERSION >= NTDDI_WIN10_RS1) -#define ALG_SID_THIRDPARTY_ANY (0) +#define ALG_SID_THIRDPARTY_ANY (0) #endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1) // Some RSA sub-ids -#define ALG_SID_RSA_ANY 0 -#define ALG_SID_RSA_PKCS 1 -#define ALG_SID_RSA_MSATWORK 2 -#define ALG_SID_RSA_ENTRUST 3 -#define ALG_SID_RSA_PGP 4 +#define ALG_SID_RSA_ANY 0 +#define ALG_SID_RSA_PKCS 1 +#define ALG_SID_RSA_MSATWORK 2 +#define ALG_SID_RSA_ENTRUST 3 +#define ALG_SID_RSA_PGP 4 // Some DSS sub-ids // -#define ALG_SID_DSS_ANY 0 -#define ALG_SID_DSS_PKCS 1 -#define ALG_SID_DSS_DMS 2 +#define ALG_SID_DSS_ANY 0 +#define ALG_SID_DSS_PKCS 1 +#define ALG_SID_DSS_DMS 2 #if (NTDDI_VERSION >= NTDDI_VISTA) -#define ALG_SID_ECDSA 3 +#define ALG_SID_ECDSA 3 #endif //(NTDDI_VERSION >= NTDDI_VISTA) // Block cipher sub ids // DES sub_ids -#define ALG_SID_DES 1 -#define ALG_SID_3DES 3 -#define ALG_SID_DESX 4 -#define ALG_SID_IDEA 5 -#define ALG_SID_CAST 6 -#define ALG_SID_SAFERSK64 7 -#define ALG_SID_SAFERSK128 8 -#define ALG_SID_3DES_112 9 -#define ALG_SID_CYLINK_MEK 12 -#define ALG_SID_RC5 13 +#define ALG_SID_DES 1 +#define ALG_SID_3DES 3 +#define ALG_SID_DESX 4 +#define ALG_SID_IDEA 5 +#define ALG_SID_CAST 6 +#define ALG_SID_SAFERSK64 7 +#define ALG_SID_SAFERSK128 8 +#define ALG_SID_3DES_112 9 +#define ALG_SID_CYLINK_MEK 12 +#define ALG_SID_RC5 13 #if (NTDDI_VERSION >= NTDDI_WINXP) -#define ALG_SID_AES_128 14 -#define ALG_SID_AES_192 15 -#define ALG_SID_AES_256 16 -#define ALG_SID_AES 17 +#define ALG_SID_AES_128 14 +#define ALG_SID_AES_192 15 +#define ALG_SID_AES_256 16 +#define ALG_SID_AES 17 #endif //(NTDDI_VERSION >= NTDDI_WINXP) // Fortezza sub-ids -#define ALG_SID_SKIPJACK 10 -#define ALG_SID_TEK 11 +#define ALG_SID_SKIPJACK 10 +#define ALG_SID_TEK 11 // KP_MODE -#define CRYPT_MODE_CBCI 6 // ANSI CBC Interleaved -#define CRYPT_MODE_CFBP 7 // ANSI CFB Pipelined -#define CRYPT_MODE_OFBP 8 // ANSI OFB Pipelined -#define CRYPT_MODE_CBCOFM 9 // ANSI CBC + OF Masking -#define CRYPT_MODE_CBCOFMI 10 // ANSI CBC + OFM Interleaved +#define CRYPT_MODE_CBCI 6 // ANSI CBC Interleaved +#define CRYPT_MODE_CFBP 7 // ANSI CFB Pipelined +#define CRYPT_MODE_OFBP 8 // ANSI OFB Pipelined +#define CRYPT_MODE_CBCOFM 9 // ANSI CBC + OF Masking +#define CRYPT_MODE_CBCOFMI 10 // ANSI CBC + OFM Interleaved // RC2 sub-ids -#define ALG_SID_RC2 2 +#define ALG_SID_RC2 2 // Stream cipher sub-ids -#define ALG_SID_RC4 1 -#define ALG_SID_SEAL 2 +#define ALG_SID_RC4 1 +#define ALG_SID_SEAL 2 // Diffie-Hellman sub-ids -#define ALG_SID_DH_SANDF 1 -#define ALG_SID_DH_EPHEM 2 -#define ALG_SID_AGREED_KEY_ANY 3 -#define ALG_SID_KEA 4 +#define ALG_SID_DH_SANDF 1 +#define ALG_SID_DH_EPHEM 2 +#define ALG_SID_AGREED_KEY_ANY 3 +#define ALG_SID_KEA 4 #if (NTDDI_VERSION >= NTDDI_VISTA) -#define ALG_SID_ECDH 5 -#define ALG_SID_ECDH_EPHEM 6 +#define ALG_SID_ECDH 5 +#define ALG_SID_ECDH_EPHEM 6 #endif //(NTDDI_VERSION >= NTDDI_VISTA) // Hash sub ids -#define ALG_SID_MD2 1 -#define ALG_SID_MD4 2 -#define ALG_SID_MD5 3 -#define ALG_SID_SHA 4 -#define ALG_SID_SHA1 4 -#define ALG_SID_MAC 5 -#define ALG_SID_RIPEMD 6 -#define ALG_SID_RIPEMD160 7 -#define ALG_SID_SSL3SHAMD5 8 -#define ALG_SID_HMAC 9 -#define ALG_SID_TLS1PRF 10 +#define ALG_SID_MD2 1 +#define ALG_SID_MD4 2 +#define ALG_SID_MD5 3 +#define ALG_SID_SHA 4 +#define ALG_SID_SHA1 4 +#define ALG_SID_MAC 5 +#define ALG_SID_RIPEMD 6 +#define ALG_SID_RIPEMD160 7 +#define ALG_SID_SSL3SHAMD5 8 +#define ALG_SID_HMAC 9 +#define ALG_SID_TLS1PRF 10 #if (NTDDI_VERSION >= NTDDI_WINXP) -#define ALG_SID_HASH_REPLACE_OWF 11 +#define ALG_SID_HASH_REPLACE_OWF 11 #endif //(NTDDI_VERSION >= NTDDI_WINXP) #if (NTDDI_VERSION > NTDDI_WINXPSP2) -#define ALG_SID_SHA_256 12 -#define ALG_SID_SHA_384 13 -#define ALG_SID_SHA_512 14 +#define ALG_SID_SHA_256 12 +#define ALG_SID_SHA_384 13 +#define ALG_SID_SHA_512 14 #endif //(NTDDI_VERSION > NTDDI_WINXPSP2) // secure channel sub ids -#define ALG_SID_SSL3_MASTER 1 -#define ALG_SID_SCHANNEL_MASTER_HASH 2 -#define ALG_SID_SCHANNEL_MAC_KEY 3 -#define ALG_SID_PCT1_MASTER 4 -#define ALG_SID_SSL2_MASTER 5 -#define ALG_SID_TLS1_MASTER 6 -#define ALG_SID_SCHANNEL_ENC_KEY 7 +#define ALG_SID_SSL3_MASTER 1 +#define ALG_SID_SCHANNEL_MASTER_HASH 2 +#define ALG_SID_SCHANNEL_MAC_KEY 3 +#define ALG_SID_PCT1_MASTER 4 +#define ALG_SID_SSL2_MASTER 5 +#define ALG_SID_TLS1_MASTER 6 +#define ALG_SID_SCHANNEL_ENC_KEY 7 #if (NTDDI_VERSION >= NTDDI_VISTA) // misc ECC sub ids -#define ALG_SID_ECMQV 1 +#define ALG_SID_ECMQV 1 #endif //(NTDDI_VERSION >= NTDDI_VISTA) // Our silly example sub-id -#define ALG_SID_EXAMPLE 80 +#define ALG_SID_EXAMPLE 80 // certenrolls_begin -- PROV_ENUMALGS_EX #ifndef ALGIDDEF @@ -194,133 +194,127 @@ typedef unsigned int ALG_ID; // certenrolls_end // algorithm identifier definitions -#define CALG_MD2 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD2) -#define CALG_MD4 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD4) -#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5) -#define CALG_SHA (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA) -#define CALG_SHA1 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA1) -#define CALG_MAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MAC) // Deprecated. Don't use. -#define CALG_RSA_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_RSA | ALG_SID_RSA_ANY) -#define CALG_DSS_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_DSS_ANY) +#define CALG_MD2 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD2) +#define CALG_MD4 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD4) +#define CALG_MD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MD5) +#define CALG_SHA (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA) +#define CALG_SHA1 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA1) +#define CALG_MAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_MAC) // Deprecated. Don't use. +#define CALG_RSA_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_RSA | ALG_SID_RSA_ANY) +#define CALG_DSS_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_DSS_ANY) #if (NTDDI_VERSION >= NTDDI_WINXP) -#define CALG_NO_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_ANY | ALG_SID_ANY) +#define CALG_NO_SIGN (ALG_CLASS_SIGNATURE | ALG_TYPE_ANY | ALG_SID_ANY) #endif //(NTDDI_VERSION >= NTDDI_WINXP) -#define CALG_RSA_KEYX (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_RSA|ALG_SID_RSA_ANY) -#define CALG_DES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_DES) -#define CALG_3DES_112 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_3DES_112) -#define CALG_3DES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_3DES) -#define CALG_DESX (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_DESX) -#define CALG_RC2 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_RC2) -#define CALG_RC4 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_STREAM|ALG_SID_RC4) -#define CALG_SEAL (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_STREAM|ALG_SID_SEAL) -#define CALG_DH_SF (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_DH_SANDF) -#define CALG_DH_EPHEM (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_DH_EPHEM) -#define CALG_AGREEDKEY_ANY (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_AGREED_KEY_ANY) -#define CALG_KEA_KEYX (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_DH|ALG_SID_KEA) -#define CALG_HUGHES_MD5 (ALG_CLASS_KEY_EXCHANGE|ALG_TYPE_ANY|ALG_SID_MD5) -#define CALG_SKIPJACK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_SKIPJACK) -#define CALG_TEK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_TEK) -#define CALG_CYLINK_MEK (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_CYLINK_MEK) // Deprecated. Do not use -#define CALG_SSL3_SHAMD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SSL3SHAMD5) -#define CALG_SSL3_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SSL3_MASTER) -#define CALG_SCHANNEL_MASTER_HASH (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_MASTER_HASH) -#define CALG_SCHANNEL_MAC_KEY (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_MAC_KEY) -#define CALG_SCHANNEL_ENC_KEY (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SCHANNEL_ENC_KEY) -#define CALG_PCT1_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_PCT1_MASTER) -#define CALG_SSL2_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_SSL2_MASTER) -#define CALG_TLS1_MASTER (ALG_CLASS_MSG_ENCRYPT|ALG_TYPE_SECURECHANNEL|ALG_SID_TLS1_MASTER) -#define CALG_RC5 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_RC5) -#define CALG_HMAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HMAC) -#define CALG_TLS1PRF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_TLS1PRF) +#define CALG_RSA_KEYX (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_RSA | ALG_SID_RSA_ANY) +#define CALG_DES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_DES) +#define CALG_3DES_112 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_3DES_112) +#define CALG_3DES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_3DES) +#define CALG_DESX (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_DESX) +#define CALG_RC2 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_RC2) +#define CALG_RC4 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_STREAM | ALG_SID_RC4) +#define CALG_SEAL (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_STREAM | ALG_SID_SEAL) +#define CALG_DH_SF (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_DH_SANDF) +#define CALG_DH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_DH_EPHEM) +#define CALG_AGREEDKEY_ANY (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_AGREED_KEY_ANY) +#define CALG_KEA_KEYX (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_KEA) +#define CALG_HUGHES_MD5 (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_MD5) +#define CALG_SKIPJACK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_SKIPJACK) +#define CALG_TEK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_TEK) +#define CALG_CYLINK_MEK (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_CYLINK_MEK) // Deprecated. Do not use +#define CALG_SSL3_SHAMD5 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SSL3SHAMD5) +#define CALG_SSL3_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SSL3_MASTER) +#define CALG_SCHANNEL_MASTER_HASH (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_MASTER_HASH) +#define CALG_SCHANNEL_MAC_KEY (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_MAC_KEY) +#define CALG_SCHANNEL_ENC_KEY (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SCHANNEL_ENC_KEY) +#define CALG_PCT1_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_PCT1_MASTER) +#define CALG_SSL2_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_SSL2_MASTER) +#define CALG_TLS1_MASTER (ALG_CLASS_MSG_ENCRYPT | ALG_TYPE_SECURECHANNEL | ALG_SID_TLS1_MASTER) +#define CALG_RC5 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_RC5) +#define CALG_HMAC (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HMAC) +#define CALG_TLS1PRF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_TLS1PRF) #if (NTDDI_VERSION >= NTDDI_WINXP) -#define CALG_HASH_REPLACE_OWF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HASH_REPLACE_OWF) -#define CALG_AES_128 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_128) -#define CALG_AES_192 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_192) -#define CALG_AES_256 (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES_256) -#define CALG_AES (ALG_CLASS_DATA_ENCRYPT|ALG_TYPE_BLOCK|ALG_SID_AES) +#define CALG_HASH_REPLACE_OWF (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_HASH_REPLACE_OWF) +#define CALG_AES_128 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_128) +#define CALG_AES_192 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_192) +#define CALG_AES_256 (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES_256) +#define CALG_AES (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_BLOCK | ALG_SID_AES) #endif //(NTDDI_VERSION >= NTDDI_WINXP) #if (NTDDI_VERSION > NTDDI_WINXPSP2) -#define CALG_SHA_256 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_256) -#define CALG_SHA_384 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_384) -#define CALG_SHA_512 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_512) +#define CALG_SHA_256 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_256) +#define CALG_SHA_384 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_384) +#define CALG_SHA_512 (ALG_CLASS_HASH | ALG_TYPE_ANY | ALG_SID_SHA_512) #endif //(NTDDI_VERSION > NTDDI_WINXPSP2) #if (NTDDI_VERSION >= NTDDI_VISTA) -#define CALG_ECDH (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_ECDH) -#define CALG_ECDH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ECDH | ALG_SID_ECDH_EPHEM) -#define CALG_ECMQV (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_ECMQV) -#define CALG_ECDSA (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_ECDSA) -#define CALG_NULLCIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_ANY | 0) +#define CALG_ECDH (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_DH | ALG_SID_ECDH) +#define CALG_ECDH_EPHEM (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ECDH | ALG_SID_ECDH_EPHEM) +#define CALG_ECMQV (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_ANY | ALG_SID_ECMQV) +#define CALG_ECDSA (ALG_CLASS_SIGNATURE | ALG_TYPE_DSS | ALG_SID_ECDSA) +#define CALG_NULLCIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_ANY | 0) #endif //(NTDDI_VERSION >= NTDDI_VISTA) #if (NTDDI_VERSION >= NTDDI_WIN10_RS1) -#define CALG_THIRDPARTY_KEY_EXCHANGE (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) -#define CALG_THIRDPARTY_SIGNATURE (ALG_CLASS_SIGNATURE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) -#define CALG_THIRDPARTY_CIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) -#define CALG_THIRDPARTY_HASH (ALG_CLASS_HASH | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_KEY_EXCHANGE (ALG_CLASS_KEY_EXCHANGE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_SIGNATURE (ALG_CLASS_SIGNATURE | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_CIPHER (ALG_CLASS_DATA_ENCRYPT | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) +#define CALG_THIRDPARTY_HASH (ALG_CLASS_HASH | ALG_TYPE_THIRDPARTY | ALG_SID_THIRDPARTY_ANY) #endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1) #include + typedef struct _MINCRYPTOAPI_BLOB { UINT32 Size; - _Field_size_bytes_(Size) UINT8* Data; + _Field_size_bytes_(Size) UINT8* Data; -} MINCRYPT_INTEGER_BLOB, * PMINCRYPT_INTEGER_BLOB, -MINCRYPT_UINT_BLOB, * PMINCRYPT_UINT_BLOB, -MINCRYPT_OBJID_BLOB, * PMINCRYPT_OBJID_BLOB, -MINCERT_NAME_BLOB, * PMINCERT_NAME_BLOB, -MINCERT_RDN_VALUE_BLOB, * PMINCERT_RDN_VALUE_BLOB, -MINCERT_BLOB, * PMINCERT_BLOB, -MINCRL_BLOB, * PMINCRL_BLOB, -MINCRYPT_DATA_BLOB, * PMINCRYPT_DATA_BLOB, -MINCRYPT_HASH_BLOB, * PMINCRYPT_HASH_BLOB, -MINCRYPT_DIGEST_BLOB, * PMINCRYPT_DIGEST_BLOB, -MINCRYPT_DER_BLOB, * PMINCRYPT_DER_BLOB, -MINCRYPT_ATTR_BLOB, * PMINCRYPT_ATTR_BLOB; +} MINCRYPT_INTEGER_BLOB, *PMINCRYPT_INTEGER_BLOB, MINCRYPT_UINT_BLOB, *PMINCRYPT_UINT_BLOB, MINCRYPT_OBJID_BLOB, + *PMINCRYPT_OBJID_BLOB, MINCERT_NAME_BLOB, *PMINCERT_NAME_BLOB, MINCERT_RDN_VALUE_BLOB, *PMINCERT_RDN_VALUE_BLOB, + MINCERT_BLOB, *PMINCERT_BLOB, MINCRL_BLOB, *PMINCRL_BLOB, MINCRYPT_DATA_BLOB, *PMINCRYPT_DATA_BLOB, + MINCRYPT_HASH_BLOB, *PMINCRYPT_HASH_BLOB, MINCRYPT_DIGEST_BLOB, *PMINCRYPT_DIGEST_BLOB, MINCRYPT_DER_BLOB, + *PMINCRYPT_DER_BLOB, MINCRYPT_ATTR_BLOB, *PMINCRYPT_ATTR_BLOB; // Encoded, length-delimited name bytes; not necessarily ANSI or NUL-terminated. // The observed 26100 x64 string tag occupies padding; see the opt-in version header. typedef struct _MINCRYPT_NAME_BLOB { - _Field_size_bytes_(Size) PCHAR Data; + _Field_size_bytes_(Size) PCHAR Data; USHORT Size; -}MINCRYPT_NAME_BLOB, * PMINCRYPT_NAME_BLOB; +} MINCRYPT_NAME_BLOB, *PMINCRYPT_NAME_BLOB; -#define MINCRYPT_MAX_HASH_LENGTH (64) -#define MINCRYPT_SHA1_LENGTH (160/8) -#define MINCRYPT_SHA256_LENGTH (256/8) -#define MINCRYPT_SHA384_LENGTH (384/8) -#define MINCRYPT_SHA512_LENGTH (512/8) +#define MINCRYPT_MAX_HASH_LENGTH (64) +#define MINCRYPT_SHA1_LENGTH (160 / 8) +#define MINCRYPT_SHA256_LENGTH (256 / 8) +#define MINCRYPT_SHA384_LENGTH (384 / 8) +#define MINCRYPT_SHA512_LENGTH (512 / 8) typedef struct _MINCRYPT_CHAIN_ELEMENT { - ALG_ID HashAlgId; // CALG_SHA1, CALG_SHA_256 - UINT32 HashSize; // SHA1(160bit/8), SHA256(256bit/8) - UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH]; + ALG_ID HashAlgId; // CALG_SHA1, CALG_SHA_256 + UINT32 HashSize; // SHA1(160bit/8), SHA256(256bit/8) + UINT8 Hash[MINCRYPT_MAX_HASH_LENGTH]; MINCRYPT_NAME_BLOB Subject; MINCRYPT_NAME_BLOB Issuer; - MINCERT_BLOB Certificate; + MINCERT_BLOB Certificate; -}MINCRYPT_CHAIN_ELEMENT, * PMINCRYPT_CHAIN_ELEMENT; +} MINCRYPT_CHAIN_ELEMENT, *PMINCRYPT_CHAIN_ELEMENT; // CI owns one chain allocation; nested pointers borrow that allocation. // Do not free child blobs separately or use memcpy to create a second owner. typedef struct _MINCRYPT_CHAIN_INFO { - UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data. + UINT32 Size; // Observed 26100 x64: total allocation bytes, including trailing data. - MINCERT_BLOB* PublicKeys; - UINT32 NumberOfPublicKeys; + MINCERT_BLOB* PublicKeys; + UINT32 NumberOfPublicKeys; - MINCERT_BLOB* EKUs; - UINT32 NumberOfEKUs; + MINCERT_BLOB* EKUs; + UINT32 NumberOfEKUs; #if (NTDDI_VERSION >= NTDDI_WIN10) PMINCRYPT_CHAIN_ELEMENT ChainElements; - UINT32 NumberOfChainElement; + UINT32 NumberOfChainElement; // REQUIRES a confirmed runtime layout (10586 and later sampled profiles). // Sampled 10240 has only 56 header bytes: +0x38 begins variable payload. @@ -346,30 +340,29 @@ typedef struct _MINCRYPT_CHAIN_INFO // ChainElements[NumberOfChainElement] -}MINCRYPT_CHAIN_INFO, * PMINCRYPT_CHAIN_INFO; +} MINCRYPT_CHAIN_INFO, *PMINCRYPT_CHAIN_INFO; typedef struct _MINCRYPT_POLICY_INFO { - UINT32 Size; - NTSTATUS VerificationStatus; // Check API return status separately. - UINT32 PolicyBits; + UINT32 Size; + NTSTATUS VerificationStatus; // Check API return status separately. + UINT32 PolicyBits; MINCRYPT_CHAIN_INFO* ChainInfo; - LARGE_INTEGER RevocationTime; // When was the certificate revoked (if applicable) + LARGE_INTEGER RevocationTime; // When was the certificate revoked (if applicable) #if (NTDDI_VERSION >= NTDDI_WIN10) - LARGE_INTEGER NotBefore; // Observed: signer certificate validity start. - LARGE_INTEGER NotAfter; // Observed: signer certificate validity end. + LARGE_INTEGER NotBefore; // Observed: signer certificate validity start. + LARGE_INTEGER NotAfter; // Observed: signer certificate validity end. #endif // NTDDI_VERSION >= NTDDI_WIN10 -}MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO; +} MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO; + #include - - /** * Release a known initialized policy's owned ChainInfo allocation and reset it. * Zero-initialize policy objects before first use. Release both signer and TSA @@ -384,13 +377,11 @@ typedef struct _MINCRYPT_POLICY_INFO */ _IRQL_requires_max_(PASSIVE_LEVEL) MINCRYPTAPI -PVOID -NTAPI +PVOID NTAPI CiFreePolicyInfo( _Inout_ MINCRYPT_POLICY_INFO* PolicyInfo ); - /** * Digest-based workflow, also implemented on observed Windows 11 build 26100. * Prefer the file-object workflow when its policy fits. Older-build ABI and @@ -429,20 +420,18 @@ CiFreePolicyInfo( */ _IRQL_requires_max_(PASSIVE_LEVEL) MINCRYPTAPI -NTSTATUS -NTAPI +NTSTATUS NTAPI CiCheckSignedFile( _In_reads_bytes_(HashSize) PVOID Hash, - _In_ UINT32 HashSize, - _In_ ALG_ID HashAlgId, + _In_ UINT32 HashSize, + _In_ ALG_ID HashAlgId, _In_reads_bytes_(SizeOfSecurityDirectory) PVOID SecurityDirectory, - _In_ UINT32 SizeOfSecurityDirectory, + _In_ UINT32 SizeOfSecurityDirectory, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo, - _Out_ LARGE_INTEGER* SigningTime, + _Out_ LARGE_INTEGER* SigningTime, _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo ); - // Existing Domito compatibility filter, not a universal trusted-root policy. // Includes test/DMD categories and a diagnostic bit; select policy deliberately. #define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL @@ -493,33 +482,29 @@ CiCheckSignedFile( */ _IRQL_requires_max_(PASSIVE_LEVEL) MINCRYPTAPI -NTSTATUS -NTAPI +NTSTATUS NTAPI CiVerifyHashInCatalog( _In_reads_bytes_(HashSize) PVOID Hash, - _In_ UINT32 HashSize, - _In_ ALG_ID HashAlgId, - _In_ BOOLEAN ReloadCatalogs, - _In_ UINT32 RequireTrustedCatalog, - _In_ UINT32 AcceptedCatalogPolicyBits, + _In_ UINT32 HashSize, + _In_ ALG_ID HashAlgId, + _In_ BOOLEAN ReloadCatalogs, + _In_ UINT32 RequireTrustedCatalog, + _In_ UINT32 AcceptedCatalogPolicyBits, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo, - _Out_opt_ UNICODE_STRING* CatalogName, - _Out_ LARGE_INTEGER* SigningTime, + _Out_opt_ UNICODE_STRING* CatalogName, + _Out_ LARGE_INTEGER* SigningTime, _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo ); - #if (NTDDI_VERSION >= NTDDI_WIN10) -typedef -_IRQL_requires_same_ -_Function_class_(MINCRYPT_ALLOCATE_ROUTINE) -__drv_allocatesMem(Mem) +typedef _IRQL_requires_same_ _Function_class_(MINCRYPT_ALLOCATE_ROUTINE) +__drv_allocatesMem( + Mem +) PVOID NTAPI -MINCRYPT_ALLOCATE_ROUTINE( - _In_ SIZE_T ByteSize -); +MINCRYPT_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize); typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE; /** @@ -548,12 +533,10 @@ CiGetCertPublisherName( _Out_ PUNICODE_STRING PublisherName ); - // Export observed already in 15063.0 x64 (1703); absent from sampled // 10240/10586/14393 files. This writes CI-global state, not per-file policy. MINCRYPTAPI -VOID -NTAPI +VOID NTAPI CiSetTrustedOriginClaimId( _In_ UINT32 ClaimId ); @@ -611,18 +594,17 @@ CiSetTrustedOriginClaimId( */ _IRQL_requires_max_(PASSIVE_LEVEL) MINCRYPTAPI -NTSTATUS -NTAPI +NTSTATUS NTAPI CiValidateFileObject( - _In_ FILE_OBJECT* FileObject, - _In_ UINT32 ValidationFlags, - _In_ UINT32 RequestedSigningLevel, + _In_ FILE_OBJECT* FileObject, + _In_ UINT32 ValidationFlags, + _In_ UINT32 RequestedSigningLevel, _Out_ MINCRYPT_POLICY_INFO* PolicyInfo, _Out_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo, - _Out_ LARGE_INTEGER* SigningTime, + _Out_ LARGE_INTEGER* SigningTime, _Out_writes_bytes_(*HashSize) UINT8* Hash, - _Inout_ UINT32* HashSize, - _Out_ ALG_ID* HashAlgId + _Inout_ UINT32* HashSize, + _Out_ ALG_ID* HashAlgId ); #endif // NTDDI_VERSION >= NTDDI_WIN10 diff --git a/include/Domito.h b/include/Domito.h index c64dc92..d3e0737 100644 --- a/include/Domito.h +++ b/include/Domito.h @@ -2,7 +2,6 @@ #include - /* ___ * / __|___ _ __ _ __ ___ _ _ * | (__/ _ \ ' \| ' \/ _ \ ' \ @@ -12,7 +11,7 @@ // // Library initialization tasks. Call once in your DriverEntry -// +// _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) @@ -25,7 +24,6 @@ EXTERN_C void DomitoShutdown(); - /* __ __ __ __ _ * | \/ |___ _ __ ___ _ _ _ _ | \/ |__ _ _ _ __ _ __ _ ___ _ __ ___ _ _| |_ * | |\/| / -_) ' \/ _ \ '_| || | | |\/| / _` | ' \/ _` / _` / -_) ' \/ -_) ' \ _| @@ -35,34 +33,26 @@ DomitoShutdown(); // // Allocator function the library uses. -// -typedef -_IRQL_requires_same_ -_Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE) -__drv_allocatesMem(mem) +// +typedef _IRQL_requires_same_ _Function_class_(EVT_DOMITO_ALLOCATE_ROUTINE) +__drv_allocatesMem( + mem +) PVOID NTAPI -EVT_DOMITO_ALLOCATE_ROUTINE( - _In_ SIZE_T ByteSize -); +EVT_DOMITO_ALLOCATE_ROUTINE(_In_ SIZE_T ByteSize); typedef EVT_DOMITO_ALLOCATE_ROUTINE* PFN_DOMITO_ALLOCATE_ROUTINE; // // Freeing function the library uses. -// -typedef -_IRQL_requires_same_ -_Function_class_(EVT_DOMITO_FREE_ROUTINE) -void -NTAPI -EVT_DOMITO_FREE_ROUTINE( - _In_ __drv_freesMem(mem) PVOID Memory -); +// +typedef _IRQL_requires_same_ _Function_class_(EVT_DOMITO_FREE_ROUTINE) void NTAPI +EVT_DOMITO_FREE_ROUTINE(_In_ __drv_freesMem(mem) PVOID Memory); typedef EVT_DOMITO_FREE_ROUTINE* PFN_DOMITO_FREE_ROUTINE; // // Get the original set of Domito memory functions. -// +// EXTERN_C void DomitoGetOriginalMemoryFunctions( @@ -72,7 +62,7 @@ DomitoGetOriginalMemoryFunctions( // // Get the current set of Domito memory functions. -// +// EXTERN_C void DomitoGetMemoryFunctions( @@ -81,7 +71,9 @@ DomitoGetMemoryFunctions( ); // -// Replace Domito's memory allocation functions with a custom set +// Replace Domito's memory allocation functions with a compatible pair during +// quiescent initialization. No in-flight calls or outstanding callback-backed +// allocations may span a pair change. Replacement is not synchronized. EXTERN_C void DomitoSetMemoryFunctions( @@ -89,7 +81,6 @@ DomitoSetMemoryFunctions( _In_opt_ PFN_DOMITO_FREE_ROUTINE Free ); - /* ___ _ _ * / __|_ _ _ _ _ __| |_ ___ __ _ _ _ __ _ _ __| |_ _ _ * | (__| '_| || | '_ \ _/ _ \/ _` | '_/ _` | '_ \ ' \ || | @@ -97,9 +88,9 @@ DomitoSetMemoryFunctions( * |__/|_| |___/ |_| |__/ */ - // - // This structure encapsulates a signature used in verifying executable files. - // +// +// This structure encapsulates a signature used in verifying executable files. +// #if !defined(WIN_CERTIFICATE) typedef struct _WIN_CERTIFICATE { @@ -112,16 +103,16 @@ typedef struct _WIN_CERTIFICATE // // UM definitions of WinCrypt.h -// +// #if !defined(WIN_CERT_TYPE_X509) -#define WIN_CERT_TYPE_X509 (0x0001) // The bCertificate member contains an X.509 certificate. +#define WIN_CERT_TYPE_X509 (0x0001) // The bCertificate member contains an X.509 certificate. #endif #if !defined(WIN_CERT_TYPE_PKCS_SIGNED_DATA) -#define WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002) // The bCertificate member contains a PKCS SignedData structure. +#define WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002) // The bCertificate member contains a PKCS SignedData structure. #endif #if !defined(WIN_CERT_TYPE_PKCS1_SIGN) -#define WIN_CERT_TYPE_PKCS1_SIGN (0x0009) // The bCertificate member contains PKCS1_MODULE_SIGN fields. +#define WIN_CERT_TYPE_PKCS1_SIGN (0x0009) // The bCertificate member contains PKCS1_MODULE_SIGN fields. #endif #if !defined(CALG_SHA1) @@ -141,6 +132,7 @@ typedef struct _WIN_CERTIFICATE // Converts a WinCrypt CALG_ID to a BCRYPT_ALGORITHM identifier. // PCWSTR + FORCEINLINE DOMITO_CALG_TO_BCRYPT_ALGORITHM( _In_ UINT32 Calg @@ -161,7 +153,6 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM( } } - /* ___ _ ___ _ _ _ * / __|___ __| |___ |_ _|_ _| |_ ___ __ _ _ _(_) |_ _ _ * | (__/ _ \/ _` / -_) | || ' \ _/ -_) _` | '_| | _| || | @@ -177,7 +168,9 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM( // _IRQL_requires_max_(DISPATCH_LEVEL) EXTERN_C -UINT32 +__declspec(deprecated( + "Use DomitoGetPortableExecutableDigestKindEx; heuristic detection silently falls back to SHA-1. Check the returned NTSTATUS." +)) UINT32 DomitoGetPortableExecutableDigestKind( _In_ PUCHAR pPeBytes, _In_ ULONG PeSize, @@ -228,12 +221,14 @@ DomitoCalculatePortableExecutableDigestEx( // The returned certificate pointer borrows the input buffer; free only the digest // with DomitoFreePortableExecutableDigest. Algorithm detection is heuristic; // general section-ordered Authenticode hashing is not implemented here. -// +// _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) EXTERN_C -NTSTATUS +__declspec(deprecated( + "Use DomitoCalculatePortableExecutableDigestEx with an explicit algorithm; review its supported PE layout and failure statuses." +)) NTSTATUS DomitoCalculatePortableExecutableDigest( _In_ PUCHAR pPeBytes, _In_ ULONG PeSize, @@ -246,7 +241,7 @@ DomitoCalculatePortableExecutableDigest( // // Frees the memory allocated by DomitoCalculatePortableExecutableDigest. -// +// _IRQL_requires_max_(DISPATCH_LEVEL) EXTERN_C void @@ -278,7 +273,7 @@ typedef struct _DOMITO_CI_VALIDATION_RESULT UINT32 HashSize; ALG_ID HashAlgId; UINT32 FileObjectValidationFlags; // Flags passed to CI export; zero for embedded helper. - UINT8 RequestedSigningLevel; // File-object helper only. + UINT8 RequestedSigningLevel; // File-object helper only. } DOMITO_CI_VALIDATION_RESULT, *PDOMITO_CI_VALIDATION_RESULT; // Safe for NULL or an empty/initialized result; resets all fields after cleanup. @@ -328,7 +323,7 @@ DomitoValidatePortableExecutableEmbeddedSignature( #endif // NTDDI_VERSION >= NTDDI_WIN10 #define DOMITO_CI_SOURCE_EMBEDDED 1UL -#define DOMITO_CI_SOURCE_CATALOG 2UL +#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 @@ -361,12 +356,14 @@ DomitoValidateFileLegacyModeEx( // Compatibility wrapper around DomitoValidateFileLegacyModeEx, with catalog // mask DOMITO_CI_LEGACY_CATALOG_POLICY_BITS and RequireTrustedCatalog = FALSE. // The same initialization, ownership, hash, handle, and stable-file rules apply. -// +// _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) EXTERN_C -NTSTATUS +__declspec(deprecated( + "Use DomitoValidateFileLegacyModeEx with explicit catalog policy and validation-source output; do not change trust policy implicitly." +)) NTSTATUS DomitoValidateFileLegacyMode( _In_ HANDLE FileHandle, _In_ PVOID Hash, @@ -378,7 +375,6 @@ DomitoValidateFileLegacyMode( _Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo ); - /* __ __ _ * | \/ (_)___ __ * | |\/| | (_-Size < 2 || Input->Data[0] != Tag) + { + return FALSE; + } + + ULONG length = Input->Data[1]; + ULONG header = 2; + + if (length & 0x80) + { + const ULONG count = length & 0x7f; + + if (!count || count > 4 || count > Input->Size - 2 || !Input->Data[2]) + { + return FALSE; + } + + length = 0; + + for (ULONG i = 0; i < count; ++i) + { + length = (length << 8) | Input->Data[header++]; + } + + if (length < 128) + { + return FALSE; + } + } + + if (length > Input->Size - header) + { + return FALSE; + } + + *Content = {Input->Data + header, length}; + + if (Encoded) + { + *Encoded = {Input->Data, header + length}; + } + + Input->Data += header + length; + Input->Size -= header + length; + return TRUE; +} + +_Use_decl_annotations_ +NTSTATUS +DomitoGetCertificateSpki( + const UCHAR* CertData, + ULONG CertSize, + DOMITO_CERTIFICATE_SPKI* Spki +) +{ + PAGED_CODE(); + + if (!Spki) + { + return STATUS_INVALID_PARAMETER; + } + + *Spki = {}; + + if (!CertData || !CertSize) + { + return STATUS_INVALID_PARAMETER; + } + + DOMITO_CERTIFICATE_DER_VIEW certificateInput = {CertData, CertSize}; + DOMITO_CERTIFICATE_DER_VIEW certificate; + DOMITO_CERTIFICATE_DER_VIEW tbsCertificate; + DOMITO_CERTIFICATE_DER_VIEW fieldContent; + DOMITO_CERTIFICATE_DER_VIEW encodedSpki; + DOMITO_CERTIFICATE_DER_VIEW spkiContent; + // Validate complete certificate framing, without interpreting trust/algorithms. + if (!DomitoCertificateDerTake(&certificateInput, 0x30, &certificate) || certificateInput.Size || + !DomitoCertificateDerTake(&certificate, 0x30, &tbsCertificate) || + !DomitoCertificateDerTake(&certificate, 0x30, &fieldContent) || + !DomitoCertificateDerTake(&certificate, 0x03, &fieldContent) || !fieldContent.Size || + fieldContent.Data[0] > 7 || certificate.Size) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + if (tbsCertificate.Size && tbsCertificate.Data[0] == 0xa0) + { + DOMITO_CERTIFICATE_DER_VIEW certificateVersion; + + if (!DomitoCertificateDerTake(&tbsCertificate, 0xa0, &fieldContent) || + !DomitoCertificateDerTake(&fieldContent, 0x02, &certificateVersion) || fieldContent.Size || + certificateVersion.Size != 1 || certificateVersion.Data[0] > 2) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + } + + const UCHAR tbsFieldTags[] = {0x02, 0x30, 0x30, 0x30, 0x30}; + + for (ULONG i = 0; i < RTL_NUMBER_OF(tbsFieldTags); ++i) + { + if (!DomitoCertificateDerTake(&tbsCertificate, tbsFieldTags[i], &fieldContent) || + (i == 0 && !fieldContent.Size)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + } + + if (!DomitoCertificateDerTake(&tbsCertificate, 0x30, &spkiContent, &encodedSpki)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + DOMITO_CERTIFICATE_DER_VIEW spkiFields = spkiContent; + DOMITO_CERTIFICATE_DER_VIEW keyAlgorithm; + DOMITO_CERTIFICATE_DER_VIEW algorithmOid; + DOMITO_CERTIFICATE_DER_VIEW publicKeyBits; + + if (!DomitoCertificateDerTake(&spkiFields, 0x30, &keyAlgorithm) || + !DomitoCertificateDerTake(&keyAlgorithm, 0x06, &algorithmOid) || !algorithmOid.Size || + !DomitoCertificateDerTake(&spkiFields, 0x03, &publicKeyBits) || publicKeyBits.Size < 2 || + publicKeyBits.Data[0] > 7 || spkiFields.Size) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + + Spki->Encoded = {encodedSpki.Data, encodedSpki.Size}; + Spki->Content = {spkiContent.Data, spkiContent.Size}; + return STATUS_SUCCESS; +} + +_Use_decl_annotations_ +NTSTATUS +DomitoHashBuffer( + const UCHAR* Bytes, + ULONG Size, + ALG_ID Algorithm, + UCHAR* Hash, + ULONG* HashSize +) +{ + PAGED_CODE(); + + if (!HashSize) + { + return STATUS_INVALID_PARAMETER; + } + + const ULONG capacity = *HashSize; + ULONG required; + + switch (Algorithm) + { + case CALG_SHA1: + required = 20; + break; + case CALG_SHA_256: + required = 32; + break; + case CALG_SHA_384: + required = 48; + break; + case CALG_SHA_512: + required = 64; + break; + default: + *HashSize = 0; + return STATUS_NOT_SUPPORTED; + } + + if (!Bytes && Size) + { + *HashSize = 0; + return STATUS_INVALID_PARAMETER; + } + + if (!Hash || capacity < required) + { + *HashSize = required; + return STATUS_BUFFER_TOO_SMALL; + } + + RtlZeroMemory(Hash, required); + BCRYPT_ALG_HANDLE algorithm = nullptr; + BCRYPT_HASH_HANDLE hash = nullptr; + NTSTATUS status = + BCryptOpenAlgorithmProvider(&algorithm, DOMITO_CALG_TO_BCRYPT_ALGORITHM(Algorithm), MS_PRIMITIVE_PROVIDER, 0); + + if (NT_SUCCESS(status)) + { + status = BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0); + } + + if (NT_SUCCESS(status) && Size) + { + status = BCryptHashData(hash, const_cast(Bytes), Size, 0); + } + + if (NT_SUCCESS(status)) + { + status = BCryptFinishHash(hash, Hash, required, 0); + } + + if (hash) + { + BCryptDestroyHash(hash); + } + + if (algorithm) + { + BCryptCloseAlgorithmProvider(algorithm, 0); + } + + if (NT_SUCCESS(status)) + { + *HashSize = required; + } + else + { + RtlZeroMemory(Hash, required); + *HashSize = 0; + } + + return status; +} + +_Use_decl_annotations_ +NTSTATUS +DomitoHashCertificate( + const UCHAR* CertData, + ULONG CertSize, + DOMITO_CERTIFICATE_HASH_KIND Kind, + ALG_ID Algorithm, + UCHAR* Hash, + ULONG* HashSize +) +{ + PAGED_CODE(); + + if (!HashSize) + { + return STATUS_INVALID_PARAMETER; + } + + if (!CertData || !CertSize) + { + *HashSize = 0; + return STATUS_INVALID_PARAMETER; + } + + DOMITO_CERTIFICATE_SPKI spki = {}; + + if (Kind == DomitoCertificateHashSpkiDer || Kind == DomitoCertificateHashSpkiContentLegacy) + { + NTSTATUS status = DomitoGetCertificateSpki(CertData, CertSize, &spki); + + if (!NT_SUCCESS(status)) + { + *HashSize = 0; + return status; + } + + const DOMITO_BYTE_VIEW selected = Kind == DomitoCertificateHashSpkiDer ? spki.Encoded : spki.Content; + return DomitoHashBuffer(selected.Data, selected.Size, Algorithm, Hash, HashSize); + } + + if (Kind != DomitoCertificateHashDer) + { + *HashSize = 0; + return STATUS_INVALID_PARAMETER; + } + + // Whole-certificate fingerprint: hashes exact input bytes, no trust claim. + return DomitoHashBuffer(CertData, CertSize, Algorithm, Hash, HashSize); +} + +static int +DomitoHexDigit( + WCHAR Value +) +{ + if (Value >= L'0' && Value <= L'9') + { + return Value - L'0'; + } + + if (Value >= L'A' && Value <= L'F') + { + return Value - L'A' + 10; + } + + if (Value >= L'a' && Value <= L'f') + { + return Value - L'a' + 10; + } + + return -1; +} + +_Use_decl_annotations_ +NTSTATUS +DomitoDecodeHex( + const UNICODE_STRING* Hex, + UCHAR* Bytes, + ULONG ByteCount +) +{ + PAGED_CODE(); + + if (!Bytes && ByteCount) + { + return STATUS_INVALID_PARAMETER; + } + + if (Bytes && ByteCount) + { + RtlZeroMemory(Bytes, ByteCount); + } + + if (!Hex || Hex->Length > Hex->MaximumLength || (Hex->Length && !Hex->Buffer) || + ByteCount > MAXUSHORT / (2 * sizeof(WCHAR)) || Hex->Length != ByteCount * 2 * sizeof(WCHAR)) + { + return STATUS_INVALID_PARAMETER; + } + + for (ULONG i = 0; i < ByteCount * 2; ++i) + { + if (DomitoHexDigit(Hex->Buffer[i]) < 0) + { + return STATUS_INVALID_PARAMETER; + } + } + + for (ULONG i = 0; i < ByteCount; ++i) + { + Bytes[i] = + static_cast((DomitoHexDigit(Hex->Buffer[2 * i]) << 4) | DomitoHexDigit(Hex->Buffer[2 * i + 1])); + } + + return STATUS_SUCCESS; +} + +_Use_decl_annotations_ +NTSTATUS +DomitoEncodeHex( + const UCHAR* Bytes, + ULONG ByteCount, + WCHAR* Hex, + ULONG HexChars +) +{ + PAGED_CODE(); + + if (Hex && HexChars) + { + Hex[0] = 0; + } + + if ((!Bytes && ByteCount) || !Hex) + { + return STATUS_INVALID_PARAMETER; + } + + if (!HexChars || ByteCount > (HexChars - 1) / 2) + { + return STATUS_BUFFER_TOO_SMALL; + } + + static const WCHAR digits[] = L"0123456789ABCDEF"; + WCHAR* destination = Hex; + + for (ULONG i = 0; i < ByteCount; ++i) + { + *destination++ = digits[Bytes[i] >> 4]; + *destination++ = digits[Bytes[i] & 15]; + } + + *destination = 0; + return STATUS_SUCCESS; +} diff --git a/src/Domito.CodeIntegrity.Validation.cpp b/src/Domito.CodeIntegrity.Validation.cpp index 2599e06..aeee18c 100644 --- a/src/Domito.CodeIntegrity.Validation.cpp +++ b/src/Domito.CodeIntegrity.Validation.cpp @@ -2,19 +2,30 @@ #if (NTDDI_VERSION >= NTDDI_WIN10) -static UINT32 DomitoCiDigestSize(ALG_ID Algorithm) +static UINT32 +DomitoCiDigestSize( + ALG_ID Algorithm +) { switch (Algorithm) { - case CALG_SHA1: return MINCRYPT_SHA1_LENGTH; - case CALG_SHA_256: return MINCRYPT_SHA256_LENGTH; - case CALG_SHA_384: return MINCRYPT_SHA384_LENGTH; - case CALG_SHA_512: return MINCRYPT_SHA512_LENGTH; - default: return 0; + case CALG_SHA1: + return MINCRYPT_SHA1_LENGTH; + case CALG_SHA_256: + return MINCRYPT_SHA256_LENGTH; + case CALG_SHA_384: + return MINCRYPT_SHA384_LENGTH; + case CALG_SHA_512: + return MINCRYPT_SHA512_LENGTH; + default: + return 0; } } -static void DomitoClearCiDigest(DOMITO_CI_VALIDATION_RESULT* Result) +static void +DomitoClearCiDigest( + DOMITO_CI_VALIDATION_RESULT* Result +) { RtlZeroMemory(Result->Hash, sizeof(Result->Hash)); Result->HashSize = 0; @@ -23,27 +34,53 @@ static void DomitoClearCiDigest(DOMITO_CI_VALIDATION_RESULT* Result) } _Use_decl_annotations_ -void DomitoFreeCiValidationResult(DOMITO_CI_VALIDATION_RESULT* Result) +void +DomitoFreeCiValidationResult( + DOMITO_CI_VALIDATION_RESULT* Result +) { PAGED_CODE(); - if (!Result) return; + + if (!Result) + { + return; + } + CiFreePolicyInfo(&Result->PolicyInfo); CiFreePolicyInfo(&Result->TimeStampPolicyInfo); RtlZeroMemory(Result, sizeof(*Result)); } _Use_decl_annotations_ -NTSTATUS DomitoValidateFileObjectEx( - PFILE_OBJECT FileObject, UINT32 ValidationFlags, UINT8 RequestedSigningLevel, - DOMITO_CI_SIGNATURE_SELECTION Selection, DOMITO_CI_VALIDATION_RESULT* Result) +NTSTATUS +DomitoValidateFileObjectEx( + PFILE_OBJECT FileObject, + UINT32 ValidationFlags, + UINT8 RequestedSigningLevel, + DOMITO_CI_SIGNATURE_SELECTION Selection, + DOMITO_CI_VALIDATION_RESULT* Result +) { PAGED_CODE(); - if (!Result) return STATUS_INVALID_PARAMETER; + + if (!Result) + { + return STATUS_INVALID_PARAMETER; + } + DomitoFreeCiValidationResult(Result); - if (!FileObject) return STATUS_INVALID_PARAMETER; + + if (!FileObject) + { + return STATUS_INVALID_PARAMETER; + } + if (Selection != DomitoCiSignatureSelectionNative && Selection != DomitoCiSignatureSelectionFirstSuccessfulExperimental) + { return STATUS_INVALID_PARAMETER; + } + if (Selection == DomitoCiSignatureSelectionFirstSuccessfulExperimental) { #if defined(_M_AMD64) || defined(_AMD64_) @@ -52,48 +89,109 @@ NTSTATUS DomitoValidateFileObjectEx( return STATUS_NOT_SUPPORTED; #endif } + Result->FileObjectValidationFlags = ValidationFlags; Result->RequestedSigningLevel = RequestedSigningLevel; Result->HashSize = sizeof(Result->Hash); - NTSTATUS status = CiValidateFileObject(FileObject, ValidationFlags, RequestedSigningLevel, - &Result->PolicyInfo, &Result->TimeStampPolicyInfo, &Result->SigningTime, - Result->Hash, &Result->HashSize, &Result->HashAlgId); + NTSTATUS status = CiValidateFileObject( + FileObject, + ValidationFlags, + RequestedSigningLevel, + &Result->PolicyInfo, + &Result->TimeStampPolicyInfo, + &Result->SigningTime, + Result->Hash, + &Result->HashSize, + &Result->HashAlgId + ); + if (NT_SUCCESS(status)) { const UINT32 expected = DomitoCiDigestSize(Result->HashAlgId); - if (!expected) status = STATUS_NOT_SUPPORTED; + + if (!expected) + { + status = STATUS_NOT_SUPPORTED; + } else if (Result->HashSize != expected || Result->HashSize > sizeof(Result->Hash)) + { status = STATUS_INVALID_BUFFER_SIZE; + } } - if (!NT_SUCCESS(status)) DomitoClearCiDigest(Result); - else RtlZeroMemory(Result->Hash + Result->HashSize, sizeof(Result->Hash) - Result->HashSize); + + if (!NT_SUCCESS(status)) + { + DomitoClearCiDigest(Result); + } + else + { + RtlZeroMemory(Result->Hash + Result->HashSize, sizeof(Result->Hash) - Result->HashSize); + } + return status; } _Use_decl_annotations_ -NTSTATUS DomitoValidatePortableExecutableEmbeddedSignature( - const UCHAR* PeBytes, ULONG PeSize, ALG_ID HashAlgId, DOMITO_CI_VALIDATION_RESULT* Result) +NTSTATUS +DomitoValidatePortableExecutableEmbeddedSignature( + const UCHAR* PeBytes, + ULONG PeSize, + ALG_ID HashAlgId, + DOMITO_CI_VALIDATION_RESULT* Result +) { PAGED_CODE(); - if (!Result) return STATUS_INVALID_PARAMETER; + + if (!Result) + { + return STATUS_INVALID_PARAMETER; + } + DomitoFreeCiValidationResult(Result); - if (!PeBytes) return STATUS_INVALID_PARAMETER; + + if (!PeBytes) + { + return STATUS_INVALID_PARAMETER; + } + const UINT32 expected = DomitoCiDigestSize(HashAlgId); - if (!expected) return STATUS_NOT_SUPPORTED; + + if (!expected) + { + return STATUS_NOT_SUPPORTED; + } + PVOID digest = nullptr; - ULONG digestSize = 0, securitySize = 0; + ULONG digestSize = 0; + ULONG securitySize = 0; LPWIN_CERTIFICATE certificate = nullptr; - NTSTATUS status = DomitoCalculatePortableExecutableDigestEx(PeBytes, PeSize, HashAlgId, - &digestSize, &digest, &certificate, &securitySize); + NTSTATUS status = DomitoCalculatePortableExecutableDigestEx( + PeBytes, PeSize, HashAlgId, &digestSize, &digest, &certificate, &securitySize + ); + if (NT_SUCCESS(status)) { - if (!certificate || !securitySize) status = STATUS_INVALID_IMAGE_HASH; + if (!certificate || !securitySize) + { + status = STATUS_INVALID_IMAGE_HASH; + } else if (!digest || digestSize != expected || digestSize > sizeof(Result->Hash)) + { status = STATUS_INVALID_BUFFER_SIZE; + } else { - status = CiCheckSignedFile(digest, digestSize, HashAlgId, certificate, securitySize, - &Result->PolicyInfo, &Result->SigningTime, &Result->TimeStampPolicyInfo); + status = CiCheckSignedFile( + digest, + digestSize, + HashAlgId, + certificate, + securitySize, + &Result->PolicyInfo, + &Result->SigningTime, + &Result->TimeStampPolicyInfo + ); + if (NT_SUCCESS(status)) { RtlCopyMemory(Result->Hash, digest, digestSize); @@ -102,8 +200,17 @@ NTSTATUS DomitoValidatePortableExecutableEmbeddedSignature( } } } - if (digest) DomitoFreePortableExecutableDigest(digest); - if (!NT_SUCCESS(status)) DomitoClearCiDigest(Result); + + if (digest) + { + DomitoFreePortableExecutableDigest(digest); + } + + if (!NT_SUCCESS(status)) + { + DomitoClearCiDigest(Result); + } + return status; } #endif // NTDDI_VERSION >= NTDDI_WIN10 diff --git a/src/Domito.CodeIntegrity.cpp b/src/Domito.CodeIntegrity.cpp index 7e67656..56ee962 100644 --- a/src/Domito.CodeIntegrity.cpp +++ b/src/Domito.CodeIntegrity.cpp @@ -8,7 +8,6 @@ #include "Domito.Internal.h" #include "Domito.MinCrypt.h" - /* _ _ _ _ _ _ _ ___ _ _ * | | | | |_(_) (_) |_ _ _ | __| _ _ _ __| |_(_)___ _ _ ___ * | |_| | _| | | | _| || | | _| || | ' \/ _| _| / _ \ ' \(_-< @@ -28,44 +27,49 @@ DomitoGetPortableExecutableDigestKind( { return CALG_SHA1; } + const IMAGE_DATA_DIRECTORY security = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; const ULONG securityRva = security.VirtualAddress; const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate); + if (!securityRva || securityRva > PeSize || security.Size > PeSize - securityRva || security.Size < certificateHeaderSize) { return CALG_SHA1; } + ULONG certificateSize; memcpy(&certificateSize, pPeBytes + securityRva, sizeof(certificateSize)); + if (certificateSize < certificateHeaderSize || certificateSize > security.Size) { return CALG_SHA1; } + const PVOID pBase = pPeBytes + securityRva; const ULONG searchLen = certificateSize; PUCHAR pMatch = NULL; if (NT_SUCCESS(DomitoMemorySearchPattern( - (const PUCHAR)"\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", - 0x00, - 15, - pBase, - searchLen, - (PVOID*)&pMatch - ))) + (const PUCHAR) "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14", + 0x00, + 15, + pBase, + searchLen, + (PVOID*)&pMatch + ))) { return CALG_SHA1; } if (NT_SUCCESS(DomitoMemorySearchPattern( - (const PUCHAR)"\x30\xcc\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\xcc\x05\x00\x04\xcc", - 0xcc, - 19, - pBase, - searchLen, - (PVOID*)&pMatch - ))) + (const PUCHAR) "\x30\xcc\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\xcc\x05\x00\x04\xcc", + 0xcc, + 19, + pBase, + searchLen, + (PVOID*)&pMatch + ))) { if (pMatch == NULL) { @@ -97,26 +101,48 @@ typedef struct _DOMITO_DER_VIEW ULONG Length; } DOMITO_DER_VIEW; -static BOOLEAN DomitoDerTake(DOMITO_DER_VIEW* Input, UCHAR Tag, DOMITO_DER_VIEW* Value) +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) + + 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; @@ -124,11 +150,15 @@ static BOOLEAN DomitoDerTake(DOMITO_DER_VIEW* Input, UCHAR Tag, DOMITO_DER_VIEW* return TRUE; } -static BOOLEAN DomitoDerOid(DOMITO_DER_VIEW* Input, const UCHAR* Oid, ULONG Size) +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; + return DomitoDerTake(Input, 0x06, &value) && value.Length == Size && memcmp(value.Bytes, Oid, Size) == 0; } _IRQL_requires_max_(DISPATCH_LEVEL) @@ -137,83 +167,136 @@ 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) + _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) + } + + 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; + USHORT revision; + USHORT type; const UCHAR* certificate = PeBytes + security.VirtualAddress; memcpy(&length, certificate, 4); memcpy(&revision, certificate + 4, 2); memcpy(&type, certificate + 6, 2); - if (length <= 8 || length > security.Size) - return STATUS_INVALID_IMAGE_FORMAT; - if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA) - return STATUS_NOT_SUPPORTED; - const UCHAR signedDataOid[] = {0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x07,0x02}; - const UCHAR indirectOid[] = {0x2B,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x01,0x04}; - const UCHAR sha1Oid[] = {0x2B,0x0E,0x03,0x02,0x1A}; - const UCHAR sha2Prefix[] = {0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02}; - DOMITO_DER_VIEW payload = {certificate + 8, length - 8}; - DOMITO_DER_VIEW content, 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)) + if (length <= 8 || length > security.Size) + { return STATUS_INVALID_IMAGE_FORMAT; + } + + if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA) + { + return STATUS_NOT_SUPPORTED; + } + + const UCHAR signedDataOid[] = {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02}; + const UCHAR indirectOid[] = {0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x04}; + const UCHAR sha1Oid[] = {0x2B, 0x0E, 0x03, 0x02, 0x1A}; + const UCHAR sha2Prefix[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02}; + DOMITO_DER_VIEW payload = {certificate + 8, length - 8}; + DOMITO_DER_VIEW content; + DOMITO_DER_VIEW wrapped; + DOMITO_DER_VIEW signedData; + DOMITO_DER_VIEW ignored; + DOMITO_DER_VIEW contentInfo; + DOMITO_DER_VIEW indirect; + DOMITO_DER_VIEW digest; + DOMITO_DER_VIEW algorithm; + DOMITO_DER_VIEW oid; + + if (!DomitoDerTake(&payload, 0x30, &content) || !DomitoDerOid(&content, signedDataOid, sizeof(signedDataOid)) || + !DomitoDerTake(&content, 0xA0, &wrapped) || content.Length || !DomitoDerTake(&wrapped, 0x30, &signedData) || + wrapped.Length || !DomitoDerTake(&signedData, 0x02, &ignored) || !DomitoDerTake(&signedData, 0x31, &ignored) || + !DomitoDerTake(&signedData, 0x30, &contentInfo) || + !DomitoDerOid(&contentInfo, indirectOid, sizeof(indirectOid)) || !DomitoDerTake(&contentInfo, 0xA0, &wrapped) || + contentInfo.Length || !DomitoDerTake(&wrapped, 0x30, &indirect) || wrapped.Length || + !DomitoDerTake(&indirect, 0x30, &ignored) || !DomitoDerTake(&indirect, 0x30, &digest) || indirect.Length || + !DomitoDerTake(&digest, 0x30, &algorithm) || !DomitoDerTake(&algorithm, 0x06, &oid)) + { + return STATUS_INVALID_IMAGE_FORMAT; + } + // Windows-signed entries can include up to seven zero alignment bytes in // dwLength itself. Accept only that bounded, aligned padding after the DER. if (payload.Length > 7 || (payload.Length && (length & 7))) + { return STATUS_INVALID_IMAGE_FORMAT; + } + for (ULONG i = 0; i < payload.Length; ++i) + { if (payload.Bytes[i]) + { return STATUS_INVALID_IMAGE_FORMAT; - if (algorithm.Length && (!DomitoDerTake(&algorithm, 0x05, &ignored) || - ignored.Length || algorithm.Length)) + } + } + + 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))) + 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; + 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; } @@ -221,114 +304,202 @@ DomitoGetPortableExecutableDigestKindEx( // 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) +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; + } + + ULONG count; + ULONG headers; memcpy(&count, Bytes + optional + directory - 4, sizeof(count)); memcpy(&headers, Bytes + optional + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfHeaders), sizeof(headers)); + if (count > (file.SizeOfOptionalHeader - directory) / sizeof(IMAGE_DATA_DIRECTORY)) + { return STATUS_INVALID_IMAGE_FORMAT; + } + if (count <= IMAGE_DIRECTORY_ENTRY_SECURITY) + { return STATUS_NOT_SUPPORTED; // SIP exclusion behavior differs for absent slots. + } + const ULONG table = optional + file.SizeOfOptionalHeader; const ULONG tableSize = file.NumberOfSections * sizeof(IMAGE_SECTION_HEADER); + if (tableSize > Size - table || headers < table + tableSize || headers > Size) + { return STATUS_INVALID_IMAGE_FORMAT; - struct { ULONG Offset; ULONG Length; } sections[96]; + } + + 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)); + 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; + USHORT revision; + USHORT type; memcpy(&length, Bytes + offset, 4); memcpy(&revision, Bytes + offset + 4, 2); memcpy(&type, Bytes + offset + 6, 2); + if (length <= 8 || length > Size - offset) + { return STATUS_INVALID_IMAGE_FORMAT; + } + if (revision != 0x0200 || type != WIN_CERT_TYPE_PKCS_SIGNED_DATA) + { return STATUS_NOT_SUPPORTED; + } + const ULONG padding = (8 - (length & 7)) & 7; + if (padding > Size - offset - length) + { return STATUS_INVALID_IMAGE_FORMAT; + } + offset += length + padding; } } } + return STATUS_SUCCESS; } - _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) @@ -339,13 +510,12 @@ DomitoCalculatePortableExecutableDigestInternal( _In_ ULONG PeSize, _Out_ PUINT32 pDigestCalgOut, _Out_ PULONG pDigestSizeOut, - _Out_ PVOID * pDigestOut, - _Outptr_result_maybenull_ LPWIN_CERTIFICATE * pCertOut, + _Out_ PVOID* pDigestOut, + _Outptr_result_maybenull_ LPWIN_CERTIFICATE* pCertOut, _Out_ PULONG pSizeOfSecurityDirectory ) { - if (!pDigestCalgOut || !pDigestSizeOut || !pDigestOut || - !pCertOut || !pSizeOfSecurityDirectory) + if (!pDigestCalgOut || !pDigestSizeOut || !pDigestOut || !pCertOut || !pSizeOfSecurityDirectory) { return STATUS_INVALID_PARAMETER; } @@ -360,6 +530,7 @@ DomitoCalculatePortableExecutableDigestInternal( { return STATUS_INVALID_PARAMETER; } + if (PeSize < sizeof(IMAGE_DOS_HEADER)) { return STATUS_INVALID_IMAGE_NOT_MZ; @@ -368,10 +539,12 @@ DomitoCalculatePortableExecutableDigestInternal( // Copy fields rather than assuming the caller's buffer is aligned. IMAGE_DOS_HEADER dos = {}; memcpy(&dos, pPeBytes, sizeof(dos)); + if (dos.e_magic != IMAGE_DOS_SIGNATURE) { return STATUS_INVALID_IMAGE_NOT_MZ; } + if (dos.e_lfanew < 0) { return STATUS_INVALID_IMAGE_FORMAT; @@ -379,14 +552,17 @@ DomitoCalculatePortableExecutableDigestInternal( const ULONG ntOffset = (ULONG)dos.e_lfanew; const ULONG ntPrefixSize = sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER); + if (ntOffset > PeSize || ntPrefixSize > PeSize - ntOffset) { return STATUS_INVALID_IMAGE_FORMAT; } + ULONG signature; IMAGE_FILE_HEADER fileHeader; memcpy(&signature, pPeBytes + ntOffset, sizeof(signature)); memcpy(&fileHeader, pPeBytes + ntOffset + sizeof(signature), sizeof(fileHeader)); + if (signature != IMAGE_NT_SIGNATURE) { return STATUS_INVALID_IMAGE_FORMAT; @@ -394,15 +570,18 @@ DomitoCalculatePortableExecutableDigestInternal( const ULONG optionalOffset = ntOffset + ntPrefixSize; const ULONG optionalSize = fileHeader.SizeOfOptionalHeader; + if (optionalSize < sizeof(USHORT) || optionalSize > PeSize - optionalOffset) { return STATUS_INVALID_IMAGE_FORMAT; } + USHORT magic; memcpy(&magic, pPeBytes + optionalOffset, sizeof(magic)); ULONG directoryOffset; ULONG checksumOffset; ULONG directoryCountOffset; + if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { directoryOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory); @@ -419,12 +598,15 @@ DomitoCalculatePortableExecutableDigestInternal( { return STATUS_INVALID_IMAGE_FORMAT; } + if (optionalSize < directoryOffset) { return STATUS_INVALID_IMAGE_FORMAT; } + ULONG directoryCount; memcpy(&directoryCount, pPeBytes + optionalOffset + directoryCountOffset, sizeof(directoryCount)); + if (directoryCount > (optionalSize - directoryOffset) / sizeof(IMAGE_DATA_DIRECTORY)) { return STATUS_INVALID_IMAGE_FORMAT; @@ -435,32 +617,39 @@ DomitoCalculatePortableExecutableDigestInternal( IMAGE_DATA_DIRECTORY directories[IMAGE_DIRECTORY_ENTRY_SECURITY + 1] = {}; ULONG securityEntryOffset = PeSize; ULONG dataOffset = PeSize; + if (hasSecurityEntry) { - securityEntryOffset = optionalOffset + directoryOffset + - IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(IMAGE_DATA_DIRECTORY); - memcpy(&directories[IMAGE_DIRECTORY_ENTRY_SECURITY], pPeBytes + securityEntryOffset, - sizeof(IMAGE_DATA_DIRECTORY)); + securityEntryOffset = + optionalOffset + directoryOffset + IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(IMAGE_DATA_DIRECTORY); + memcpy( + &directories[IMAGE_DIRECTORY_ENTRY_SECURITY], pPeBytes + securityEntryOffset, sizeof(IMAGE_DATA_DIRECTORY) + ); dataOffset = securityEntryOffset + sizeof(IMAGE_DATA_DIRECTORY); } + const IMAGE_DATA_DIRECTORY security = directories[IMAGE_DIRECTORY_ENTRY_SECURITY]; const BOOLEAN hasEmbeddedSig = security.VirtualAddress != 0; + if (hasEmbeddedSig != (security.Size != 0)) { return STATUS_INVALID_IMAGE_FORMAT; } + if (hasEmbeddedSig) { // The security directory contains a FILE OFFSET, not an image RVA. const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate); + if (security.VirtualAddress < dataOffset || security.VirtualAddress > PeSize || - security.Size > PeSize - security.VirtualAddress || - security.Size < certificateHeaderSize) + security.Size > PeSize - security.VirtualAddress || security.Size < certificateHeaderSize) { return STATUS_INVALID_IMAGE_FORMAT; } + ULONG certificateSize; memcpy(&certificateSize, pPeBytes + security.VirtualAddress, sizeof(certificateSize)); + if (certificateSize < certificateHeaderSize || certificateSize > security.Size) { return STATUS_INVALID_IMAGE_FORMAT; @@ -472,30 +661,38 @@ DomitoCalculatePortableExecutableDigestInternal( BCRYPT_HASH_HANDLE hbHash = nullptr; ULONG hashLength = 0; ULONG resultLength = 0; - const UINT32 peDigestKind = ExplicitCalg ? ExplicitCalg : - DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories); - NTSTATUS status = BCryptOpenAlgorithmProvider( - &hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0); - if (!NT_SUCCESS(status)) - { - goto cleanup; - } - status = BCryptGetProperty(hbAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength, - sizeof(hashLength), &resultLength, 0); + // Intentional compatibility branch; the explicit API never calls the heuristic. +#pragma warning(push) +#pragma warning(disable : 4996) + const UINT32 peDigestKind = + ExplicitCalg ? ExplicitCalg : DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories); +#pragma warning(pop) + NTSTATUS status = + BCryptOpenAlgorithmProvider(&hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0); + if (!NT_SUCCESS(status)) { goto cleanup; } -#pragma warning(disable:4996) + status = BCryptGetProperty(hbAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength, sizeof(hashLength), &resultLength, 0); + + if (!NT_SUCCESS(status)) + { + goto cleanup; + } + +#pragma warning(disable : 4996) pHash = (PUCHAR)ExAllocatePoolWithTag(NonPagedPoolNx, hashLength, DOMITO_POOL_TAG); -#pragma warning(default:4996) +#pragma warning(default : 4996) if (!pHash) { status = STATUS_INSUFFICIENT_RESOURCES; goto cleanup; } + status = BCryptCreateHash(hbAlg, &hbHash, nullptr, 0, nullptr, 0, 0); + if (!NT_SUCCESS(status)) { goto cleanup; @@ -505,26 +702,35 @@ DomitoCalculatePortableExecutableDigestInternal( // directly: no fixed-size scratch buffer and no unchecked memcpy lengths. // General section-ordered Authenticode hashing is a separate change. status = BCryptHashData(hbHash, pPeBytes, checksumOffset, 0); + if (!NT_SUCCESS(status)) { goto cleanup; } - status = BCryptHashData(hbHash, pPeBytes + checksumOffset + sizeof(ULONG), - securityEntryOffset - checksumOffset - sizeof(ULONG), 0); + + status = BCryptHashData( + hbHash, pPeBytes + checksumOffset + sizeof(ULONG), securityEntryOffset - checksumOffset - sizeof(ULONG), 0 + ); + if (!NT_SUCCESS(status)) { goto cleanup; } + if (hasSecurityEntry) { - status = BCryptHashData(hbHash, pPeBytes + dataOffset, - (hasEmbeddedSig ? security.VirtualAddress : PeSize) - dataOffset, 0); + status = BCryptHashData( + hbHash, pPeBytes + dataOffset, (hasEmbeddedSig ? security.VirtualAddress : PeSize) - dataOffset, 0 + ); + if (!NT_SUCCESS(status)) { goto cleanup; } } + status = BCryptFinishHash(hbHash, pHash, hashLength, 0); + if (!NT_SUCCESS(status)) { goto cleanup; @@ -534,6 +740,7 @@ DomitoCalculatePortableExecutableDigestInternal( *pDigestSizeOut = hashLength; *pDigestOut = pHash; pHash = nullptr; // Transfer ownership only after all work has succeeded. + if (hasEmbeddedSig) { *pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + security.VirtualAddress); @@ -541,32 +748,42 @@ DomitoCalculatePortableExecutableDigestInternal( } cleanup: + if (hbHash) { BCryptDestroyHash(hbHash); } + if (pHash) { ExFreePoolWithTag(pHash, DOMITO_POOL_TAG); } + if (hbAlg) { BCryptCloseAlgorithmProvider(hbAlg, 0); } + return status; } - _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS DomitoCalculatePortableExecutableDigest( - PUCHAR PeBytes, ULONG PeSize, PUINT32 DigestCalg, PULONG DigestSize, - PVOID* Digest, LPWIN_CERTIFICATE* Certificate, PULONG SecuritySize) + 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); + return DomitoCalculatePortableExecutableDigestInternal( + 0, PeBytes, PeSize, DigestCalg, DigestSize, Digest, Certificate, SecuritySize + ); } _Success_(return == STATUS_SUCCESS) @@ -574,28 +791,48 @@ _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) + 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) + } + + 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); + return DomitoCalculatePortableExecutableDigestInternal( + HashAlgId, (PUCHAR)PeBytes, PeSize, &actualCalg, DigestSize, Digest, Certificate, SecuritySize + ); } _IRQL_requires_max_(DISPATCH_LEVEL) @@ -622,10 +859,10 @@ DomitoValidateFileLegacyModeInternal( _In_ PVOID Hash, _In_ UINT32 HashSize, _In_ ALG_ID HashAlgId, - _In_ const IMAGE_DATA_DIRECTORY * SecurityDirectory, - _Inout_ MINCRYPT_POLICY_INFO * PolicyInfo, - _Out_ LARGE_INTEGER * SigningTime, - _Inout_ MINCRYPT_POLICY_INFO * TimeStampPolicyInfo, + _In_ const IMAGE_DATA_DIRECTORY* SecurityDirectory, + _Inout_ MINCRYPT_POLICY_INFO* PolicyInfo, + _Out_ LARGE_INTEGER* SigningTime, + _Inout_ MINCRYPT_POLICY_INFO* TimeStampPolicyInfo, _In_ UINT32 AcceptedCatalogPolicyBits, _In_ BOOLEAN RequireTrustedCatalog, _Out_ PULONG ValidationSource @@ -634,11 +871,18 @@ DomitoValidateFileLegacyModeInternal( PAGED_CODE(); if (!ValidationSource) + { return STATUS_INVALID_PARAMETER; + } + *ValidationSource = 0; - if (!Hash || !SecurityDirectory || !PolicyInfo || !SigningTime || - !TimeStampPolicyInfo || PolicyInfo == TimeStampPolicyInfo) + + if (!Hash || !SecurityDirectory || !PolicyInfo || !SigningTime || !TimeStampPolicyInfo || + PolicyInfo == TimeStampPolicyInfo) + { return STATUS_INVALID_PARAMETER; + } + NTSTATUS status = STATUS_SUCCESS; PVOID certDirectory = nullptr; KAPC_STATE systemContext = {}; @@ -655,50 +899,56 @@ DomitoValidateFileLegacyModeInternal( status = STATUS_NOT_SUPPORTED; break; } + if (HashSize != (UINT32)(HashAlgId == CALG_SHA1 ? MINCRYPT_SHA1_LENGTH : MINCRYPT_SHA256_LENGTH)) { status = STATUS_INVALID_PARAMETER; break; } + if ((SecurityDirectory->Size != 0) != (SecurityDirectory->VirtualAddress != 0)) { status = STATUS_INVALID_IMAGE_FORMAT; break; } - if (SecurityDirectory->Size != 0u && - SecurityDirectory->VirtualAddress != 0u) + if (SecurityDirectory->Size != 0u && SecurityDirectory->VirtualAddress != 0u) { if ((SecurityDirectory->VirtualAddress & 7) || SecurityDirectory->Size < 12) { status = STATUS_INVALID_IMAGE_FORMAT; break; } + // Bound the allocation/read against the file before trusting its table. PFILE_OBJECT fileObject = nullptr; - status = ObReferenceObjectByHandle(FileHandle, FILE_READ_DATA, *IoFileObjectType, - KernelMode, (PVOID*)&fileObject, nullptr); + 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 || + } + + if (fileSize.QuadPart < 0 || (ULONGLONG)SecurityDirectory->VirtualAddress > (ULONGLONG)fileSize.QuadPart || SecurityDirectory->Size > (ULONGLONG)fileSize.QuadPart - SecurityDirectory->VirtualAddress) { status = STATUS_INVALID_IMAGE_FORMAT; break; } -#pragma warning(disable:4996) - certDirectory = ExAllocatePoolWithTag( - PagedPool, - SecurityDirectory->Size, - DOMITO_POOL_TAG - ); -#pragma warning(default:4996) +#pragma warning(disable : 4996) + certDirectory = ExAllocatePoolWithTag(PagedPool, SecurityDirectory->Size, DOMITO_POOL_TAG); +#pragma warning(default : 4996) if (certDirectory == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; @@ -708,8 +958,11 @@ DomitoValidateFileLegacyModeInternal( LARGE_INTEGER offset = {}; offset.QuadPart = SecurityDirectory->VirtualAddress; status = DomitoReadFileAt(FileHandle, certDirectory, SecurityDirectory->Size, &offset); + if (!NT_SUCCESS(status)) + { break; + } KeStackAttachProcess(PsInitialSystemProcess, &systemContext); { @@ -724,6 +977,7 @@ DomitoValidateFileLegacyModeInternal( TimeStampPolicyInfo ); } + KeUnstackDetachProcess(&systemContext); if (NT_SUCCESS(status)) @@ -775,9 +1029,13 @@ DomitoValidateFileLegacyModeInternal( ); } } + KeUnstackDetachProcess(&systemContext); + if (NT_SUCCESS(status)) + { *ValidationSource = DOMITO_CI_SOURCE_CATALOG; + } } while (FALSE); @@ -794,14 +1052,30 @@ _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) + 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); + return DomitoValidateFileLegacyModeInternal( + FileHandle, + Hash, + HashSize, + HashAlgId, + SecurityDirectory, + PolicyInfo, + SigningTime, + TimeStampPolicyInfo, + DOMITO_CI_LEGACY_CATALOG_POLICY_BITS, + FALSE, + &source + ); } _Success_(return == STATUS_SUCCESS) @@ -809,13 +1083,32 @@ _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) + 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); + return DomitoValidateFileLegacyModeInternal( + FileHandle, + Hash, + HashSize, + HashAlgId, + SecurityDirectory, + PolicyInfo, + SigningTime, + TimeStampPolicyInfo, + AcceptedCatalogPolicyBits, + RequireTrustedCatalog, + ValidationSource + ); } + #pragma code_seg() diff --git a/src/Domito.Consumer.cpp b/src/Domito.Consumer.cpp new file mode 100644 index 0000000..c14404b --- /dev/null +++ b/src/Domito.Consumer.cpp @@ -0,0 +1,395 @@ +#include "Domito.Internal.h" + +_Use_decl_annotations_ +NTSTATUS +DomitoFindModuleExportedFunctionAddress( + PANSI_STRING ModuleName, + PANSI_STRING FunctionName, + PVOID* FunctionAddress +) +{ + PAGED_CODE(); + + if (!FunctionAddress) + { + return STATUS_INVALID_PARAMETER; + } + + *FunctionAddress = nullptr; + + if (!ModuleName || !ModuleName->Buffer || !ModuleName->Length || ModuleName->Length > ModuleName->MaximumLength || + !FunctionName || !FunctionName->Buffer || !FunctionName->Length || + FunctionName->Length > FunctionName->MaximumLength) + { + return STATUS_INVALID_PARAMETER; + } + + PVOID moduleBase = nullptr; + NTSTATUS status = DomitoFindModuleBaseAddress(ModuleName, &moduleBase); + + if (!NT_SUCCESS(status)) + { + return status; + } + + return DomitoFindExportedFunctionAddress(moduleBase, FunctionName, FunctionAddress); +} + +_Use_decl_annotations_ +void +DomitoFreeProcessImageName( + PUNICODE_STRING ImageName +) +{ + PAGED_CODE(); + + if (ImageName) + { + G_Memory.Free(ImageName); + } +} + +_Use_decl_annotations_ +NTSTATUS +DomitoGetProcessImageNameEx( + PEPROCESS Process, + PUNICODE_STRING* ImageName +) +{ + PAGED_CODE(); + + if (!ImageName) + { + return STATUS_INVALID_PARAMETER; + } + + *ImageName = nullptr; + + if (!Process) + { + return STATUS_INVALID_PARAMETER; + } + + PUNICODE_STRING processImageName = nullptr; + NTSTATUS status = SeLocateProcessImageName(Process, &processImageName); + + if (NT_SUCCESS(status)) + { + if (!processImageName || !processImageName->Buffer || !processImageName->Length || + (processImageName->Length & 1) || processImageName->Length > processImageName->MaximumLength || + processImageName->Length > MAXUSHORT - sizeof(WCHAR)) + { + status = STATUS_INVALID_BUFFER_SIZE; + } + else + { + const SIZE_T size = sizeof(UNICODE_STRING) + processImageName->Length + sizeof(WCHAR); + PUNICODE_STRING imageName = static_cast(G_Memory.Allocate(size)); + + if (!imageName) + { + status = STATUS_INSUFFICIENT_RESOURCES; + } + else + { + imageName->Buffer = reinterpret_cast(imageName + 1); + imageName->Length = processImageName->Length; + imageName->MaximumLength = processImageName->Length + sizeof(WCHAR); + RtlCopyMemory(imageName->Buffer, processImageName->Buffer, processImageName->Length); + imageName->Buffer[imageName->Length / sizeof(WCHAR)] = 0; + *ImageName = imageName; + } + } + } + + if (processImageName) + { + ExFreePool(processImageName); + } + + return status; +} + +#if (NTDDI_VERSION >= NTDDI_WIN10) +_Use_decl_annotations_ +NTSTATUS +DomitoValidateFileHandleEx( + HANDLE FileHandle, + UINT32 ValidationFlags, + UINT8 RequestedSigningLevel, + DOMITO_CI_SIGNATURE_SELECTION Selection, + DOMITO_CI_VALIDATION_RESULT* Result +) +{ + PAGED_CODE(); + + if (!Result) + { + return STATUS_INVALID_PARAMETER; + } + + DomitoFreeCiValidationResult(Result); + + if (!FileHandle) + { + return STATUS_INVALID_PARAMETER; + } + + PFILE_OBJECT fileObject = nullptr; + NTSTATUS status = ObReferenceObjectByHandle( + FileHandle, FILE_READ_DATA, *IoFileObjectType, KernelMode, reinterpret_cast(&fileObject), nullptr + ); + + if (!NT_SUCCESS(status)) + { + return status; + } + + status = DomitoValidateFileObjectEx(fileObject, ValidationFlags, RequestedSigningLevel, Selection, Result); + ObDereferenceObject(fileObject); + return status; +} + +_Use_decl_annotations_ +NTSTATUS +DomitoValidateFilePathEx( + const UNICODE_STRING* Path, + UINT32 ValidationFlags, + UINT8 RequestedSigningLevel, + DOMITO_CI_SIGNATURE_SELECTION Selection, + DOMITO_CI_VALIDATION_RESULT* Result +) +{ + PAGED_CODE(); + + if (!Result) + { + return STATUS_INVALID_PARAMETER; + } + + DomitoFreeCiValidationResult(Result); + + if (!Path || !Path->Buffer || Path->Length < sizeof(WCHAR) || (Path->Length & 1) || + Path->Length > Path->MaximumLength || Path->Buffer[0] != L'\\') + { + return STATUS_INVALID_PARAMETER; + } + + for (USHORT i = 0; i < Path->Length / sizeof(WCHAR); ++i) + { + if (!Path->Buffer[i]) + { + return STATUS_INVALID_PARAMETER; + } + } + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes( + &attributes, const_cast(Path), OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, nullptr, nullptr + ); + HANDLE fileHandle = nullptr; + IO_STATUS_BLOCK iosb = {}; + NTSTATUS status = ZwOpenFile( + &fileHandle, + FILE_READ_DATA | SYNCHRONIZE, + &attributes, + &iosb, + FILE_SHARE_READ, + FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT + ); + + if (!NT_SUCCESS(status)) + { + return status; + } + + status = DomitoValidateFileHandleEx(fileHandle, ValidationFlags, RequestedSigningLevel, Selection, Result); + ZwClose(fileHandle); + return status; +} + +#if defined(_M_AMD64) || defined(_AMD64_) +static BOOLEAN +DomitoCiOwnedSpan( + const MINCRYPT_CHAIN_INFO* Chain, + const void* Data, + SIZE_T Size +) +{ + const ULONG_PTR chainBaseAddress = reinterpret_cast(Chain); + const ULONG_PTR blobAddress = reinterpret_cast(Data); + // Empty blobs may be NULL; they are never dereferenced by this accessor. + if (!Size) + { + return TRUE; + } + + return blobAddress >= chainBaseAddress && blobAddress - chainBaseAddress >= 56 && + blobAddress - chainBaseAddress <= Chain->Size && Size <= Chain->Size - (blobAddress - chainBaseAddress); +} +#endif + +_Use_decl_annotations_ +NTSTATUS +DomitoGetCiChainView( + const MINCRYPT_POLICY_INFO* Policy, + DOMITO_CI_CHAIN_LAYOUT Layout, + DOMITO_CI_CHAIN_VIEW* View +) +{ + PAGED_CODE(); + + if (!View) + { + return STATUS_INVALID_PARAMETER; + } + + *View = {}; + + if (!Policy || Layout != DomitoCiChainLayoutObservedX64) + { + return STATUS_INVALID_PARAMETER; + } +#if defined(_M_AMD64) || defined(_AMD64_) + C_ASSERT(sizeof(MINCRYPT_CHAIN_ELEMENT) == 120); + C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, ChainElements) == 40); + C_ASSERT(FIELD_OFFSET(MINCRYPT_CHAIN_INFO, NumberOfChainElement) == 48); + + if (Policy->Size != sizeof(*Policy)) + { + return STATUS_NOT_SUPPORTED; + } + + const MINCRYPT_CHAIN_INFO* chain = Policy->ChainInfo; + + if (!chain) + { + return STATUS_NOT_FOUND; + } + + if (chain->Size < 56) + { + return STATUS_INVALID_BUFFER_SIZE; + } + + const ULONG count = chain->NumberOfChainElement; + + if (!count) + { + return STATUS_NOT_FOUND; + } + + if (count > (chain->Size - 56) / sizeof(MINCRYPT_CHAIN_ELEMENT) || + !DomitoCiOwnedSpan(chain, chain->ChainElements, static_cast(count) * sizeof(MINCRYPT_CHAIN_ELEMENT))) + { + return STATUS_INVALID_BUFFER_SIZE; + } + + for (ULONG i = 0; i < count; ++i) + { + const MINCRYPT_CHAIN_ELEMENT& element = chain->ChainElements[i]; + + if (element.HashSize > sizeof(element.Hash) || !element.Certificate.Size || + !DomitoCiOwnedSpan(chain, element.Certificate.Data, element.Certificate.Size) || + !DomitoCiOwnedSpan(chain, element.Subject.Data, element.Subject.Size) || + !DomitoCiOwnedSpan(chain, element.Issuer.Data, element.Issuer.Size)) + { + return STATUS_INVALID_BUFFER_SIZE; + } + } + + ULONG rootId; + RtlCopyMemory(&rootId, reinterpret_cast(chain) + 52, sizeof(rootId)); + *View = {chain->ChainElements, count, rootId}; + return STATUS_SUCCESS; +#else + return STATUS_NOT_SUPPORTED; +#endif +} + +static MINCRYPT_ALLOCATE_ROUTINE DomitoAllocatePublisher; + +static PVOID NTAPI +DomitoAllocatePublisher( + SIZE_T Size +) +{ +#pragma warning(suppress : 4996) + return ExAllocatePoolWithTag(PagedPool, Size, DOMITO_POOL_TAG); +} + +_Use_decl_annotations_ +void +DomitoFreeCertificatePublisherName( + PUNICODE_STRING PublisherName +) +{ + PAGED_CODE(); + + if (!PublisherName) + { + return; + } + + if (PublisherName->Buffer) + { + ExFreePoolWithTag(PublisherName->Buffer, DOMITO_POOL_TAG); + } + + *PublisherName = {}; +} + +_Use_decl_annotations_ +NTSTATUS +DomitoGetCertificatePublisherName( + const MINCERT_BLOB* Certificate, + PUNICODE_STRING PublisherName +) +{ + PAGED_CODE(); + + if (!PublisherName) + { + return STATUS_INVALID_PARAMETER; + } + + *PublisherName = {}; + + if (!Certificate || !Certificate->Data || !Certificate->Size) + { + return STATUS_INVALID_PARAMETER; + } + + ANSI_STRING moduleName = RTL_CONSTANT_STRING("\\SystemRoot\\system32\\CI.dll"); + ANSI_STRING exportName = RTL_CONSTANT_STRING("CiGetCertPublisherName"); + PVOID publisherRoutine = nullptr; + NTSTATUS status = DomitoFindModuleExportedFunctionAddress(&moduleName, &exportName, &publisherRoutine); + + if (!NT_SUCCESS(status)) + { + return status; + } + + if (!publisherRoutine) + { + return STATUS_PROCEDURE_NOT_FOUND; + } + + typedef NTSTATUS(NTAPI * GET_PUBLISHER)(MINCERT_BLOB*, PMINCRYPT_ALLOCATE_ROUTINE, PUNICODE_STRING); + const GET_PUBLISHER getPublisher = reinterpret_cast(publisherRoutine); + status = getPublisher(const_cast(Certificate), DomitoAllocatePublisher, PublisherName); + + if (NT_SUCCESS(status) && (PublisherName->Length > PublisherName->MaximumLength || (PublisherName->Length & 1) || + !PublisherName->Length || !PublisherName->Buffer)) + { + status = STATUS_INVALID_BUFFER_SIZE; + } + + if (!NT_SUCCESS(status)) + { + DomitoFreeCertificatePublisherName(PublisherName); + } + + return status; +} +#endif diff --git a/src/Domito.cpp b/src/Domito.cpp index 1ea20e4..4753fdb 100644 --- a/src/Domito.cpp +++ b/src/Domito.cpp @@ -7,18 +7,26 @@ #include "Domito.Internal.h" - DOMITO_COMMON G_Common = {}; -DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QipRoutineName, L"ZwQueryInformationProcess"); -DECLARE_GLOBAL_CONST_UNICODE_STRING(G_IdetdRoutineName, L"RtlImageDirectoryEntryToData"); -DECLARE_GLOBAL_CONST_UNICODE_STRING(G_QsiRoutineName, L"ZwQuerySystemInformation"); +DECLARE_GLOBAL_CONST_UNICODE_STRING( + G_QipRoutineName, + L"ZwQueryInformationProcess" +); +DECLARE_GLOBAL_CONST_UNICODE_STRING( + G_IdetdRoutineName, + L"RtlImageDirectoryEntryToData" +); +DECLARE_GLOBAL_CONST_UNICODE_STRING( + G_QsiRoutineName, + L"ZwQuerySystemInformation" +); #ifndef LOG -#define LOG(Format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[Domito][" __FUNCTION__ "] " Format " \n", __VA_ARGS__) +#define LOG(Format, ...) \ + DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "[Domito][" __FUNCTION__ "] " Format " \n", __VA_ARGS__) #endif - _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) @@ -27,7 +35,7 @@ DomitoInit() { // // Do those first since the follow-up code depends on them - // + // G_Common.ZwQueryInformationProcess = (t_ZwQueryInformationProcess)MmGetSystemRoutineAddress((PUNICODE_STRING)&G_QipRoutineName); @@ -43,19 +51,27 @@ _IRQL_requires_max_(PASSIVE_LEVEL) void DomitoShutdown() { - } - _Success_(return == STATUS_SUCCESS) _Must_inspect_result_ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS DomitoFindModuleBaseAddress( _In_ PANSI_STRING ModuleName, - _Inout_opt_ PVOID * ModuleBase + _Inout_opt_ PVOID* ModuleBase ) { + if (ModuleBase) + { + *ModuleBase = nullptr; + } + + if (!ModuleName || !ModuleName->Buffer || !ModuleName->Length || ModuleName->Length > ModuleName->MaximumLength) + { + return STATUS_INVALID_PARAMETER; + } + ULONG bufferSize = 0; PSYSTEM_MODULE_INFORMATION moduleInfo = NULL; @@ -67,12 +83,7 @@ DomitoFindModuleBaseAddress( const ULONG SystemModuleInformation = 11; // Query the required buffer size for module information - NTSTATUS status = G_Common.ZwQuerySystemInformation( - SystemModuleInformation, - &bufferSize, - 0, - &bufferSize - ); + NTSTATUS status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, &bufferSize, 0, &bufferSize); if (status != STATUS_INFO_LENGTH_MISMATCH) { @@ -80,9 +91,7 @@ DomitoFindModuleBaseAddress( } // Allocate memory for the module information - moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate( - bufferSize - ); + moduleInfo = (PSYSTEM_MODULE_INFORMATION)G_Memory.Allocate(bufferSize); if (moduleInfo == NULL) { @@ -90,12 +99,7 @@ DomitoFindModuleBaseAddress( } // Retrieve the module information - status = G_Common.ZwQuerySystemInformation( - SystemModuleInformation, - moduleInfo, - bufferSize, - NULL - ); + status = G_Common.ZwQuerySystemInformation(SystemModuleInformation, moduleInfo, bufferSize, NULL); if (!NT_SUCCESS(status)) { @@ -120,6 +124,7 @@ DomitoFindModuleBaseAddress( { *ModuleBase = moduleInfo->Module[i].Base; } + break; } } @@ -131,7 +136,13 @@ DomitoFindModuleBaseAddress( // 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) +static BOOLEAN +DomitoImageRvaSpan( + ULONG ImageSize, + ULONG Rva, + ULONG Count, + ULONG ElementSize +) { return Rva <= ImageSize && ElementSize != 0 && Count <= (ImageSize - Rva) / ElementSize; } @@ -143,17 +154,19 @@ NTSTATUS DomitoFindExportedFunctionAddress( _In_ PVOID ModuleBase, _In_ PANSI_STRING FunctionName, - _Inout_opt_ PVOID * FunctionAddress + _Inout_opt_ PVOID* FunctionAddress ) { if (FunctionAddress) { *FunctionAddress = nullptr; } + if (!ModuleBase || !FunctionName || !FunctionName->Buffer || !FunctionName->Length) { return STATUS_INVALID_PARAMETER; } + if (!G_Common.RtlImageDirectoryEntryToData) { return STATUS_NOT_IMPLEMENTED; @@ -163,95 +176,123 @@ DomitoFindExportedFunctionAddress( const PUCHAR base = (PUCHAR)ModuleBase; IMAGE_DOS_HEADER dos; memcpy(&dos, base, sizeof(dos)); + if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < (LONG)sizeof(dos)) { return STATUS_INVALID_IMAGE_FORMAT; } + const PUCHAR nt = base + dos.e_lfanew; ULONG signature; memcpy(&signature, nt, sizeof(signature)); USHORT magic; memcpy(&magic, nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER), sizeof(magic)); + if (signature != IMAGE_NT_SIGNATURE || (magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC && magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)) { return STATUS_INVALID_IMAGE_FORMAT; } + // SizeOfImage is at the same offset in PE32 and PE32+ optional headers. ULONG imageSize; - memcpy(&imageSize, nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + - FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage), sizeof(imageSize)); - if (!DomitoImageRvaSpan(imageSize, (ULONG)dos.e_lfanew, - sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + - FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage) + sizeof(imageSize), 1)) + memcpy( + &imageSize, + nt + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage), + sizeof(imageSize) + ); + + if (!DomitoImageRvaSpan( + imageSize, + (ULONG)dos.e_lfanew, + sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER) + FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, SizeOfImage) + + sizeof(imageSize), + 1 + )) { return STATUS_INVALID_IMAGE_FORMAT; } ULONG exportSize = 0; - const PVOID directory = G_Common.RtlImageDirectoryEntryToData( - ModuleBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize); + const PVOID directory = + G_Common.RtlImageDirectoryEntryToData(ModuleBase, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize); const ULONG_PTR directoryOffset = (ULONG_PTR)directory - (ULONG_PTR)ModuleBase; + if (!directory || directoryOffset > imageSize || exportSize < sizeof(IMAGE_EXPORT_DIRECTORY) || exportSize > imageSize - directoryOffset) { return STATUS_INVALID_IMAGE_FORMAT; } + IMAGE_EXPORT_DIRECTORY exports; memcpy(&exports, directory, sizeof(exports)); + if (!DomitoImageRvaSpan(imageSize, exports.AddressOfFunctions, exports.NumberOfFunctions, sizeof(ULONG)) || !DomitoImageRvaSpan(imageSize, exports.AddressOfNames, exports.NumberOfNames, sizeof(ULONG)) || !DomitoImageRvaSpan(imageSize, exports.AddressOfNameOrdinals, exports.NumberOfNames, sizeof(USHORT))) { return STATUS_INVALID_IMAGE_FORMAT; } + for (ULONG i = 0; i < exports.NumberOfNames; ++i) { ULONG nameRva; USHORT ordinal; memcpy(&nameRva, base + exports.AddressOfNames + i * sizeof(ULONG), sizeof(nameRva)); memcpy(&ordinal, base + exports.AddressOfNameOrdinals + i * sizeof(USHORT), sizeof(ordinal)); + if (nameRva >= imageSize || ordinal >= exports.NumberOfFunctions) { return STATUS_INVALID_IMAGE_FORMAT; } + ULONG length = 0; while (length < imageSize - nameRva && base[nameRva + length] != 0) { ++length; } + if (length == imageSize - nameRva || length > MAXUSHORT) { return STATUS_INVALID_IMAGE_FORMAT; } + ANSI_STRING current; current.Buffer = (PCHAR)(base + nameRva); current.Length = (USHORT)length; current.MaximumLength = (USHORT)length; + if (RtlCompareString(FunctionName, ¤t, TRUE) != 0) { continue; } + ULONG functionRva; memcpy(&functionRva, base + exports.AddressOfFunctions + ordinal * sizeof(ULONG), sizeof(functionRva)); + if (!functionRva) { return STATUS_NOT_FOUND; // Empty EAT slot. } + if (functionRva >= imageSize) { return STATUS_INVALID_IMAGE_FORMAT; } + if (functionRva >= directoryOffset && functionRva - directoryOffset < exportSize) { return STATUS_NOT_SUPPORTED; // Forwarder string, not a callable address. } + if (FunctionAddress) { *FunctionAddress = base + functionRva; } + return STATUS_SUCCESS; } + return STATUS_NOT_FOUND; } @@ -265,7 +306,7 @@ DomitoMemorySearchPattern( _In_ SIZE_T puLen, _In_ PVOID pcBase, _In_ SIZE_T puSize, - _Outptr_result_maybenull_ PVOID * ppMatch + _Outptr_result_maybenull_ PVOID* ppMatch ) { ASSERT(ppMatch != NULL && pcPattern != NULL && pcBase != NULL); @@ -285,6 +326,7 @@ DomitoMemorySearchPattern( for (SIZE_T i = 0; i + puLen <= puSize; i++) { BOOLEAN found = TRUE; + for (SIZE_T j = 0; j < puLen; j++) { if (pcPattern[j] != uWildcard && pcPattern[j] != ((PCUCHAR)pcBase)[i + j]) @@ -310,7 +352,7 @@ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS DomitoReadFileAt( _In_ HANDLE FileHandle, - _Out_ PVOID Buffer, + _Out_writes_bytes_(BufferSize) PVOID Buffer, _In_ ULONG BufferSize, _In_ const LARGE_INTEGER* ByteOffset ) @@ -319,6 +361,7 @@ DomitoReadFileAt( { return STATUS_INVALID_PARAMETER; } + if (!BufferSize) { return STATUS_SUCCESS; @@ -327,15 +370,16 @@ DomitoReadFileAt( HANDLE eventHandle = nullptr; OBJECT_ATTRIBUTES attributes; InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr); - NTSTATUS status = ZwCreateEvent(&eventHandle, EVENT_ALL_ACCESS, &attributes, - NotificationEvent, FALSE); + 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); + status = ObReferenceObjectByHandle(eventHandle, SYNCHRONIZE, *ExEventObjectType, KernelMode, &eventObject, nullptr); + if (!NT_SUCCESS(status)) { ZwClose(eventHandle); @@ -344,8 +388,9 @@ DomitoReadFileAt( IO_STATUS_BLOCK ioStatusBlock = {}; LARGE_INTEGER offset = *ByteOffset; - status = ZwReadFile(FileHandle, eventHandle, nullptr, nullptr, &ioStatusBlock, - Buffer, BufferSize, &offset, nullptr); + 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. @@ -353,14 +398,17 @@ DomitoReadFileAt( 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; @@ -387,95 +435,94 @@ _IRQL_requires_max_(PASSIVE_LEVEL) NTSTATUS DomitoGetProcessImageName( _In_ ULONG ProcessId, - _Inout_ PUNICODE_STRING * ProcessImageName + _Outptr_result_maybenull_ PUNICODE_STRING* ProcessImageName ) { - NTSTATUS status = STATUS_UNSUCCESSFUL; - ULONG returnedLength; - HANDLE hProcess = NULL; + PAGED_CODE(); - PAGED_CODE(); // this eliminates the possibility of the IDLE Thread/Process - - if (ProcessId == 0 || ProcessId == 4) - { - return STATUS_INVALID_PARAMETER_1; - } - - if (ProcessImageName == NULL) + if (!ProcessImageName) { return STATUS_INVALID_PARAMETER_2; } - CLIENT_ID cid; + *ProcessImageName = nullptr; + + if (!ProcessId || ProcessId == 4) + { + return STATUS_INVALID_PARAMETER_1; + } + + if (!G_Common.ZwQueryInformationProcess) + { + return STATUS_NOT_IMPLEMENTED; + } + + CLIENT_ID cid = {}; cid.UniqueProcess = ULongToHandle(ProcessId); - cid.UniqueThread = NULL; + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, nullptr, OBJ_KERNEL_HANDLE, nullptr, nullptr); + HANDLE process = nullptr; + // PROCESS_QUERY_LIMITED_INFORMATION (WinNT.h); not named by the WDK headers. + const ACCESS_MASK queryLimitedInformation = 0x1000; + NTSTATUS status = ZwOpenProcess(&process, queryLimitedInformation, &attributes, &cid); - OBJECT_ATTRIBUTES objAttr; - InitializeObjectAttributes( - &objAttr, - NULL, - OBJ_KERNEL_HANDLE, - NULL, - NULL - ); - - if (!NT_SUCCESS(status = ZwOpenProcess( - &hProcess, - PROCESS_ALL_ACCESS, - &objAttr, - &cid - ))) + if (!NT_SUCCESS(status)) { return status; } - if (G_Common.ZwQueryInformationProcess == NULL) + ULONG size = 0; + status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, nullptr, 0, &size); + + if (status == STATUS_INFO_LENGTH_MISMATCH) { - status = STATUS_NOT_IMPLEMENTED; - goto cleanUp; - } + if (size < sizeof(UNICODE_STRING)) + { + status = STATUS_INVALID_BUFFER_SIZE; + } + else + { + auto image = static_cast(G_Memory.Allocate(size)); - /* Query the actual size of the process path */ - status = G_Common.ZwQueryInformationProcess( - hProcess, - ProcessImageFileName, - NULL, // buffer - 0, // buffer size - &returnedLength - ); + if (!image) + { + status = STATUS_INSUFFICIENT_RESOURCES; + } + else + { + ULONG returned = 0; + status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, image, size, &returned); - if (STATUS_INFO_LENGTH_MISMATCH != status) - { - goto cleanUp; - } + if (NT_SUCCESS(status)) + { + const ULONG_PTR base = reinterpret_cast(image); + const ULONG_PTR buffer = reinterpret_cast(image->Buffer); - *ProcessImageName = (PUNICODE_STRING)G_Memory.Allocate(returnedLength); + if (!image->Length || (image->Length & 1) || image->Length > image->MaximumLength || + buffer < base || buffer - base < sizeof(UNICODE_STRING) || buffer - base > size || + image->MaximumLength > size - (buffer - base)) + { + status = STATUS_INVALID_BUFFER_SIZE; + } + } - if (*ProcessImageName == NULL) - { - status = STATUS_INSUFFICIENT_RESOURCES; - goto cleanUp; + if (NT_SUCCESS(status)) + { + *ProcessImageName = image; + } + else + { + G_Memory.Free(image); + } + } + } } - - /* Retrieve the process path from the handle to the process */ - if (!NT_SUCCESS(status = G_Common.ZwQueryInformationProcess( - hProcess, - ProcessImageFileName, - *ProcessImageName, - returnedLength, - &returnedLength - ))) - { - G_Memory.Free(*ProcessImageName); - } - -cleanUp: - - if (hProcess) + else if (NT_SUCCESS(status)) { - ZwClose(hProcess); + status = STATUS_INVALID_BUFFER_SIZE; } + ZwClose(process); return status; } #pragma code_seg() diff --git a/src/Domito.vcxproj b/src/Domito.vcxproj index 74f0803..173306a 100644 --- a/src/Domito.vcxproj +++ b/src/Domito.vcxproj @@ -121,6 +121,8 @@ + + @@ -130,6 +132,7 @@ + diff --git a/src/Domito.vcxproj.filters b/src/Domito.vcxproj.filters index 18bd842..bf45072 100644 --- a/src/Domito.vcxproj.filters +++ b/src/Domito.vcxproj.filters @@ -31,6 +31,12 @@ Source Files + + Source Files + + + Source Files + Source Files @@ -39,6 +45,9 @@ Header Files + + Header Files + Header Files\Public diff --git a/tests/README.md b/tests/README.md index 87c926e..29b3a13 100644 --- a/tests/README.md +++ b/tests/README.md @@ -80,3 +80,30 @@ The host tests do not establish CI trust policy, real asynchronous kernel I/O, real policy ownership under Driver Verifier, revocation/timestamp behavior, or all Windows-version/layout combinations. See the [implementation ledger](../docs/implementation-status.md) for the remaining work. + +## Consumer helper regression tests + +Run `python tests/run-consumer-helper-tests.py` in an x64 Visual Studio developer +shell. This fifth host runner compiles the actual certificate/common-consumer +implementations and the existing PID name function, with kernel operations +replaced by deterministic doubles. Hashing uses real Windows CNG; independent +Crypt32 decoding/re-encoding/hash calls verify SPKI representation and pins. +The synthetic v1/v3 certificates are framing fixtures, not trust fixtures. + +Coverage includes truncated certificate prefixes at inaccessible guard pages, +malformed DER lengths and optional read-only comparisons with real system-root +certificate SPKIs, +four hash algorithms, short capacities and four injected CNG failures, strict +hex conversion across all byte values, path/handle ownership, failed references, +partial publisher allocation, process-name query failures, stale-output cleanup, +and malformed/overflowing x64 chain spans. A second host build exercises the +non-x64 rejection branch without claiming ARM64 ABI or runtime emulation. +No service is started, certificate trusted, or real CI policy evaluated. + +## Deprecation compiler checks + +Run `python tests/run-deprecation-tests.py --arch x64` or `--arch ARM64` from a +Visual Studio developer shell with the WDK installed. This compile-only runner +uses the actual public headers: modern API references must compile under /WX, +while each of four deprecated references must fail specifically with C4996 and +its migration message. Both C and C++ are checked. It does not link or load code. diff --git a/tests/ci-file-object-probe-tests.cpp b/tests/ci-file-object-probe-tests.cpp index 05ecfaf..d7924b4 100644 --- a/tests/ci-file-object-probe-tests.cpp +++ b/tests/ci-file-object-probe-tests.cpp @@ -10,34 +10,76 @@ #define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0) #define DOMITO_CI_PROBE_HOST_TEST typedef void* PFILE_OBJECT; -struct MINCRYPT_POLICY_INFO { - ULONG Size, VerificationStatus, PolicyBits; + +struct MINCRYPT_POLICY_INFO +{ + ULONG Size; + ULONG VerificationStatus; + ULONG PolicyBits; void* ChainInfo; }; -static unsigned calls, frees, outstanding, checks; + +static unsigned calls; +static unsigned frees; +static unsigned outstanding; +static unsigned checks; static bool failureMode; -static int fileToken, chainToken; -static void Check(bool value, const char* label) +static int fileToken; +static int chainToken; + +static void +Check( + bool value, + const char* label +) { ++checks; - if (!value) { std::fprintf(stderr, "FAIL: %s\n", label); std::exit(1); } + + if (!value) + { + std::fprintf(stderr, "FAIL: %s\n", label); + std::exit(1); + } } -static NTSTATUS CiValidateFileObject(PFILE_OBJECT file, ULONG flags, ULONG level, - MINCRYPT_POLICY_INFO* policy, MINCRYPT_POLICY_INFO* timestamp, - LARGE_INTEGER* time, UCHAR* hash, UINT32* size, ALG_ID* algorithm) + +static NTSTATUS +CiValidateFileObject( + PFILE_OBJECT file, + ULONG flags, + ULONG level, + MINCRYPT_POLICY_INFO* policy, + MINCRYPT_POLICY_INFO* timestamp, + LARGE_INTEGER* time, + UCHAR* hash, + UINT32* size, + ALG_ID* algorithm +) { const ULONG expectedFlags[] = {0, 0x20, 0x20, 0, 0, 0x20}; Check(calls < 6 && flags == expectedFlags[calls], "paired flags and order"); Check(file == &fileToken && level == 0, "same file and level zero"); Check(outstanding == 0, "previous outputs freed before next call"); Check(policy->Size == sizeof(*policy) && timestamp->Size == sizeof(*timestamp), "policy sizes"); - Check(!policy->ChainInfo && !timestamp->ChainInfo && !policy->PolicyBits && - !timestamp->PolicyBits, "fresh policy objects"); + Check( + !policy->ChainInfo && !timestamp->ChainInfo && !policy->PolicyBits && !timestamp->PolicyBits, + "fresh policy objects" + ); Check(*size == (calls < 4 ? 64u : 63u), "capacity reset for every call"); Check(*algorithm == 0 && time->QuadPart == 0, "fresh scalar outputs"); - for (unsigned i = 0; i < 64; ++i) Check(hash[i] == 0, "fresh hash buffer"); + + for (unsigned i = 0; i < 64; ++i) + { + Check(hash[i] == 0, "fresh hash buffer"); + } + ++calls; - if (*size < 64) { *size = 64; return STATUS_BUFFER_TOO_SMALL; } + + if (*size < 64) + { + *size = 64; + return STATUS_BUFFER_TOO_SMALL; + } + policy->ChainInfo = timestamp->ChainInfo = &chainToken; outstanding = 2; policy->PolicyBits = 0xA5; @@ -45,22 +87,38 @@ static NTSTATUS CiValidateFileObject(PFILE_OBJECT file, ULONG flags, ULONG level policy->VerificationStatus = failureMode ? STATUS_INVALID_IMAGE_HASH : STATUS_SUCCESS; timestamp->VerificationStatus = STATUS_SUCCESS; // Also allocate on simulated failure to exercise unconditional cleanup. - if (failureMode) return STATUS_INVALID_IMAGE_HASH; + if (failureMode) + { + return STATUS_INVALID_IMAGE_HASH; + } + std::memset(hash, 0xCC, 32); *size = 32; *algorithm = 0x800C; time->QuadPart = 123; return STATUS_SUCCESS; } -static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* policy) + +static void +CiFreePolicyInfo( + MINCRYPT_POLICY_INFO* policy +) { ++frees; - if (policy->ChainInfo) { Check(outstanding > 0, "owned chain"); --outstanding; } + + if (policy->ChainInfo) + { + Check(outstanding > 0, "owned chain"); + --outstanding; + } + *policy = {}; } + #include "ci-file-object-probe.cpp" -int main() +int +main() { for (unsigned pass = 0; pass < 2; ++pass) { @@ -70,27 +128,48 @@ int main() std::memset(results, 0xFF, sizeof(results)); DomitoProbeCiValidateFileObject(&fileToken, results); Check(calls == 6 && frees == 12 && outstanding == 0, "all calls and frees"); + for (unsigned i = 0; i < 6; ++i) { const auto& r = results[i]; Check(r.HashGuardIntact, "buffer guard"); - const NTSTATUS expected = i >= 4 ? STATUS_BUFFER_TOO_SMALL : - failureMode ? STATUS_INVALID_IMAGE_HASH : STATUS_SUCCESS; + const NTSTATUS expected = i >= 4 ? STATUS_BUFFER_TOO_SMALL + : failureMode ? STATUS_INVALID_IMAGE_HASH + : STATUS_SUCCESS; Check(r.Status == expected, "status preserved"); - if (i < 4) { + + if (i < 4) + { Check(r.PolicyBits == 0xA5 && r.TimeStampPolicyBits == 0x5A, "policy capture"); Check(r.HasChain && r.HasTimeStampChain, "chain presence captured before free"); } - if (NT_SUCCESS(r.Status)) { - Check(r.HashSize == 32 && r.HashAlgId == 0x800C && r.SigningTime.QuadPart == 123, - "digest metadata and signing time"); + + if (NT_SUCCESS(r.Status)) + { + Check( + r.HashSize == 32 && r.HashAlgId == 0x800C && r.SigningTime.QuadPart == 123, + "digest metadata and signing time" + ); + for (unsigned j = 0; j < 64; ++j) + { Check(r.Hash[j] == (j < 32 ? 0xCC : 0), "digest copied and tail cleared"); - } else { - for (auto byte : r.Hash) Check(byte == 0, "no digest from failed call"); + } + } + else + { + for (auto byte : r.Hash) + { + Check(byte == 0, "no digest from failed call"); + } + } + + if (i >= 4) + { + Check(r.HashSize == 64, "required capacity captured"); } - if (i >= 4) Check(r.HashSize == 64, "required capacity captured"); } } + std::printf("PASS: %u probe checks (mock CI; no runtime trust validation)\n", checks); } diff --git a/tests/ci-file-object-probe.cpp b/tests/ci-file-object-probe.cpp index f9ea540..41e3b9d 100644 --- a/tests/ci-file-object-probe.cpp +++ b/tests/ci-file-object-probe.cpp @@ -6,33 +6,51 @@ #include "ci-file-object-probe.h" _IRQL_requires_max_(PASSIVE_LEVEL) -void DomitoProbeCiValidateFileObject( - PFILE_OBJECT FileObject, DOMITO_CI_PROBE_RESULT* Results) +void +DomitoProbeCiValidateFileObject( + PFILE_OBJECT FileObject, + DOMITO_CI_PROBE_RESULT* Results +) { const ULONG flags[DOMITO_CI_PROBE_RESULT_COUNT] = { - 0, DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE, - DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE, 0, - 0, DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE + 0, + DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE, + DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE, + 0, + 0, + DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE }; + for (ULONG i = 0; i < DOMITO_CI_PROBE_RESULT_COUNT; ++i) { MINCRYPT_POLICY_INFO policy = {}; MINCRYPT_POLICY_INFO timestamp = {}; policy.Size = sizeof(policy); timestamp.Size = sizeof(timestamp); - struct { + + struct + { UCHAR Bytes[MINCRYPT_MAX_HASH_LENGTH]; ULONG Guard; } hash = {}; + hash.Guard = 0xD01170AB; DOMITO_CI_PROBE_RESULT& result = Results[i]; result = {}; result.Flags = flags[i]; result.InputCapacity = i < 4 ? sizeof(hash.Bytes) : sizeof(hash.Bytes) - 1; result.HashSize = result.InputCapacity; - result.Status = CiValidateFileObject(FileObject, result.Flags, 0, - &policy, ×tamp, &result.SigningTime, hash.Bytes, - &result.HashSize, &result.HashAlgId); + result.Status = CiValidateFileObject( + FileObject, + result.Flags, + 0, + &policy, + ×tamp, + &result.SigningTime, + hash.Bytes, + &result.HashSize, + &result.HashAlgId + ); result.HashGuardIntact = hash.Guard == 0xD01170AB; // Keep failure outputs as raw observations; only successful calls have // a usable digest. Do not dereference the incompletely mapped chains. @@ -42,10 +60,12 @@ void DomitoProbeCiValidateFileObject( result.TimeStampVerificationStatus = (NTSTATUS)timestamp.VerificationStatus; result.HasChain = policy.ChainInfo != nullptr; result.HasTimeStampChain = timestamp.ChainInfo != nullptr; + if (NT_SUCCESS(result.Status) && result.HashSize <= sizeof(result.Hash)) { RtlCopyMemory(result.Hash, hash.Bytes, result.HashSize); } + CiFreePolicyInfo(&policy); CiFreePolicyInfo(×tamp); } diff --git a/tests/ci-file-object-probe.h b/tests/ci-file-object-probe.h index 2ec9434..04e4f87 100644 --- a/tests/ci-file-object-probe.h +++ b/tests/ci-file-object-probe.h @@ -24,6 +24,8 @@ typedef struct _DOMITO_CI_PROBE_RESULT // PASSIVE_LEVEL; referenced file object; file bytes must remain stable. // Writes observations, not an overall trust verdict. Frees both policies per call. _IRQL_requires_max_(PASSIVE_LEVEL) -void DomitoProbeCiValidateFileObject( +void +DomitoProbeCiValidateFileObject( _In_ PFILE_OBJECT FileObject, - _Out_writes_(DOMITO_CI_PROBE_RESULT_COUNT) DOMITO_CI_PROBE_RESULT* Results); + _Out_writes_(DOMITO_CI_PROBE_RESULT_COUNT) DOMITO_CI_PROBE_RESULT* Results +); diff --git a/tests/ci-validation-tests.cpp b/tests/ci-validation-tests.cpp index f0a0645..ff21972 100644 --- a/tests/ci-validation-tests.cpp +++ b/tests/ci-validation-tests.cpp @@ -15,81 +15,207 @@ #define PAGED_CODE() ((void)0) typedef void* PFILE_OBJECT; typedef void* LPWIN_CERTIFICATE; -struct MINCRYPT_POLICY_INFO { + +struct MINCRYPT_POLICY_INFO +{ ULONG Size; NTSTATUS VerificationStatus; ULONG PolicyBits; void* ChainInfo; }; + #include "ci-validation-api.inc" -static unsigned checks, live, validationCalls, embeddedCalls, digestCalls, digestFrees; -static NTSTATUS nativeStatus = STATUS_SUCCESS, embeddedStatus = STATUS_SUCCESS, digestStatus = STATUS_SUCCESS; -static UINT32 nativeAlgorithm = CALG_SHA_256, nativeSize = 32; +static unsigned checks; +static unsigned live; +static unsigned validationCalls; +static unsigned embeddedCalls; +static unsigned digestCalls; +static unsigned digestFrees; +static NTSTATUS nativeStatus = STATUS_SUCCESS; +static NTSTATUS embeddedStatus = STATUS_SUCCESS; +static NTSTATUS digestStatus = STATUS_SUCCESS; +static UINT32 nativeAlgorithm = CALG_SHA_256; +static UINT32 nativeSize = 32; static ULONG fakeDigestSize = 32; -static bool omitCertificate, omitDigest, partialDigestOnFailure; -static const UCHAR image[] = {1,2,3}; -static ULONG observedFlags, observedLevel, observedCapacity; +static bool omitCertificate; +static bool omitDigest; +static bool partialDigestOnFailure; +static const UCHAR image[] = {1, 2, 3}; +static ULONG observedFlags; +static ULONG observedLevel; +static ULONG observedCapacity; static PFILE_OBJECT observedObject; static ALG_ID observedEmbeddedAlgorithm; static ULONG observedSecuritySize; static void* observedCertificate; -static void Check(bool value, const char* message) { +static void +Check( + bool value, + const char* message +) +{ ++checks; - if (!value) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } + + if (!value) + { + std::fprintf(stderr, "FAIL: %s\n", message); + std::exit(1); + } } -static void* Allocate(SIZE_T size) { + +static void* +Allocate( + SIZE_T size +) +{ void* value = std::malloc(size); - Check(value != nullptr, "host allocation"); ++live; + Check(value != nullptr, "host allocation"); + ++live; return value; } -static void Release(void* value) { Check(value && live, "single allocation owner"); --live; std::free(value); } -static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* policy) { - if (policy->ChainInfo) Release(policy->ChainInfo); + +static void +Release( + void* value +) +{ + Check(value && live, "single allocation owner"); + --live; + std::free(value); +} + +static void +CiFreePolicyInfo( + MINCRYPT_POLICY_INFO* policy +) +{ + if (policy->ChainInfo) + { + Release(policy->ChainInfo); + } + *policy = {}; } -static void Populate(MINCRYPT_POLICY_INFO* policy, MINCRYPT_POLICY_INFO* timestamp) { + +static void +Populate( + MINCRYPT_POLICY_INFO* policy, + MINCRYPT_POLICY_INFO* timestamp +) +{ Check(!policy->ChainInfo && !timestamp->ChainInfo, "old result freed before reuse"); policy->Size = timestamp->Size = sizeof(*policy); - policy->ChainInfo = Allocate(1); timestamp->ChainInfo = Allocate(1); - policy->PolicyBits = 0x123; timestamp->PolicyBits = 0x456; + policy->ChainInfo = Allocate(1); + timestamp->ChainInfo = Allocate(1); + policy->PolicyBits = 0x123; + timestamp->PolicyBits = 0x456; policy->VerificationStatus = STATUS_INVALID_IMAGE_HASH; } -static NTSTATUS CiValidateFileObject(PFILE_OBJECT object, UINT32 flags, UINT32 level, - MINCRYPT_POLICY_INFO* policy, MINCRYPT_POLICY_INFO* timestamp, LARGE_INTEGER* time, - UINT8* hash, UINT32* size, ALG_ID* algorithm) { + +static NTSTATUS +CiValidateFileObject( + PFILE_OBJECT object, + UINT32 flags, + UINT32 level, + MINCRYPT_POLICY_INFO* policy, + MINCRYPT_POLICY_INFO* timestamp, + LARGE_INTEGER* time, + UINT8* hash, + UINT32* size, + ALG_ID* algorithm +) +{ ++validationCalls; - observedObject = object; observedFlags = flags; observedLevel = level; observedCapacity = *size; + observedObject = object; + observedFlags = flags; + observedLevel = level; + observedCapacity = *size; Check(*size == 64, "fresh native hash capacity"); Check(!time->QuadPart && !*algorithm, "native scalar outputs initialized"); - for (unsigned i=0;i<64;++i) Check(!hash[i], "native digest starts empty"); - Populate(policy, timestamp); time->QuadPart = 99; - std::memset(hash, 0x5a, 64); *size = nativeSize; *algorithm = nativeAlgorithm; + + for (unsigned i = 0; i < 64; ++i) + { + Check(!hash[i], "native digest starts empty"); + } + + Populate(policy, timestamp); + time->QuadPart = 99; + std::memset(hash, 0x5a, 64); + *size = nativeSize; + *algorithm = nativeAlgorithm; return nativeStatus; } -static NTSTATUS DomitoCalculatePortableExecutableDigestEx(const UCHAR* bytes, ULONG size, UINT32 algorithm, - PULONG digestSize, PVOID* digest, LPWIN_CERTIFICATE* certificate, PULONG securitySize) { + +static NTSTATUS +DomitoCalculatePortableExecutableDigestEx( + const UCHAR* bytes, + ULONG size, + UINT32 algorithm, + PULONG digestSize, + PVOID* digest, + LPWIN_CERTIFICATE* certificate, + PULONG securitySize +) +{ ++digestCalls; Check(bytes == image && size == sizeof(image), "same PE snapshot forwarded"); - Check(algorithm == CALG_SHA1 || algorithm == CALG_SHA_256 || algorithm == CALG_SHA_384 || algorithm == CALG_SHA_512, - "supported digest choice forwarded"); - if (NT_SUCCESS(digestStatus) || partialDigestOnFailure) { + Check( + algorithm == CALG_SHA1 || algorithm == CALG_SHA_256 || algorithm == CALG_SHA_384 || algorithm == CALG_SHA_512, + "supported digest choice forwarded" + ); + + if (NT_SUCCESS(digestStatus) || partialDigestOnFailure) + { *digest = omitDigest ? nullptr : Allocate(64); - if (*digest) std::memset(*digest, 0xa5, 64); + + if (*digest) + { + std::memset(*digest, 0xa5, 64); + } + *digestSize = fakeDigestSize; *certificate = omitCertificate ? nullptr : const_cast(image); *securitySize = omitCertificate ? 0 : 123; } + return digestStatus; } -static void DomitoFreePortableExecutableDigest(PVOID digest) { ++digestFrees; Release(digest); } -static NTSTATUS CiCheckSignedFile(PVOID hash, UINT32 size, ALG_ID algorithm, LPWIN_CERTIFICATE certificate, - ULONG securitySize, MINCRYPT_POLICY_INFO* policy, LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp) { + +static void +DomitoFreePortableExecutableDigest( + PVOID digest +) +{ + ++digestFrees; + Release(digest); +} + +static NTSTATUS +CiCheckSignedFile( + PVOID hash, + UINT32 size, + ALG_ID algorithm, + LPWIN_CERTIFICATE certificate, + ULONG securitySize, + MINCRYPT_POLICY_INFO* policy, + LARGE_INTEGER* time, + MINCRYPT_POLICY_INFO* timestamp +) +{ ++embeddedCalls; - observedEmbeddedAlgorithm = algorithm; observedSecuritySize = securitySize; observedCertificate = certificate; - for (UINT32 i=0;i(hash)[i] == 0xa5, "computed digest forwarded unchanged"); - Populate(policy, timestamp); time->QuadPart = 123; + observedEmbeddedAlgorithm = algorithm; + observedSecuritySize = securitySize; + observedCertificate = certificate; + + for (UINT32 i = 0; i < size; ++i) + { + Check(static_cast(hash)[i] == 0xa5, "computed digest forwarded unchanged"); + } + + Populate(policy, timestamp); + time->QuadPart = 123; return embeddedStatus; } @@ -99,113 +225,291 @@ static NTSTATUS CiCheckSignedFile(PVOID hash, UINT32 size, ALG_ID algorithm, LPW #endif #include "ci-validation-under-test.inc" -static void CheckEmptyDigest(const DOMITO_CI_VALIDATION_RESULT& result) { +static void +CheckEmptyDigest( + const DOMITO_CI_VALIDATION_RESULT& result +) +{ Check(!result.HashSize && !result.HashAlgId && !result.SigningTime.QuadPart, "failure digest/time cleared"); - for (auto value : result.Hash) Check(!value, "failure hash bytes cleared"); + + for (auto value : result.Hash) + { + Check(!value, "failure hash bytes cleared"); + } } -static void Clear(DOMITO_CI_VALIDATION_RESULT* result) { + +static void +Clear( + DOMITO_CI_VALIDATION_RESULT* result +) +{ DomitoFreeCiValidationResult(result); DOMITO_CI_VALIDATION_RESULT empty = {}; Check(std::memcmp(result, &empty, sizeof(empty)) == 0, "full result cleared by destructor"); Check(live == 0, "no leaked policy/digest allocation"); } -int main() { +int +main() +{ DOMITO_CI_VALIDATION_RESULT result = {}; PFILE_OBJECT object = reinterpret_cast(static_cast(0x1234)); - DomitoFreeCiValidationResult(nullptr); Clear(&result); Clear(&result); - Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,nullptr)==STATUS_INVALID_PARAMETER, "null native result"); - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,nullptr)==STATUS_INVALID_PARAMETER, "null embedded result"); + DomitoFreeCiValidationResult(nullptr); + Clear(&result); + Clear(&result); + Check( + DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, nullptr) == STATUS_INVALID_PARAMETER, + "null native result" + ); + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, nullptr) == + STATUS_INVALID_PARAMETER, + "null embedded result" + ); Check(!validationCalls && !embeddedCalls && !digestCalls, "null result made no validation calls"); - for (UINT32 flags : {0UL,0x20UL,0xa0UL,0x80000024UL}) { - for (UINT8 level : {UINT8(0),UINT8(7),UINT8(255)}) { - result.HashSize=1; result.HashAlgId=0xdead; - Check(DomitoValidateFileObjectEx(object,flags,level,DomitoCiSignatureSelectionNative,&result)==STATUS_SUCCESS,"native status"); - Check(observedObject==object && observedFlags==flags && observedLevel==level && observedCapacity==64,"native inputs unchanged"); - Check(result.FileObjectValidationFlags==flags && result.RequestedSigningLevel==level,"export input controls recorded"); - Check(result.HashSize==32 && result.HashAlgId==CALG_SHA_256 && result.Hash[0]==0x5a && result.SigningTime.QuadPart==99,"native result"); + for (UINT32 flags : {0UL, 0x20UL, 0xa0UL, 0x80000024UL}) + { + for (UINT8 level : {UINT8(0), UINT8(7), UINT8(255)}) + { + result.HashSize = 1; + result.HashAlgId = 0xdead; + Check( + DomitoValidateFileObjectEx(object, flags, level, DomitoCiSignatureSelectionNative, &result) == + STATUS_SUCCESS, + "native status" + ); + Check( + observedObject == object && observedFlags == flags && observedLevel == level && observedCapacity == 64, + "native inputs unchanged" + ); + Check( + result.FileObjectValidationFlags == flags && result.RequestedSigningLevel == level, + "export input controls recorded" + ); + Check( + result.HashSize == 32 && result.HashAlgId == CALG_SHA_256 && result.Hash[0] == 0x5a && + result.SigningTime.QuadPart == 99, + "native result" + ); // API return success does not silently reinterpret a policy status. - Check(result.PolicyInfo.VerificationStatus==STATUS_INVALID_IMAGE_HASH,"policy observation retained"); + Check(result.PolicyInfo.VerificationStatus == STATUS_INVALID_IMAGE_HASH, "policy observation retained"); Clear(&result); } } - const unsigned beforeExperimental=validationCalls; - NTSTATUS experimental=DomitoValidateFileObjectEx(object,0x20,9,DomitoCiSignatureSelectionFirstSuccessfulExperimental,&result); + + const unsigned beforeExperimental = validationCalls; + NTSTATUS experimental = + DomitoValidateFileObjectEx(object, 0x20, 9, DomitoCiSignatureSelectionFirstSuccessfulExperimental, &result); #ifdef DOMITO_TEST_OTHER_ARCH - Check(experimental==STATUS_NOT_SUPPORTED && validationCalls==beforeExperimental,"experimental mode excluded on non-x64"); + Check( + experimental == STATUS_NOT_SUPPORTED && validationCalls == beforeExperimental, + "experimental mode excluded on non-x64" + ); CheckEmptyDigest(result); #else - Check(experimental==STATUS_SUCCESS && validationCalls==beforeExperimental+1,"experimental native call"); - Check(observedFlags==0xa0 && observedLevel==9,"only experimental bit added"); + Check(experimental == STATUS_SUCCESS && validationCalls == beforeExperimental + 1, "experimental native call"); + Check(observedFlags == 0xa0 && observedLevel == 9, "only experimental bit added"); #endif Clear(&result); - const unsigned beforeInvalid=validationCalls; - Check(DomitoValidateFileObjectEx(nullptr,0x20,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_PARAMETER,"null file object"); - Check(DomitoValidateFileObjectEx(object,0x20,0,static_cast(-1),&result)==STATUS_INVALID_PARAMETER,"invalid selection"); - Check(validationCalls==beforeInvalid,"invalid requests never call validation"); Clear(&result); - for (NTSTATUS failure : {STATUS_INVALID_IMAGE_HASH,STATUS_BUFFER_TOO_SMALL,STATUS_INSUFFICIENT_RESOURCES}) { - nativeStatus=failure; - Check(DomitoValidateFileObjectEx(object,0x20,0,DomitoCiSignatureSelectionNative,&result)==failure,"native failure preserved"); - CheckEmptyDigest(result); Check(live==2 && result.PolicyInfo.PolicyBits==0x123,"failure policy diagnostics remain owned"); Clear(&result); - } - nativeStatus=STATUS_SUCCESS; - nativeSize=65; - Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_BUFFER_SIZE,"oversized returned hash rejected"); - CheckEmptyDigest(result); Clear(&result); - nativeSize=31; - Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_BUFFER_SIZE,"algorithm length mismatch rejected"); Clear(&result); - nativeSize=32; nativeAlgorithm=0xdead; - Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_NOT_SUPPORTED,"unknown returned algorithm rejected"); Clear(&result); - for (ALG_ID algorithm : {CALG_SHA1,CALG_SHA_256,CALG_SHA_384,CALG_SHA_512}) { - nativeAlgorithm=algorithm; nativeSize=DomitoCiDigestSize(algorithm); - Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_SUCCESS,"known native digest sizes"); - for (UINT32 i=result.HashSize;i(-1), &result) == + STATUS_INVALID_PARAMETER, + "invalid selection" + ); + Check(validationCalls == beforeInvalid, "invalid requests never call validation"); + Clear(&result); + + for (NTSTATUS failure : {STATUS_INVALID_IMAGE_HASH, STATUS_BUFFER_TOO_SMALL, STATUS_INSUFFICIENT_RESOURCES}) + { + nativeStatus = failure; + Check( + DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == failure, + "native failure preserved" + ); + CheckEmptyDigest(result); + Check(live == 2 && result.PolicyInfo.PolicyBits == 0x123, "failure policy diagnostics remain owned"); Clear(&result); - fakeDigestSize=DomitoCiDigestSize(algorithm); - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),algorithm,&result)==STATUS_SUCCESS,"embedded success"); - Check(observedEmbeddedAlgorithm==algorithm && observedSecuritySize==123 && observedCertificate==image,"embedded certificate/algorithm forwarded"); - Check(result.HashSize==fakeDigestSize && result.HashAlgId==algorithm && result.Hash[0]==0xa5 && result.SigningTime.QuadPart==123,"embedded result owns copied digest"); - Check(!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live==2,"embedded metadata and digest cleanup"); Clear(&result); } - nativeAlgorithm=CALG_SHA_256; nativeSize=32; fakeDigestSize=32; - nativeStatus=STATUS_MORE_ENTRIES; - Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_MORE_ENTRIES,"nonnegative native status preserved"); - Clear(&result); nativeStatus=STATUS_SUCCESS; - Check(DomitoValidatePortableExecutableEmbeddedSignature(nullptr,0,CALG_SHA_256,&result)==STATUS_INVALID_PARAMETER,"null PE"); - unsigned beforeDigest=digestCalls; - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),0xdead,&result)==STATUS_NOT_SUPPORTED,"unsupported algorithm"); - Check(digestCalls==beforeDigest,"unsupported algorithm does not hash"); - for (NTSTATUS failure : {STATUS_NOT_SUPPORTED,STATUS_INVALID_IMAGE_FORMAT,STATUS_INSUFFICIENT_RESOURCES}) { - digestStatus=failure; - unsigned beforeEmbedded=embeddedCalls; - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==failure,"digest failure propagation"); - Check(embeddedCalls==beforeEmbedded,"digest failure never calls CI"); CheckEmptyDigest(result); Clear(&result); + + nativeStatus = STATUS_SUCCESS; + nativeSize = 65; + Check( + DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == + STATUS_INVALID_BUFFER_SIZE, + "oversized returned hash rejected" + ); + CheckEmptyDigest(result); + Clear(&result); + nativeSize = 31; + Check( + DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == + STATUS_INVALID_BUFFER_SIZE, + "algorithm length mismatch rejected" + ); + Clear(&result); + nativeSize = 32; + nativeAlgorithm = 0xdead; + Check( + DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_NOT_SUPPORTED, + "unknown returned algorithm rejected" + ); + Clear(&result); + + for (ALG_ID algorithm : {CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512}) + { + nativeAlgorithm = algorithm; + nativeSize = DomitoCiDigestSize(algorithm); + Check( + DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS, + "known native digest sizes" + ); + + for (UINT32 i = result.HashSize; i < sizeof(result.Hash); ++i) + { + Check(!result.Hash[i], "unused native digest bytes cleared"); + } + + Clear(&result); + fakeDigestSize = DomitoCiDigestSize(algorithm); + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), algorithm, &result) == + STATUS_SUCCESS, + "embedded success" + ); + Check( + observedEmbeddedAlgorithm == algorithm && observedSecuritySize == 123 && observedCertificate == image, + "embedded certificate/algorithm forwarded" + ); + Check( + result.HashSize == fakeDigestSize && result.HashAlgId == algorithm && result.Hash[0] == 0xa5 && + result.SigningTime.QuadPart == 123, + "embedded result owns copied digest" + ); + Check( + !result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2, + "embedded metadata and digest cleanup" + ); + Clear(&result); } - partialDigestOnFailure=true; - unsigned beforeFree=digestFrees; - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INSUFFICIENT_RESOURCES,"partial digest failure"); - Check(digestFrees==beforeFree+1,"partial digest allocation freed"); Clear(&result); - partialDigestOnFailure=false; digestStatus=STATUS_SUCCESS; - omitCertificate=true; - unsigned beforeEmbedded=embeddedCalls; - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_IMAGE_HASH,"unsigned PE rejected"); - Check(embeddedCalls==beforeEmbedded,"unsigned PE does not call CI"); Clear(&result); omitCertificate=false; - omitDigest=true; - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_BUFFER_SIZE,"missing digest rejected"); Clear(&result); omitDigest=false; - fakeDigestSize=31; - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_BUFFER_SIZE,"digest helper length mismatch"); Clear(&result); fakeDigestSize=32; - embeddedStatus=STATUS_INVALID_IMAGE_HASH; - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_IMAGE_HASH,"CI embedded failure propagated without fallback"); - CheckEmptyDigest(result); Check(live==2 && result.TimeStampPolicyInfo.PolicyBits==0x456,"embedded failure diagnostics owned"); Clear(&result); embeddedStatus=STATUS_SUCCESS; - for (unsigned i=0;i<16;++i) { - Check(DomitoValidateFileObjectEx(object,0x20,0,DomitoCiSignatureSelectionNative,&result)==STATUS_SUCCESS,"repeated native"); - Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_SUCCESS,"reuse native result as embedded result"); - Check(!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live==2,"reuse resets metadata without leaking"); + + nativeAlgorithm = CALG_SHA_256; + nativeSize = 32; + fakeDigestSize = 32; + nativeStatus = STATUS_MORE_ENTRIES; + Check( + DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_MORE_ENTRIES, + "nonnegative native status preserved" + ); + Clear(&result); + nativeStatus = STATUS_SUCCESS; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(nullptr, 0, CALG_SHA_256, &result) == + STATUS_INVALID_PARAMETER, + "null PE" + ); + unsigned beforeDigest = digestCalls; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), 0xdead, &result) == + STATUS_NOT_SUPPORTED, + "unsupported algorithm" + ); + Check(digestCalls == beforeDigest, "unsupported algorithm does not hash"); + + for (NTSTATUS failure : {STATUS_NOT_SUPPORTED, STATUS_INVALID_IMAGE_FORMAT, STATUS_INSUFFICIENT_RESOURCES}) + { + digestStatus = failure; + unsigned beforeEmbedded = embeddedCalls; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == failure, + "digest failure propagation" + ); + Check(embeddedCalls == beforeEmbedded, "digest failure never calls CI"); + CheckEmptyDigest(result); + Clear(&result); } + + partialDigestOnFailure = true; + unsigned beforeFree = digestFrees; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == + STATUS_INSUFFICIENT_RESOURCES, + "partial digest failure" + ); + Check(digestFrees == beforeFree + 1, "partial digest allocation freed"); + Clear(&result); + partialDigestOnFailure = false; + digestStatus = STATUS_SUCCESS; + omitCertificate = true; + unsigned beforeEmbedded = embeddedCalls; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == + STATUS_INVALID_IMAGE_HASH, + "unsigned PE rejected" + ); + Check(embeddedCalls == beforeEmbedded, "unsigned PE does not call CI"); + Clear(&result); + omitCertificate = false; + omitDigest = true; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == + STATUS_INVALID_BUFFER_SIZE, + "missing digest rejected" + ); + Clear(&result); + omitDigest = false; + fakeDigestSize = 31; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == + STATUS_INVALID_BUFFER_SIZE, + "digest helper length mismatch" + ); + Clear(&result); + fakeDigestSize = 32; + embeddedStatus = STATUS_INVALID_IMAGE_HASH; + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == + STATUS_INVALID_IMAGE_HASH, + "CI embedded failure propagated without fallback" + ); + CheckEmptyDigest(result); + Check(live == 2 && result.TimeStampPolicyInfo.PolicyBits == 0x456, "embedded failure diagnostics owned"); + Clear(&result); + embeddedStatus = STATUS_SUCCESS; + + for (unsigned i = 0; i < 16; ++i) + { + Check( + DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS, + "repeated native" + ); + Check( + DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == + STATUS_SUCCESS, + "reuse native result as embedded result" + ); + Check( + !result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2, + "reuse resets metadata without leaking" + ); + } + // Invalid calls must release a previously valid result, too. - Check(DomitoValidateFileObjectEx(nullptr,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_PARAMETER,"invalid reuse cleans old result"); Clear(&result); - std::printf("PASS: %u CI validation checks (%s architecture branch), no live allocations\n",checks, + Check( + DomitoValidateFileObjectEx(nullptr, 0, 0, DomitoCiSignatureSelectionNative, &result) == + STATUS_INVALID_PARAMETER, + "invalid reuse cleans old result" + ); + Clear(&result); + std::printf( + "PASS: %u CI validation checks (%s architecture branch), no live allocations\n", + checks, #ifdef DOMITO_TEST_OTHER_ARCH "non-x64" #else diff --git a/tests/common-tests.cpp b/tests/common-tests.cpp index 25c2c64..ade7b29 100644 --- a/tests/common-tests.cpp +++ b/tests/common-tests.cpp @@ -14,139 +14,382 @@ #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; }; +#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f)) + +struct OBJECT_ATTRIBUTES +{ + ULONG Attributes; +}; + +struct ANSI_STRING +{ + USHORT Length; + USHORT MaximumLength; + PCHAR Buffer; +}; + typedef ANSI_STRING* PANSI_STRING; -struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; }; + +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) { + +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; + + 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) { + +static PVOID +Directory( + PVOID, + BOOLEAN mapped, + USHORT index, + PULONG bytes +) +{ Check(mapped && index == IMAGE_DIRECTORY_ENTRY_EXPORT, "mapped export lookup"); - *bytes = exportBytes; return exportPointer; + *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; + +struct +{ + PVOID (*RtlImageDirectoryEntryToData)( + PVOID, + BOOLEAN, + USHORT, + PULONG + ); +} G_Common = {Directory}; + +static int eventToken; +static int fileToken; +static unsigned created; +static unsigned referenced; +static unsigned closed; +static unsigned dereferenced; +static unsigned reads; +static unsigned waits; static int ioMode; static bool outstanding; static IO_STATUS_BLOCK* pendingIosb; static ULONG requested; static LONGLONG expectedOffset; -static NTSTATUS ZwCreateEvent(HANDLE* event, ACCESS_MASK access, OBJECT_ATTRIBUTES* attributes, int type, BOOLEAN signaled) { - Check(access == EVENT_ALL_ACCESS && attributes->Attributes == OBJ_KERNEL_HANDLE && type == NotificationEvent && !signaled, "private kernel event"); - if (ioMode == 4) return STATUS_INSUFFICIENT_RESOURCES; - ++created; *event = &eventToken; return STATUS_SUCCESS; -} -static NTSTATUS ObReferenceObjectByHandle(HANDLE event, ACCESS_MASK access, void* type, int mode, void** object, void*) { - Check(event == &eventToken && access == SYNCHRONIZE && type == *ExEventObjectType && mode == KernelMode, "reference own event"); - if (ioMode == 5) return STATUS_ACCESS_DENIED; - ++referenced; *object = &eventToken; return STATUS_SUCCESS; -} -static void ZwClose(HANDLE event) { - Check(event == &eventToken && !outstanding, "close only after completion"); ++closed; -} -static void ObDereferenceObject(void* object) { - Check(object == &eventToken && !outstanding, "dereference only after completion"); ++dereferenced; -} -static NTSTATUS ZwReadFile(HANDLE file, HANDLE event, void*, void*, IO_STATUS_BLOCK* iosb, void*, ULONG size, LARGE_INTEGER* offset, void*) { - ++reads; - Check(file == &fileToken && event == &eventToken && offset && offset->QuadPart == expectedOffset, "file and explicit offset"); - Check(iosb->Status == 0 && iosb->Information == 0, "initialized IOSB"); - requested = size; - if (ioMode == 6) return STATUS_ACCESS_DENIED; - if (ioMode == 1 || ioMode == 2) { outstanding = true; pendingIosb = iosb; return STATUS_PENDING; } - iosb->Status = STATUS_SUCCESS; iosb->Information = ioMode == 3 ? size - 1 : size; + +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 KeWaitForSingleObject(void* object, int reason, int mode, BOOLEAN alertable, LARGE_INTEGER* timeout) { + +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"); + Check( + object == &eventToken && reason == Executive && mode == KernelMode && !alertable && !timeout && outstanding, + "noninterruptible per-request wait" + ); outstanding = false; pendingIosb->Status = ioMode == 2 ? STATUS_END_OF_FILE : STATUS_SUCCESS; pendingIosb->Information = ioMode == 2 ? 0 : requested; return STATUS_SUCCESS; } + #include "common-under-test.inc" -static std::vector Image() { + +static std::vector +Image() +{ std::vector image(4096); - auto dos = (IMAGE_DOS_HEADER*)image.data(); dos->e_magic = IMAGE_DOS_SIGNATURE; dos->e_lfanew = 128; - auto nt = (IMAGE_NT_HEADERS64*)(image.data() + 128); nt->Signature = IMAGE_NT_SIGNATURE; - nt->OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; nt->OptionalHeader.SizeOfImage = (ULONG)image.size(); + auto 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}; + exports->NumberOfFunctions = 3; + exports->NumberOfNames = 2; + exports->AddressOfFunctions = 801; + exports->AddressOfNames = 901; + exports->AddressOfNameOrdinals = 1001; + const ULONG functions[] = {2000, 2100, 2200}; + const ULONG names[] = {1100, 1120}; const USHORT ordinals[] = {2, 0}; - std::memcpy(image.data()+801, functions, sizeof(functions)); std::memcpy(image.data()+901, names, sizeof(names)); - std::memcpy(image.data()+1001, ordinals, sizeof(ordinals)); - std::memcpy(image.data()+1100, "Beta", 5); std::memcpy(image.data()+1120, "Alpha", 6); + std::memcpy(image.data() + 801, functions, sizeof(functions)); + std::memcpy(image.data() + 901, names, sizeof(names)); + std::memcpy(image.data() + 1001, ordinals, sizeof(ordinals)); + std::memcpy(image.data() + 1100, "Beta", 5); + std::memcpy(image.data() + 1120, "Alpha", 6); return image; } -static NTSTATUS Lookup(std::vector& image, const char* name, PVOID* output) { + +static NTSTATUS +Lookup( + std::vector& image, + const char* name, + PVOID* output +) +{ ANSI_STRING string = {(USHORT)std::strlen(name), (USHORT)std::strlen(name), (PCHAR)name}; - exportPointer = image.data() + 512; exportBytes = 256; + 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"); + +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"); + + 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"); + + ioMode = 0; + expectedOffset = 0; + Check(DomitoReadFile(&fileToken, buffer, sizeof(buffer)) == STATUS_SUCCESS, "zero-offset wrapper"); + LARGE_INTEGER offset = {}; + offset.QuadPart = -1; + Check( + DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), &offset) == STATUS_INVALID_PARAMETER, + "negative offset rejected" + ); + Check( + DomitoReadFileAt(&fileToken, buffer, sizeof(buffer), nullptr) == STATUS_INVALID_PARAMETER, + "null offset rejected" + ); + offset.QuadPart = 0; + Check(DomitoReadFileAt(&fileToken, nullptr, 0, &offset) == STATUS_SUCCESS, "zero byte read"); + Check(DomitoReadFileAt(&fileToken, nullptr, 1, &offset) == STATUS_INVALID_PARAMETER, "null buffer rejected"); std::printf("PASS: %u common-helper checks (mock kernel I/O)\n", checks); } diff --git a/tests/consumer-helper-tests.cpp b/tests/consumer-helper-tests.cpp new file mode 100644 index 0000000..4cdbb06 --- /dev/null +++ b/tests/consumer-helper-tests.cpp @@ -0,0 +1,967 @@ +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0) +#define PASSIVE_LEVEL 0 +#define PAGED_CODE() ((void)0) +#undef NTDDI_VERSION +#define NTDDI_VERSION 0x0a000008 +#undef NTDDI_WIN10 +#define NTDDI_WIN10 0x0a000000 +#define DOMITO_POOL_TAG 0x446f6d69 +#define MAXUSHORT 0xffff +#define PagedPool 1 +#define KernelMode 0 +#define ProcessImageFileName 27 +#define OBJ_KERNEL_HANDLE 0x200 +#define OBJ_CASE_INSENSITIVE 0x40 +#define FILE_NON_DIRECTORY_FILE 0x40 +#define FILE_SYNCHRONOUS_IO_NONALERT 0x20 +typedef void* PEPROCESS; +typedef void* PFILE_OBJECT; +typedef const UCHAR* PCUCHAR; + +typedef struct _UNICODE_STRING +{ + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; +} UNICODE_STRING, *PUNICODE_STRING; + +typedef struct _ANSI_STRING +{ + USHORT Length; + USHORT MaximumLength; + PCHAR Buffer; +} ANSI_STRING, *PANSI_STRING; + +struct OBJECT_ATTRIBUTES +{ + ULONG Attributes; + PUNICODE_STRING ObjectName; +}; + +struct CLIENT_ID +{ + HANDLE UniqueProcess; + HANDLE UniqueThread; +}; + +struct IO_STATUS_BLOCK +{ + NTSTATUS Status; + ULONG_PTR Information; +}; + +#define InitializeObjectAttributes(a, n, f, r, s) ((a)->Attributes = (f), (a)->ObjectName = (n)) +#define RTL_CONSTANT_STRING(s) {sizeof(s) - sizeof((s)[0]), sizeof(s), const_cast(s)} +typedef PVOID(NTAPI* PMINCRYPT_ALLOCATE_ROUTINE)(SIZE_T); +typedef PVOID NTAPI MINCRYPT_ALLOCATE_ROUTINE(SIZE_T); +#include "consumer-api.inc" + +static unsigned checks; +static unsigned live; +static unsigned refs; +static unsigned derefs; +static unsigned closed; +static unsigned validated; +static unsigned opened; +static unsigned queried; +static int failure; +static int cryptoFailure; +static NTSTATUS moduleStatus = STATUS_SUCCESS; +static NTSTATUS exportStatus = STATUS_SUCCESS; +static NTSTATUS validateStatus = STATUS_SUCCESS; +static int fileToken; +static int processToken; +static void* fileTypeValue = &fileToken; +static void** IoFileObjectType = &fileTypeValue; + +static void +Check( + bool value, + const char* msg +) +{ + ++checks; + + if (!value) + { + std::fprintf(stderr, "FAIL: %s\n", msg); + std::exit(1); + } +} + +static void* +Allocate( + SIZE_T size +) +{ + if (failure == 1) + { + return nullptr; + } + + void* p = std::malloc(size); + Check(p != nullptr, "allocation"); + ++live; + std::memset(p, 0xa5, size); + return p; +} + +static void +Release( + void* p +) +{ + Check(p && live, "single owner release"); + --live; + std::free(p); +} + +static struct +{ + PVOID (*Allocate)(SIZE_T); + void (*Free)(PVOID); +} G_Memory = {Allocate, Release}; + +static PVOID +ExAllocatePoolWithTag( + int, + SIZE_T size, + ULONG +) +{ + return Allocate(size); +} + +static void +ExFreePoolWithTag( + void* p, + ULONG +) +{ + Release(p); +} + +static void +ExFreePool( + void* p +) +{ + Release(p); +} + +static NTSTATUS +FakePublisher( + MINCERT_BLOB*, + PMINCRYPT_ALLOCATE_ROUTINE allocator, + PUNICODE_STRING name +) +{ + name->Buffer = static_cast(allocator(12)); + + if (!name->Buffer) + { + return STATUS_INSUFFICIENT_RESOURCES; + } + + std::memcpy(name->Buffer, L"hello", 12); + name->Length = 10; + name->MaximumLength = 12; + + if (failure == 8) + { + return STATUS_UNSUCCESSFUL; + } + + if (failure == 9) + { + name->Length = 13; + } + + return STATUS_SUCCESS; +} + +static NTSTATUS +DomitoFindModuleBaseAddress( + PANSI_STRING name, + PVOID* module +) +{ + Check(name && name->Length, "module name"); + + if (NT_SUCCESS(moduleStatus)) + { + *module = &fileToken; + } + + return moduleStatus; +} + +static NTSTATUS +DomitoFindExportedFunctionAddress( + PVOID module, + PANSI_STRING name, + PVOID* routine +) +{ + Check(module == &fileToken && name->Length, "export resolution"); + + if (NT_SUCCESS(exportStatus)) + { + *routine = reinterpret_cast(FakePublisher); + } + + return exportStatus; +} + +void +DomitoFreeCiValidationResult( + DOMITO_CI_VALIDATION_RESULT* result +) +{ + if (result->PolicyInfo.ChainInfo) + { + Release(result->PolicyInfo.ChainInfo); + } + + *result = {}; +} + +NTSTATUS +DomitoValidateFileObjectEx( + PFILE_OBJECT object, + UINT32 flags, + UINT8 level, + DOMITO_CI_SIGNATURE_SELECTION selection, + DOMITO_CI_VALIDATION_RESULT* result +) +{ + ++validated; + Check( + object == &fileToken && flags == 0x20 && level == 7 && selection == DomitoCiSignatureSelectionNative, + "CI arguments unchanged" + ); + Check(!result->PolicyInfo.ChainInfo, "result reset before native call"); + result->PolicyInfo.ChainInfo = static_cast(Allocate(1)); + return validateStatus; +} + +static NTSTATUS +ObReferenceObjectByHandle( + HANDLE handle, + ACCESS_MASK access, + void* type, + int mode, + void** object, + void* +) +{ + Check( + handle == &fileToken && access == FILE_READ_DATA && type == *IoFileObjectType && mode == KernelMode, + "trusted handle reference" + ); + + if (failure == 2) + { + return STATUS_ACCESS_DENIED; + } + + ++refs; + *object = &fileToken; + return STATUS_SUCCESS; +} + +static void +ObDereferenceObject( + void* object +) +{ + Check(object == &fileToken, "file dereference"); + ++derefs; +} + +static NTSTATUS +ZwOpenFile( + HANDLE* handle, + ACCESS_MASK access, + OBJECT_ATTRIBUTES* a, + IO_STATUS_BLOCK*, + ULONG share, + ULONG options +) +{ + ++opened; + Check( + access == (FILE_READ_DATA | SYNCHRONIZE) && a->Attributes == (OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE) && + share == FILE_SHARE_READ && options == (FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT), + "read-only stable path open" + ); + + if (failure == 3) + { + return STATUS_OBJECT_NAME_NOT_FOUND; + } + + *handle = &fileToken; + return STATUS_SUCCESS; +} + +static void +ZwClose( + HANDLE handle +) +{ + Check(handle == &fileToken || handle == &processToken, "close owned handle"); + ++closed; +} + +static void +FillName( + PUNICODE_STRING name +) +{ + name->Length = 8; + name->MaximumLength = 10; + name->Buffer = reinterpret_cast(name + 1); + std::memcpy(name->Buffer, L"test", 10); +} + +static NTSTATUS +SeLocateProcessImageName( + PEPROCESS process, + PUNICODE_STRING* image +) +{ + Check(process == &processToken, "referenced process passed unchanged"); + + if (failure == 4) + { + return STATUS_NOT_FOUND; + } + + // OS allocations are independent of configurable Domito allocator failures. + *image = static_cast(std::malloc(sizeof(UNICODE_STRING) + 10)); + ++live; + FillName(*image); + + if (failure == 5) + { + (*image)->Length = 9; + } + + return STATUS_SUCCESS; +} + +static NTSTATUS +ZwOpenProcess( + HANDLE* process, + ACCESS_MASK access, + OBJECT_ATTRIBUTES* a, + CLIENT_ID* cid +) +{ + Check( + access == 0x1000 && a->Attributes == OBJ_KERNEL_HANDLE && cid->UniqueProcess == ULongToHandle(123), + "PID least privilege kernel open" + ); + + if (failure == 6) + { + return STATUS_ACCESS_DENIED; + } + + *process = &processToken; + return STATUS_SUCCESS; +} + +static NTSTATUS +QueryProcess( + HANDLE process, + int info, + PVOID buffer, + ULONG size, + PULONG returned +) +{ + ++queried; + Check(process == &processToken && info == ProcessImageFileName, "PID query bound to one handle"); + *returned = sizeof(UNICODE_STRING) + 10; + + if (!buffer) + { + return failure == 10 ? STATUS_SUCCESS : STATUS_INFO_LENGTH_MISMATCH; + } + + Check(size == *returned, "sized name allocation"); + FillName(static_cast(buffer)); + + if (failure == 7) + { + return STATUS_UNSUCCESSFUL; + } + + if (failure == 11) + { + static_cast(buffer)->Buffer = reinterpret_cast(1); + } + + return STATUS_SUCCESS; +} + +static struct +{ + NTSTATUS (*ZwQueryInformationProcess)( + HANDLE, + int, + PVOID, + ULONG, + PULONG + ); +} G_Common = {QueryProcess}; + +static NTSTATUS +OpenAlgorithm( + BCRYPT_ALG_HANDLE* a, + LPCWSTR n, + LPCWSTR p, + ULONG f +) +{ + return cryptoFailure == 1 ? STATUS_UNSUCCESSFUL : BCryptOpenAlgorithmProvider(a, n, p, f); +} + +static NTSTATUS +CreateHash( + BCRYPT_ALG_HANDLE a, + BCRYPT_HASH_HANDLE* h, + PUCHAR o, + ULONG os, + PUCHAR s, + ULONG ss, + ULONG f +) +{ + return cryptoFailure == 2 ? STATUS_UNSUCCESSFUL : BCryptCreateHash(a, h, o, os, s, ss, f); +} + +static NTSTATUS +HashData( + BCRYPT_HASH_HANDLE h, + PUCHAR p, + ULONG n, + ULONG f +) +{ + return cryptoFailure == 3 ? STATUS_UNSUCCESSFUL : BCryptHashData(h, p, n, f); +} + +static NTSTATUS +FinishHash( + BCRYPT_HASH_HANDLE h, + PUCHAR p, + ULONG n, + ULONG f +) +{ + if (cryptoFailure == 4) + { + std::memset(p, 0xff, n); + return STATUS_UNSUCCESSFUL; + } + + return BCryptFinishHash(h, p, n, f); +} + +#define BCryptOpenAlgorithmProvider OpenAlgorithm +#define BCryptCreateHash CreateHash +#define BCryptHashData HashData +#define BCryptFinishHash FinishHash +#ifdef DOMITO_TEST_OTHER_ARCH +#undef _M_AMD64 +#undef _AMD64_ +#endif +#include "consumer-under-test.inc" +#undef BCryptOpenAlgorithmProvider +#undef BCryptCreateHash +#undef BCryptHashData +#undef BCryptFinishHash + +using Bytes = std::vector; + +static Bytes +Der( + UCHAR tag, + const Bytes& contents +) +{ + Bytes b = {tag}; + + if (contents.size() < 128) + { + b.push_back(static_cast(contents.size())); + } + else + { + Check(contents.size() < 256, "fixture DER bound"); + b.push_back(0x81); + b.push_back(static_cast(contents.size())); + } + + b.insert(b.end(), contents.begin(), contents.end()); + return b; +} + +static Bytes +Join( + std::initializer_list values +) +{ + Bytes b; + + for (const auto& v : values) + { + b.insert(b.end(), v.begin(), v.end()); + } + + return b; +} + +static Bytes +Fixture( + bool version +) +{ + Bytes sig = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 11}), Der(5, {})})); + Bytes rsa = Der(0x30, Join({Der(6, {0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 1, 1, 1}), Der(5, {})})); + Bytes name = Der(0x30, Der(0x31, Der(0x30, Join({Der(6, {0x55, 4, 3}), Der(0x0c, {'t', 'e', 's', 't'})})))); + const std::string date = "260101000000Z"; + Bytes time = Der(0x17, Bytes(date.begin(), date.end())); + Bytes key = Der(0x30, Join({Der(2, {0, 0xab, 0xcd}), Der(2, {1, 0, 1})})); + key.insert(key.begin(), 0); + Bytes spki = Der(0x30, Join({rsa, Der(3, key)})); + Bytes tbs = Join( + {version ? Der(0xa0, Der(2, {2})) : Bytes{}, Der(2, {1}), sig, name, Der(0x30, Join({time, time})), name, spki} + ); + return Der(0x30, Join({Der(0x30, tbs), sig, Der(3, {0, 0xab, 0xcd})})); +} + +static void +TestCertificates() +{ + for (bool v : {false, true}) + { + auto cert = Fixture(v); + DOMITO_CERTIFICATE_SPKI spki = {}; + Check( + NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast(cert.size()), &spki)), + "v1/v3 extraction" + ); + Check( + spki.Content.Data > spki.Encoded.Data && spki.Content.Size < spki.Encoded.Size, + "content and complete TLV differ" + ); + auto context = CertCreateCertificateContext(X509_ASN_ENCODING, cert.data(), static_cast(cert.size())); + Check(context != nullptr, "Crypt32 independently decodes fixture certificate"); + BYTE* encoded = nullptr; + DWORD size = 0; + Check( + CryptEncodeObjectEx( + X509_ASN_ENCODING, + X509_PUBLIC_KEY_INFO, + &context->pCertInfo->SubjectPublicKeyInfo, + CRYPT_ENCODE_ALLOC_FLAG, + nullptr, + &encoded, + &size + ) != FALSE, + "Crypt32 independent SPKI encoding" + ); + Check( + size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size), + "extracted complete SPKI equals Crypt32" + ); + LocalFree(encoded); + CertFreeCertificateContext(context); + UCHAR full[64]; + UCHAR legacy[64]; + UCHAR whole[64]; + UCHAR oracle[64]; + ULONG a = 64; + ULONG b = 64; + ULONG c = 64; + DWORD d = 64; + Check( + NT_SUCCESS(DomitoHashCertificate( + cert.data(), static_cast(cert.size()), DomitoCertificateHashSpkiDer, CALG_SHA_256, full, &a + )), + "full SPKI hash" + ); + Check( + NT_SUCCESS(DomitoHashCertificate( + cert.data(), + static_cast(cert.size()), + DomitoCertificateHashSpkiContentLegacy, + CALG_SHA_256, + legacy, + &b + )), + "legacy content SPKI hash" + ); + Check(a == 32 && b == 32 && std::memcmp(full, legacy, 32) != 0, "explicit SPKI conventions differ"); + Check( + CryptHashCertificate2( + BCRYPT_SHA256_ALGORITHM, 0, nullptr, spki.Content.Data, spki.Content.Size, oracle, &d + ) != FALSE && + d == b && !std::memcmp(legacy, oracle, d), + "legacy hash matches exact content-only oracle" + ); + Check( + NT_SUCCESS(DomitoHashCertificate( + cert.data(), static_cast(cert.size()), DomitoCertificateHashDer, CALG_SHA_256, whole, &c + )), + "DER thumbprint" + ); + d = 64; + Check( + CryptHashCertificate2( + BCRYPT_SHA256_ALGORITHM, 0, nullptr, cert.data(), static_cast(cert.size()), oracle, &d + ) != FALSE && + d == c && !std::memcmp(whole, oracle, d), + "thumbprint matches Crypt32" + ); + + for (ULONG i = 0; i < cert.size(); ++i) + { + spki.Encoded = {cert.data(), 99}; + Check(!NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), i, &spki)), "every truncated certificate rejected"); + Check(!spki.Encoded.Data && !spki.Content.Data, "failed extraction clears borrowed views"); + } + + cert.push_back(0); + Check( + !NT_SUCCESS(DomitoGetCertificateSpki(cert.data(), static_cast(cert.size()), &spki)), + "trailing byte rejected" + ); + } + + for (auto malformed : + {Bytes{0x30, 0x80, 0, 0}, + Bytes{0x30, 0x81, 1, 0}, + Bytes{0x30, 0x85, 1, 2, 3, 4, 5}, + Bytes{0x30, 0x84, 0xff, 0xff, 0xff, 0xff}}) + { + DOMITO_CERTIFICATE_SPKI spki = {}; + Check( + !NT_SUCCESS(DomitoGetCertificateSpki(malformed.data(), static_cast(malformed.size()), &spki)), + "bad DER lengths rejected" + ); + } + + const UCHAR abc[] = {'a', 'b', 'c'}; + UCHAR hash[64]; + + for (ALG_ID alg : {CALG_SHA1, CALG_SHA_256, CALG_SHA_384, CALG_SHA_512}) + { + ULONG n = 0; + Check(DomitoHashBuffer(abc, 3, alg, nullptr, &n) == STATUS_BUFFER_TOO_SMALL, "hash sizing"); + Check(n == 20 || n == 32 || n == 48 || n == 64, "known output size"); + ULONG cap = n - 1; + Check(DomitoHashBuffer(abc, 3, alg, hash, &cap) == STATUS_BUFFER_TOO_SMALL && cap == n, "short hash capacity"); + cap = 64; + Check(NT_SUCCESS(DomitoHashBuffer(nullptr, 0, alg, hash, &cap)), "empty hash"); + } + + for (int fail = 1; fail <= 4; ++fail) + { + cryptoFailure = fail; + ULONG n = 64; + std::memset(hash, 0xff, 64); + Check(!NT_SUCCESS(DomitoHashBuffer(abc, 3, CALG_SHA_256, hash, &n)) && !n, "CNG failure propagated"); + + for (int i = 0; i < 32; ++i) + { + Check(!hash[i], "CNG failure digest erased"); + } + } + + cryptoFailure = 0; + + for (unsigned byte = 0; byte < 256; ++byte) + { + UCHAR in = static_cast(byte); + UCHAR out = 0; + WCHAR hex[3]; + Check(NT_SUCCESS(DomitoEncodeHex(&in, 1, hex, 3)), "hex encoding"); + UNICODE_STRING str = {4, 6, hex}; + Check(NT_SUCCESS(DomitoDecodeHex(&str, &out, 1)) && out == in, "all byte values hex roundtrip"); + } + + for (WCHAR ch : {L'g', L'/', L':', L' ', L'\0', L'\u1234'}) + { + WCHAR text[] = {L'0', ch}; + UNICODE_STRING str = {4, 4, text}; + UCHAR out = 9; + Check( + DomitoDecodeHex(&str, &out, 1) == STATUS_INVALID_PARAMETER && !out, + "invalid hex digit rejected and output zeroed" + ); + } + + WCHAR shortHex[2]; + Check(DomitoEncodeHex(abc, 3, shortHex, 2) == STATUS_BUFFER_TOO_SMALL && !shortHex[0], "short hex buffer cleared"); + + // Put every truncated prefix immediately before an inaccessible page. + SYSTEM_INFO system = {}; + GetSystemInfo(&system); + const SIZE_T page = system.dwPageSize; + auto guarded = static_cast(VirtualAlloc(nullptr, 2 * page, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + Check(guarded != nullptr, "guard-page allocation"); + DWORD old = 0; + Check(VirtualProtect(guarded + page, page, PAGE_NOACCESS, &old) != FALSE, "guard page installed"); + const auto fixture = Fixture(true); + + for (ULONG n = 0; n < fixture.size(); ++n) + { + UCHAR* input = guarded + page - n; + std::memcpy(input, fixture.data(), n); + DOMITO_CERTIFICATE_SPKI spki = {}; + Check(!NT_SUCCESS(DomitoGetCertificateSpki(input, n, &spki)), "truncated DER at guard page rejected"); + } + + VirtualFree(guarded, 0, MEM_RELEASE); + + // Read-only optional corpus: exercises real long-length RSA/ECC certificates. + HCERTSTORE store = CertOpenSystemStoreW(0, L"ROOT"); + + if (store) + { + PCCERT_CONTEXT context = nullptr; + unsigned count = 0; + while (count < 16 && (context = CertEnumCertificatesInStore(store, context)) != nullptr) + { + ++count; + DOMITO_CERTIFICATE_SPKI spki = {}; + Check( + NT_SUCCESS(DomitoGetCertificateSpki(context->pbCertEncoded, context->cbCertEncoded, &spki)), + "real certificate SPKI extraction" + ); + BYTE* encoded = nullptr; + DWORD size = 0; + Check( + CryptEncodeObjectEx( + X509_ASN_ENCODING, + X509_PUBLIC_KEY_INFO, + &context->pCertInfo->SubjectPublicKeyInfo, + CRYPT_ENCODE_ALLOC_FLAG, + nullptr, + &encoded, + &size + ) != FALSE, + "real Crypt32 SPKI encoding" + ); + Check( + size == spki.Encoded.Size && !std::memcmp(encoded, spki.Encoded.Data, size), + "real certificate SPKI equals Crypt32" + ); + LocalFree(encoded); + } + + if (context) + { + CertFreeCertificateContext(context); + } + + CertCloseStore(store, 0); + } +} + +static void +TestOwnership() +{ + DOMITO_CI_VALIDATION_RESULT result = {}; + UNICODE_STRING path = {8, 10, const_cast(L"\\foo")}; + + for (int fail : {0, 2, 3}) + { + failure = fail; + const auto r = refs, d = derefs, c = closed; + result.PolicyInfo.ChainInfo = static_cast(Allocate(1)); + NTSTATUS s = DomitoValidateFilePathEx(&path, 0x20, 7, DomitoCiSignatureSelectionNative, &result); + Check(NT_SUCCESS(s) == (fail == 0), "path/reference failure returned"); + Check( + refs - r == derefs - d && closed - c == (fail == 3 ? 0u : 1u), + "all references and owned path handles released" + ); + DomitoFreeCiValidationResult(&result); + Check(!live, "path no allocations leaked"); + } + + failure = 0; + validateStatus = STATUS_INVALID_IMAGE_HASH; + const auto c = closed; + Check( + DomitoValidateFileHandleEx(&fileToken, 0x20, 7, DomitoCiSignatureSelectionNative, &result) == validateStatus, + "CI failure retained" + ); + Check(closed == c && result.PolicyInfo.ChainInfo, "borrowed handle open and diagnostics retained"); + DomitoFreeCiValidationResult(&result); + validateStatus = STATUS_SUCCESS; + + for (int fail : {0, 1, 4, 5}) + { + failure = fail; + PUNICODE_STRING name = reinterpret_cast(1); + NTSTATUS s = DomitoGetProcessImageNameEx(&processToken, &name); + Check(NT_SUCCESS(s) == (fail == 0), "PEPROCESS lookup failure"); + + if (NT_SUCCESS(s)) + { + Check( + name->Buffer == reinterpret_cast(name + 1) && name->Length == 8, + "one contiguous process-name allocation" + ); + } + else + { + Check(!name, "failed process name null"); + } + + DomitoFreeProcessImageName(name); + Check(!live, "process-name allocations released"); + } + + for (int fail : {0, 1, 6, 7, 10, 11}) + { + failure = fail; + PUNICODE_STRING name = reinterpret_cast(1); + NTSTATUS s = DomitoGetProcessImageName(123, &name); + Check(NT_SUCCESS(s) == (fail == 0), "PID query failure"); + + if (!NT_SUCCESS(s)) + { + Check(!name, "PID failure never leaves freed pointer"); + } + + DomitoFreeProcessImageName(name); + Check(!live, "PID query cleanup"); + } + + failure = 0; + MINCERT_BLOB blob = {1, reinterpret_cast(&fileToken)}; + + for (int fail : {0, 1, 8, 9}) + { + failure = fail; + UNICODE_STRING name = {}; + NTSTATUS s = DomitoGetCertificatePublisherName(&blob, &name); + Check(NT_SUCCESS(s) == (fail == 0), "publisher error propagation"); + DomitoFreeCertificatePublisherName(&name); + DomitoFreeCertificatePublisherName(&name); + Check(!live && !name.Buffer, "publisher matching free including partial failure"); + } + + failure = 0; + ANSI_STRING module = RTL_CONSTANT_STRING("module"); + ANSI_STRING name = RTL_CONSTANT_STRING("export"); + PVOID address = reinterpret_cast(1); + moduleStatus = STATUS_NOT_FOUND; + Check( + DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_FOUND && !address, + "missing module nulls export" + ); + moduleStatus = STATUS_SUCCESS; + exportStatus = STATUS_NOT_SUPPORTED; + address = reinterpret_cast(1); + Check( + DomitoFindModuleExportedFunctionAddress(&module, &name, &address) == STATUS_NOT_SUPPORTED && !address, + "unsupported forwarder status preserved" + ); + exportStatus = STATUS_SUCCESS; +} + +static void +TestChains() +{ + alignas(8) UCHAR storage[512] = {}; + auto chain = reinterpret_cast(storage); + chain->Size = sizeof(storage); + chain->ChainElements = reinterpret_cast(storage + 56); + chain->NumberOfChainElement = 2; + + for (ULONG i = 0; i < 2; ++i) + { + chain->ChainElements[i].Certificate = {1, storage + 400 + i}; + chain->ChainElements[i].HashSize = 32; + } + + MINCRYPT_POLICY_INFO policy = {}; + policy.Size = sizeof(policy); + policy.ChainInfo = chain; + DOMITO_CI_CHAIN_VIEW view = {}; +#ifdef DOMITO_TEST_OTHER_ARCH + Check( + DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view) == STATUS_NOT_SUPPORTED && !view.Count, + "non-x64 rejects unverified layout" + ); +#else + Check( + NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && view.Count == 2, + "minimal historical 56-byte chain prefix" + ); + // The last certificate is an arbitrary byte, not a proven self-signed root. + chain->ChainElements[1].Certificate.Data = storage + 512; + Check( + !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)) && !view.Count, + "child span past allocation rejected" + ); + chain->ChainElements[1].Certificate.Data = storage + 401; + chain->ChainElements[0].Subject = {reinterpret_cast(storage + 500), 13}; + Check( + !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)), + "malformed name span rejected" + ); + chain->ChainElements[0].Subject = {}; + chain->NumberOfChainElement = 0xffffffff; + Check( + !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)), + "element multiplication overflow rejected" + ); + chain->NumberOfChainElement = 2; + chain->ChainElements[0].HashSize = 65; + Check( + !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)), + "oversize CI element hash rejected" + ); + chain->ChainElements[0].HashSize = 32; + + for (ULONG n = 0; n < 56; ++n) + { + chain->Size = n; + Check( + !NT_SUCCESS(DomitoGetCiChainView(&policy, DomitoCiChainLayoutObservedX64, &view)), + "short historical header rejected" + ); + } +#endif +} + +int +main() +{ + TestCertificates(); + TestOwnership(); + TestChains(); + Check(!live, "all allocations released"); + std::printf("PASS: %u consumer-helper checks, no live allocations\n", checks); +} diff --git a/tests/digest-tests.cpp b/tests/digest-tests.cpp index 227db68..b425464 100644 --- a/tests/digest-tests.cpp +++ b/tests/digest-tests.cpp @@ -26,8 +26,12 @@ #define DOMITO_CI_SOURCE_CATALOG 2UL typedef const UCHAR* PCUCHAR; -static unsigned checks, allocations, ciCalls, hashCalls; -static int failOperation, failHashCall; +static unsigned checks; +static unsigned allocations; +static unsigned ciCalls; +static unsigned hashCalls; +static int failOperation; +static int failHashCall; static ULONG_PTR bytesRead; static bool allocatePolicies; static NTSTATUS embeddedResult = STATUS_SUCCESS; @@ -36,85 +40,232 @@ 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; - if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } + + if (!ok) + { + std::fprintf(stderr, "FAIL: %s\n", message); + std::exit(1); + } } -static PVOID ExAllocatePoolWithTag(int, SIZE_T size, ULONG) + +static PVOID +ExAllocatePoolWithTag( + int, + SIZE_T size, + ULONG +) { - if (failOperation == 4) return nullptr; + if (failOperation == 4) + { + return nullptr; + } + void* p = HeapAlloc(GetProcessHeap(), 0, size); - if (p) ++allocations; + + if (p) + { + ++allocations; + } + return p; } -static void ExFreePoolWithTag(PVOID p, ULONG) + +static void +ExFreePoolWithTag( + PVOID p, + ULONG +) { Check(p != nullptr && allocations != 0, "allocation ownership"); --allocations; Check(HeapFree(GetProcessHeap(), 0, p) != FALSE, "heap free"); } -static NTSTATUS TestCreateHash(BCRYPT_ALG_HANDLE a, BCRYPT_HASH_HANDLE* h, - PUCHAR o, ULONG os, PUCHAR s, ULONG ss, ULONG f) + +static NTSTATUS +TestCreateHash( + BCRYPT_ALG_HANDLE a, + BCRYPT_HASH_HANDLE* h, + PUCHAR o, + ULONG os, + PUCHAR s, + ULONG ss, + ULONG f +) { - if (failOperation == 1) return STATUS_UNSUCCESSFUL; + if (failOperation == 1) + { + return STATUS_UNSUCCESSFUL; + } + return BCryptCreateHash(a, h, o, os, s, ss, f); } -static NTSTATUS TestHashData(BCRYPT_HASH_HANDLE h, PUCHAR b, ULONG n, ULONG f) + +static NTSTATUS +TestHashData( + BCRYPT_HASH_HANDLE h, + PUCHAR b, + ULONG n, + ULONG f +) { ++hashCalls; - if (failOperation == 2 && hashCalls == (unsigned)failHashCall) return STATUS_UNSUCCESSFUL; + + if (failOperation == 2 && hashCalls == (unsigned)failHashCall) + { + return STATUS_UNSUCCESSFUL; + } + return BCryptHashData(h, b, n, f); } -static NTSTATUS TestFinishHash(BCRYPT_HASH_HANDLE h, PUCHAR b, ULONG n, ULONG f) + +static NTSTATUS +TestFinishHash( + BCRYPT_HASH_HANDLE h, + PUCHAR b, + ULONG n, + ULONG f +) { - if (failOperation == 3) return STATUS_UNSUCCESSFUL; + if (failOperation == 3) + { + return STATUS_UNSUCCESSFUL; + } + return BCryptFinishHash(h, b, n, f); } // Minimal dependencies for the legacy wrapper; no actual CI code is invoked. -struct MINCRYPT_POLICY_INFO { ULONG Size; PVOID ChainInfo; }; -struct KAPC_STATE { ULONG unused; }; -struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; }; +struct MINCRYPT_POLICY_INFO +{ + ULONG Size; + PVOID ChainInfo; +}; + +struct KAPC_STATE +{ + ULONG unused; +}; + +struct IO_STATUS_BLOCK +{ + NTSTATUS Status; + ULONG_PTR Information; +}; + static PVOID PsInitialSystemProcess = nullptr; -static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* p) { - if (p->ChainInfo) ExFreePoolWithTag(p->ChainInfo, DOMITO_POOL_TAG); + +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) + +static void +PopulatePolicies( + MINCRYPT_POLICY_INFO* policy, + MINCRYPT_POLICY_INFO* timestamp +) { Check(!policy->ChainInfo && !timestamp->ChainInfo, "previous CI attempt released before reuse"); - if (allocatePolicies) { + + if (allocatePolicies) + { policy->Size = timestamp->Size = sizeof(*policy); policy->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG); timestamp->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG); } } -static void KeStackAttachProcess(PVOID, KAPC_STATE*) {} -static void KeUnstackDetachProcess(KAPC_STATE*) {} + +static void +KeStackAttachProcess( + PVOID, + KAPC_STATE* +) +{ +} + +static void +KeUnstackDetachProcess( + KAPC_STATE* +) +{ +} + typedef PVOID PFILE_OBJECT; static PVOID fileObjectType; static PVOID* IoFileObjectType = &fileObjectType; #define KernelMode 0 -static NTSTATUS ObReferenceObjectByHandle(HANDLE, ULONG, PVOID, int, PVOID* object, PVOID) + +static NTSTATUS +ObReferenceObjectByHandle( + HANDLE, + ULONG, + PVOID, + int, + PVOID* object, + PVOID +) { *object = &fileObjectType; return STATUS_SUCCESS; } -static void ObDereferenceObject(PVOID) {} -static NTSTATUS FsRtlGetFileSize(PFILE_OBJECT, LARGE_INTEGER* size) + +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) + +static NTSTATUS +DomitoReadFileAt( + HANDLE, + PVOID buffer, + ULONG size, + const LARGE_INTEGER* offset +) { Check(offset->QuadPart == 1024, "explicit certificate read offset"); std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size)); return bytesRead == size ? STATUS_SUCCESS : STATUS_END_OF_FILE; } -static NTSTATUS CiCheckSignedFile(PVOID, UINT32, ALG_ID, PVOID, UINT32, - MINCRYPT_POLICY_INFO* policy, LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp) + +static NTSTATUS +CiCheckSignedFile( + PVOID, + UINT32, + ALG_ID, + PVOID, + UINT32, + MINCRYPT_POLICY_INFO* policy, + LARGE_INTEGER* time, + MINCRYPT_POLICY_INFO* timestamp +) { ++ciCalls; Check(time->QuadPart == 0, "fresh signing time for embedded validation"); @@ -122,9 +273,20 @@ static NTSTATUS CiCheckSignedFile(PVOID, UINT32, ALG_ID, PVOID, UINT32, time->QuadPart = 123; return embeddedResult; } -static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN reload, - UINT32 trusted, UINT32 mask, MINCRYPT_POLICY_INFO* policy, PVOID, - LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp) + +static NTSTATUS +CiVerifyHashInCatalog( + PVOID, + UINT32, + ALG_ID, + BOOLEAN reload, + UINT32 trusted, + UINT32 mask, + MINCRYPT_POLICY_INFO* policy, + PVOID, + LARGE_INTEGER* time, + MINCRYPT_POLICY_INFO* timestamp +) { ++ciCalls; Check(mask == expectedCatalogMask && trusted == expectedTrustedMarker, "explicit catalog policy forwarded"); @@ -144,11 +306,17 @@ static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN reload, #undef BCryptHashData #undef BCryptFinishHash -static std::vector Image(bool pe64, ULONG nt = 128, bool certificate = false) +static std::vector +Image( + bool pe64, + ULONG nt = 128, + bool certificate = false +) { std::vector b((size_t)nt + 896 + (certificate ? 32 : 0), 0); IMAGE_DOS_HEADER dos = {}; - dos.e_magic = IMAGE_DOS_SIGNATURE; dos.e_lfanew = (LONG)nt; + dos.e_magic = IMAGE_DOS_SIGNATURE; + dos.e_lfanew = (LONG)nt; std::memcpy(b.data(), &dos, sizeof(dos)); ULONG signature = IMAGE_NT_SIGNATURE; std::memcpy(b.data() + nt, &signature, sizeof(signature)); @@ -159,56 +327,90 @@ static std::vector Image(bool pe64, ULONG nt = 128, bool certificate = fa std::memcpy(b.data() + nt + 4, &f, sizeof(f)); IMAGE_OPTIONAL_HEADER64 h64 = {}; IMAGE_OPTIONAL_HEADER32 h32 = {}; - h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC; + h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; + h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC; h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16; h64.SectionAlignment = h32.SectionAlignment = 4096; h64.FileAlignment = h32.FileAlignment = 512; h64.SizeOfImage = h32.SizeOfImage = 8192; h64.Subsystem = h32.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI; h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384; + if (certificate) { - h64.DataDirectory[4] = h32.DataDirectory[4] = { nt + 896, 32 }; + h64.DataDirectory[4] = h32.DataDirectory[4] = {nt + 896, 32}; ULONG length = 32; std::memcpy(b.data() + nt + 896, &length, sizeof(length)); - USHORT revision = 0x0200, type = WIN_CERT_TYPE_PKCS_SIGNED_DATA; + USHORT revision = 0x0200; + USHORT type = WIN_CERT_TYPE_PKCS_SIGNED_DATA; std::memcpy(b.data() + nt + 900, &revision, 2); std::memcpy(b.data() + nt + 902, &type, 2); } + std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader); IMAGE_SECTION_HEADER section = {}; section.VirtualAddress = 4096; section.Misc.VirtualSize = 512; section.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ; - section.PointerToRawData = nt + 384; section.SizeOfRawData = 512; + section.PointerToRawData = nt + 384; + section.SizeOfRawData = 512; std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, §ion, sizeof(section)); - for (ULONG i = nt + 384; i < nt + 896; ++i) b[i] = (UCHAR)(i * 31); + + for (ULONG i = nt + 384; i < nt + 896; ++i) + { + b[i] = (UCHAR)(i * 31); + } + return b; } -static void Set32(std::vector& b, size_t at, ULONG value) + +static void +Set32( + std::vector& b, + size_t at, + ULONG value +) { std::memcpy(b.data() + at, &value, sizeof(value)); } -static void Set16(std::vector& b, size_t at, USHORT value) + +static void +Set16( + std::vector& b, + size_t at, + USHORT value +) { std::memcpy(b.data() + at, &value, sizeof(value)); } -static std::vector Digest(PUCHAR b, ULONG size, NTSTATUS expected = STATUS_SUCCESS) + +static std::vector +Digest( + PUCHAR b, + ULONG size, + NTSTATUS expected = STATUS_SUCCESS +) { UINT32 algorithm = 0xdead; - ULONG length = 0xdead, certSize = 0xdead; + ULONG length = 0xdead; + ULONG certSize = 0xdead; PVOID digest = (void*)1; LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1; hashCalls = 0; - NTSTATUS s = DomitoCalculatePortableExecutableDigest(b, size, &algorithm, &length, - &digest, &cert, &certSize); + NTSTATUS s = DomitoCalculatePortableExecutableDigest(b, size, &algorithm, &length, &digest, &cert, &certSize); Check(s == expected, "digest status"); std::vector result; + if (NT_SUCCESS(s)) { Check(algorithm == CALG_SHA1 && length == 20 && digest != nullptr, "SHA1 output"); Check((cert == nullptr) == (certSize == 0), "certificate outputs agree"); - if (cert) Check((PUCHAR)cert >= b && (PUCHAR)cert + certSize <= b + size, "borrowed certificate span"); + + if (cert) + { + Check((PUCHAR)cert >= b && (PUCHAR)cert + certSize <= b + size, "borrowed certificate span"); + } + result.assign((PUCHAR)digest, (PUCHAR)digest + length); DomitoFreePortableExecutableDigest(digest); } @@ -216,26 +418,49 @@ static std::vector Digest(PUCHAR b, ULONG size, NTSTATUS expected = STATU { Check(!algorithm && !length && !digest && !cert && !certSize, "failure outputs cleared"); } + Check(allocations == 0, "no leaked pool allocations"); return result; } -static std::vector Reference(const std::vector& b, ULONG checksum, - ULONG securitySlot, ULONG end) + +static std::vector +Reference( + const std::vector& b, + ULONG checksum, + ULONG securitySlot, + ULONG end +) { // Explicit fixture ranges, independent of the production parser. std::vector bytes(b.begin(), b.begin() + checksum); bytes.insert(bytes.end(), b.begin() + checksum + 4, b.begin() + securitySlot); - if (securitySlot != end) bytes.insert(bytes.end(), b.begin() + securitySlot + 8, b.begin() + end); + + if (securitySlot != end) + { + bytes.insert(bytes.end(), b.begin() + securitySlot + 8, b.begin() + end); + } + std::vector digest(20); BCRYPT_ALG_HANDLE algorithm = nullptr; - Check(NT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA1_ALGORITHM, nullptr, 0)), "reference algorithm"); - Check(NT_SUCCESS(BCryptHash(algorithm, nullptr, 0, bytes.data(), (ULONG)bytes.size(), digest.data(), 20)), "reference digest"); + Check( + NT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA1_ALGORITHM, nullptr, 0)), "reference algorithm" + ); + Check( + NT_SUCCESS(BCryptHash(algorithm, nullptr, 0, bytes.data(), (ULONG)bytes.size(), digest.data(), 20)), + "reference digest" + ); BCryptCloseAlgorithmProvider(algorithm, 0); return digest; } -static void GuardedInvalid(const std::vector& b, NTSTATUS expected) + +static void +GuardedInvalid( + const std::vector& b, + NTSTATUS expected +) { - SYSTEM_INFO si; GetSystemInfo(&si); + SYSTEM_INFO si; + GetSystemInfo(&si); const SIZE_T span = ((b.size() + si.dwPageSize - 1) / si.dwPageSize) * si.dwPageSize; UCHAR* region = (UCHAR*)VirtualAlloc(nullptr, span + si.dwPageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); Check(region != nullptr, "guard allocation"); @@ -244,43 +469,76 @@ static void GuardedInvalid(const std::vector& b, NTSTATUS expected) UCHAR* input = region + span - b.size(); std::memcpy(input, b.data(), b.size()); Digest(input, (ULONG)b.size(), expected); - ULONG strictSize = 0, securitySize = 0; + ULONG strictSize = 0; + ULONG securitySize = 0; PVOID strictDigest = nullptr; LPWIN_CERTIFICATE certificate = nullptr; - Check(!NT_SUCCESS(DomitoCalculatePortableExecutableDigestEx(input, (ULONG)b.size(), CALG_SHA256, - &strictSize, &strictDigest, &certificate, &securitySize)), "strict guard-page malformed input rejected"); + Check( + !NT_SUCCESS(DomitoCalculatePortableExecutableDigestEx( + input, (ULONG)b.size(), CALG_SHA256, &strictSize, &strictDigest, &certificate, &securitySize + )), + "strict guard-page malformed input rejected" + ); Check(!strictSize && !strictDigest && !certificate && !securitySize, "strict malformed outputs cleared"); VirtualFree(region, 0, MEM_RELEASE); } -static std::vector ExplicitDigest(const std::vector& b, UINT32 calg, - NTSTATUS expected = STATUS_SUCCESS) + +static std::vector +ExplicitDigest( + const std::vector& b, + UINT32 calg, + NTSTATUS expected = STATUS_SUCCESS +) { - ULONG length = 0xdead, certSize = 0xdead; + ULONG length = 0xdead; + ULONG certSize = 0xdead; PVOID digest = (PVOID)1; LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1; - NTSTATUS status = DomitoCalculatePortableExecutableDigestEx(b.data(), (ULONG)b.size(), - calg, &length, &digest, &cert, &certSize); + NTSTATUS status = + DomitoCalculatePortableExecutableDigestEx(b.data(), (ULONG)b.size(), calg, &length, &digest, &cert, &certSize); Check(status == expected, "explicit digest status"); std::vector result; + if (NT_SUCCESS(status)) { - Check(length == (calg == CALG_SHA1 ? 20u : calg == CALG_SHA256 ? 32u : - calg == CALG_SHA384 ? 48u : 64u), "explicit algorithm digest length"); + 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"); + else + { + Check(!length && !digest && !cert && !certSize, "explicit failure outputs cleared"); + } + Check(allocations == 0, "explicit digest allocation cleanup"); return result; } -static std::vector SipDigest(const std::vector& b, UINT32 calg) +static std::vector +SipDigest( + const std::vector& b, + UINT32 calg +) { - WCHAR tempPath[MAX_PATH], tempName[MAX_PATH]; + WCHAR tempPath[MAX_PATH]; + WCHAR tempName[MAX_PATH]; Check(GetTempPathW(MAX_PATH, tempPath) != 0, "temp path"); Check(GetTempFileNameW(tempPath, L"dci", 0, tempName) != 0, "temp name"); - HANDLE file = CreateFileW(tempName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, - nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr); + 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"); @@ -292,8 +550,12 @@ static std::vector SipDigest(const std::vector& b, UINT32 calg) info.hFile = file; info.pwsFileName = tempName; info.dwEncodingType = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; - info.DigestAlgorithm.pszObjId = const_cast(calg == CALG_SHA1 ? szOID_OIWSEC_sha1 : - calg == CALG_SHA256 ? szOID_NIST_sha256 : calg == CALG_SHA384 ? szOID_NIST_sha384 : szOID_NIST_sha512); + info.DigestAlgorithm.pszObjId = const_cast( + calg == CALG_SHA1 ? szOID_OIWSEC_sha1 + : calg == CALG_SHA256 ? szOID_NIST_sha256 + : calg == CALG_SHA384 ? szOID_NIST_sha384 + : szOID_NIST_sha512 + ); DWORD size = 0; Check(CryptSIPCreateIndirectData(&info, &size, nullptr) != FALSE, "SIP size query"); std::vector buffer(size); @@ -304,64 +566,134 @@ static std::vector SipDigest(const std::vector& b, UINT32 calg) return result; } -static std::vector Der(UCHAR tag, const std::vector& value) +static std::vector +Der( + UCHAR tag, + const std::vector& value +) { std::vector out = {tag}; - if (value.size() < 128) out.push_back((UCHAR)value.size()); - else if (value.size() < 256) { out.push_back(0x81); out.push_back((UCHAR)value.size()); } - else { out.push_back(0x82); out.push_back((UCHAR)(value.size() >> 8)); out.push_back((UCHAR)value.size()); } + + if (value.size() < 128) + { + out.push_back((UCHAR)value.size()); + } + else if (value.size() < 256) + { + out.push_back(0x81); + out.push_back((UCHAR)value.size()); + } + else + { + out.push_back(0x82); + out.push_back((UCHAR)(value.size() >> 8)); + out.push_back((UCHAR)value.size()); + } + out.insert(out.end(), value.begin(), value.end()); return out; } -static std::vector Join(std::initializer_list> values) + +static std::vector +Join( + std::initializer_list> values +) { std::vector out; - for (auto& value : values) out.insert(out.end(), value.begin(), value.end()); + + for (auto& value : values) + { + out.insert(out.end(), value.begin(), value.end()); + } + return out; } -static std::vector SignedContent(UINT32 calg, bool nullParameters = true) + +static std::vector +SignedContent( + UINT32 calg, + bool nullParameters = true +) { - std::vector oid = calg == CALG_SHA1 ? std::vector{0x2B,0x0E,0x03,0x02,0x1A} : - std::vector{0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02, - (UCHAR)(calg == CALG_SHA256 ? 1 : calg == CALG_SHA384 ? 2 : 3)}; + std::vector oid = calg == CALG_SHA1 ? std::vector{0x2B, 0x0E, 0x03, 0x02, 0x1A} + : std::vector{ + 0x60, + 0x86, + 0x48, + 0x01, + 0x65, + 0x03, + 0x04, + 0x02, + (UCHAR)(calg == CALG_SHA256 ? 1 + : calg == CALG_SHA384 ? 2 + : 3) + }; auto algorithm = Der(0x30, Join({Der(0x06, oid), nullParameters ? Der(0x05, {}) : std::vector{}})); - auto digest = Der(0x30, Join({algorithm, Der(0x04, std::vector( - calg == CALG_SHA1 ? 20 : calg == CALG_SHA256 ? 32 : calg == CALG_SHA384 ? 48 : 64, 0xAA))})); + auto digest = + Der(0x30, + Join( + {algorithm, + Der(0x04, + std::vector( + calg == CALG_SHA1 ? 20 + : calg == CALG_SHA256 ? 32 + : calg == CALG_SHA384 ? 48 + : 64, + 0xAA + ))} + )); // A complete SHA-1 DigestInfo decoy in the ignored data attribute must not // override the selected SpcIndirectDataContent digest AlgorithmIdentifier. - auto data = Der(0x30, Der(0x04, {0x30,0x21,0x30,0x09,0x06,0x05,0x2B,0x0E,0x03,0x02,0x1A,0x05,0,0x04,0x14})); + auto 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 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)})); + return Der(0x30, Join({Der(0x06, {0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02}), Der(0xA0, signedData)})); } -static void ExtendedTests() + +static void +ExtendedTests() { for (bool pe64 : {false, true}) - for (bool cert : {false, true}) - for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512}) { - auto b = Image(pe64, 128, cert); - Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP differential fixture"); - if (!cert) + for (bool cert : {false, true}) { - b.insert(b.end(), 48, 0x42); // Non-section overlay. - Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP overlay differential"); + for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512}) + { + auto b = Image(pe64, 128, cert); + Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP differential fixture"); + + if (!cert) + { + b.insert(b.end(), 48, 0x42); // Non-section overlay. + Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP overlay differential"); + } + } } } + for (bool pe64 : {false, true}) - for (ULONG count : {0UL, 4UL}) { - auto absent = Image(pe64); - Set32(absent, pe64 ? 260 : 244, count); - ExplicitDigest(absent, CALG_SHA256, STATUS_NOT_SUPPORTED); + 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; + IMAGE_SECTION_HEADER first = {}; + IMAGE_SECTION_HEADER second = {}; + first.PointerToRawData = 768; + first.SizeOfRawData = 256; + second.PointerToRawData = 512; + second.SizeOfRawData = 256; std::memcpy(b.data() + 392, &first, sizeof(first)); std::memcpy(b.data() + 432, &second, sizeof(second)); Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "section header order differential"); @@ -370,7 +702,8 @@ static void ExtendedTests() 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); + 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); @@ -382,22 +715,26 @@ static void ExtendedTests() b = Image(true, 128, true); b.resize(1088); Set32(b, 300, 64); - Set32(b, 1056, 32); Set16(b, 1060, 0x0200); Set16(b, 1062, 2); + 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); + ExplicitDigest(Image(true), CALG_SHA256, failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL); } + failOperation = 0; + for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512}) { auto content = SignedContent(calg); @@ -405,37 +742,68 @@ static void ExtendedTests() 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, 296, 1024); + Set32(signedImage, 300, tableSize); Set32(signedImage, 1024, certLength); - Set16(signedImage, 1028, 0x0200); Set16(signedImage, 1030, 2); + 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"); + 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"); + 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"); + 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"); + 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"); + 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); @@ -443,105 +811,175 @@ static void ExtendedTests() 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); + 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"); + 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}; + 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"); + { + 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"); + Check( + DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual) == STATUS_NOT_FOUND && + actual == 0, + "absent DER does not silently select SHA1" + ); } -static void SystemImageTests() +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); + 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"); + Check( + GetFileSizeEx(file, &size) && size.QuadPart > 0 && size.QuadPart < 32 * 1024 * 1024, + "bounded system PE size" + ); std::vector b((size_t)size.QuadPart); DWORD read; Check(ReadFile(file, b.data(), (DWORD)b.size(), &read, nullptr) && read == b.size(), "read system PE fixture"); CloseHandle(file); + for (UINT32 calg : std::initializer_list{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512}) + { Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "real system PE Windows SIP differential"); + } + IMAGE_DOS_HEADER dos; std::memcpy(&dos, b.data(), sizeof(dos)); USHORT magic; const ULONG optional = (ULONG)dos.e_lfanew + 24; std::memcpy(&magic, b.data() + optional, 2); - const ULONG directory = magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC ? - FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory) : FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory); + 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"); + 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"); + 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"); + Check( + CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, nullptr, &contentSize) != FALSE, + "Windows SPC content size" + ); std::vector content(contentSize); - Check(CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, content.data(), &contentSize) != FALSE, - "Windows SPC content"); + 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"); + 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; + 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"); + 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 certificate : {false, true}) { auto b = Image(pe64, 128, certificate); auto result = Digest(b.data(), (ULONG)b.size()); - Check(result == Reference(b, 216, pe64 ? 296 : 280, 1024), "fixture digest equals independent CNG reference"); + Check( + result == Reference(b, 216, pe64 ? 296 : 280, 1024), "fixture digest equals independent CNG reference" + ); Set32(b, 216, 0x12345678); Check(result == Digest(b.data(), (ULONG)b.size()), "checksum excluded"); + if (certificate) { b.back() ^= 0x55; @@ -549,53 +987,105 @@ int main() } } } + auto large = Image(true, 4 * 1024 * 1024); - Check(Digest(large.data(), (ULONG)large.size()) == Reference(large, 4194392, 4194472, (ULONG)large.size()), "large DOS stub"); + Check( + Digest(large.data(), (ULONG)large.size()) == Reference(large, 4194392, 4194472, (ULONG)large.size()), + "large DOS stub" + ); auto b = Image(true); + for (ULONG size = 0; size < 392; ++size) { std::vector prefix(b.begin(), b.begin() + size); - if (size == 0) Digest(b.data(), 0, STATUS_INVALID_IMAGE_NOT_MZ); - else GuardedInvalid(prefix, size < 64 ? STATUS_INVALID_IMAGE_NOT_MZ : STATUS_INVALID_IMAGE_FORMAT); + + if (size == 0) + { + Digest(b.data(), 0, STATUS_INVALID_IMAGE_NOT_MZ); + } + else + { + GuardedInvalid(prefix, size < 64 ? STATUS_INVALID_IMAGE_NOT_MZ : STATUS_INVALID_IMAGE_FORMAT); + } } - auto bad = b; Set32(bad, 60, 0xffffffff); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = b; Set32(bad, 60, 0x7fffffff); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = b; Set16(bad, 148, 1); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = b; Set16(bad, 152, 0x999); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = b; Set32(bad, 260, 17); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + + auto bad = b; + Set32(bad, 60, 0xffffffff); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 60, 0x7fffffff); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set16(bad, 148, 1); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set16(bad, 152, 0x999); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 260, 17); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + for (ULONG offset : {200UL, 2048UL, 0xffffffffUL}) { - bad = b; Set32(bad, 296, offset); Set32(bad, 300, 32); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 296, offset); + Set32(bad, 300, 32); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); } - bad = b; Set32(bad, 300, 32); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = b; Set32(bad, 296, 800); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = b; Set32(bad, 296, 800); Set32(bad, 300, 0xffffffff); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = Image(true, 128, true); Set32(bad, 1024, 33); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = Image(true, 128, true); Set32(bad, 1024, 7); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); - bad = Image(true); Set32(bad, 260, 0); // No directory entry: don't exclude its physical bytes. + + bad = b; + Set32(bad, 300, 32); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 296, 800); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = b; + Set32(bad, 296, 800); + Set32(bad, 300, 0xffffffff); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = Image(true, 128, true); + Set32(bad, 1024, 33); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = Image(true, 128, true); + Set32(bad, 1024, 7); + GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT); + bad = Image(true); + Set32(bad, 260, 0); // No directory entry: don't exclude its physical bytes. Check(Digest(bad.data(), (ULONG)bad.size()) == Reference(bad, 216, 1024, 1024), "absent security directory"); + for (int failure : {1, 2, 3, 4}) { failOperation = failure; + for (int call = 1; call <= (failure == 2 ? 3 : 1); ++call) { failHashCall = call; Digest(b.data(), (ULONG)b.size(), failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL); } } + failOperation = 0; Digest(nullptr, 1024, STATUS_INVALID_PARAMETER); IMAGE_DATA_DIRECTORY directory = {1024, 32}; - MINCRYPT_POLICY_INFO policy = {}, timestamp = {}; + MINCRYPT_POLICY_INFO policy = {}; + MINCRYPT_POLICY_INFO timestamp = {}; LARGE_INTEGER signing = {}; UCHAR hash[20] = {}; - bytesRead = 16; ciCalls = 0; - Check(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, - &policy, &signing, ×tamp) == STATUS_END_OF_FILE, "short certificate read rejected"); + bytesRead = 16; + ciCalls = 0; + Check( + DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp) == + STATUS_END_OF_FILE, + "short certificate read rejected" + ); Check(ciCalls == 0 && allocations == 0, "short read does not reach CI or leak"); bytesRead = 32; - Check(NT_SUCCESS(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, - &policy, &signing, ×tamp)), "complete certificate read accepted"); + Check( + NT_SUCCESS( + DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory, &policy, &signing, ×tamp) + ), + "complete certificate read accepted" + ); Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once"); // Failure paths must release previous attempts before embedded/catalog/reload reuse. allocatePolicies = true; @@ -606,34 +1096,75 @@ int main() 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( + 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"); + 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( + 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( + 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); + CiFreePolicyInfo(&policy); + CiFreePolicyInfo(×tamp); Check(allocations == 0, "failed final policies freed once"); std::printf("PASS: %u digest/legacy checks (Windows SIP, Crypt32, guard pages, failures, ownership).\n", checks); } diff --git a/tests/mincrypt-layout-tests.cpp b/tests/mincrypt-layout-tests.cpp index 39c6903..2c674da 100644 --- a/tests/mincrypt-layout-tests.cpp +++ b/tests/mincrypt-layout-tests.cpp @@ -4,19 +4,49 @@ #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( + 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); +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); +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. diff --git a/tests/run-ci-probe-tests.py b/tests/run-ci-probe-tests.py index 77dbbb9..00f0861 100644 --- a/tests/run-ci-probe-tests.py +++ b/tests/run-ci-probe-tests.py @@ -1,20 +1,34 @@ """Test the actual kernel probe body with mock CI; run in an x64 VS dev shell.""" + from pathlib import Path import re import subprocess import tempfile tests = Path(__file__).resolve().parent -header = (tests.parent / 'include/Domito.MinCrypt.h').read_text(encoding='utf-8-sig') -names = ['MINCRYPT_MAX_HASH_LENGTH', 'DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE'] -constants = '\n'.join(re.search(r'^#define\s+' + name + r'\s+[^\r\n]+', header, re.M)[0] - for name in names) -with tempfile.TemporaryDirectory(prefix='domito-ci-probe-') as temp: +header = (tests.parent / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig") +names = ["MINCRYPT_MAX_HASH_LENGTH", "DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE"] +constants = "\n".join( + re.search(r"^#define\s+" + name + r"\s+[^\r\n]+", header, re.M)[0] for name in names +) +with tempfile.TemporaryDirectory(prefix="domito-ci-probe-") as temp: build = Path(temp) - (build / 'ci-probe-constants.inc').write_text(constants + '\n', encoding='utf-8') - exe = build / 'ci-probe-tests.exe' - subprocess.run(['cl', '/nologo', '/EHsc', '/W4', '/WX', '/std:c++17', - '/I' + str(build), str(tests / 'ci-file-object-probe-tests.cpp'), - '/Fe' + str(exe), '/Fo' + str(build / 'ci-probe-tests.obj')], - cwd=build, check=True) + (build / "ci-probe-constants.inc").write_text(constants + "\n", encoding="utf-8") + exe = build / "ci-probe-tests.exe" + subprocess.run( + [ + "cl", + "/nologo", + "/EHsc", + "/W4", + "/WX", + "/std:c++17", + "/I" + str(build), + str(tests / "ci-file-object-probe-tests.cpp"), + "/Fe" + str(exe), + "/Fo" + str(build / "ci-probe-tests.obj"), + ], + cwd=build, + check=True, + ) subprocess.run([str(exe)], cwd=build, check=True) diff --git a/tests/run-ci-validation-tests.py b/tests/run-ci-validation-tests.py index 0c12530..f535eb7 100644 --- a/tests/run-ci-validation-tests.py +++ b/tests/run-ci-validation-tests.py @@ -1,27 +1,53 @@ """Compile the actual production CI wrapper body against deterministic CI doubles.""" + from pathlib import Path import subprocess import tempfile root = Path(__file__).resolve().parents[1] -header = (root / 'include/Domito.h').read_text(encoding='utf-8-sig') -start = header.index('#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards') -end = header.index('#define DOMITO_CI_SOURCE_EMBEDDED', start) -constants_header = (root / 'include/Domito.MinCrypt.h').read_text(encoding='utf-8-sig') -constants = '\n'.join(line for line in constants_header.splitlines() - if line.startswith(('#define DOMITO_CI_VALIDATE_FLAG_', '#define MINCRYPT_MAX_HASH_LENGTH', '#define MINCRYPT_SHA'))) -source = (root / 'src/Domito.CodeIntegrity.Validation.cpp').read_text(encoding='utf-8-sig') -source = source.replace('#include "Domito.Internal.h"', '') -with tempfile.TemporaryDirectory(prefix='domito-ci-validation-') as temporary: +header = (root / "include/Domito.h").read_text(encoding="utf-8-sig") +start = header.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards") +end = header.index("#define DOMITO_CI_SOURCE_EMBEDDED", start) +constants_header = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig") +constants = "\n".join( + line + for line in constants_header.splitlines() + if line.startswith( + ( + "#define DOMITO_CI_VALIDATE_FLAG_", + "#define MINCRYPT_MAX_HASH_LENGTH", + "#define MINCRYPT_SHA", + ) + ) +) +source = (root / "src/Domito.CodeIntegrity.Validation.cpp").read_text(encoding="utf-8-sig") +source = source.replace('#include "Domito.Internal.h"', "") +with tempfile.TemporaryDirectory(prefix="domito-ci-validation-") as temporary: directory = Path(temporary) - (directory / 'ci-validation-api.inc').write_text(constants + '\n' + header[start:end], encoding='utf-8') - (directory / 'ci-validation-under-test.inc').write_text(source, encoding='utf-8') - for branch in ['x64', 'other']: - program = directory / (branch + '.exe') - flags = ['/DDOMITO_TEST_OTHER_ARCH'] if branch == 'other' else [] - subprocess.run(['cl','/nologo','/EHsc','/W4','/WX','/std:c++17', - '/DNTDDI_VERSION=0x0A000008','/DNTDDI_WIN10=0x0A000000', - '/I' + str(directory), *flags, str(Path(__file__).with_name('ci-validation-tests.cpp')), - '/Fe' + str(program), '/Fo' + str(directory / (branch + '.obj'))], - cwd=directory, check=True) + (directory / "ci-validation-api.inc").write_text( + constants + "\n" + header[start:end], encoding="utf-8" + ) + (directory / "ci-validation-under-test.inc").write_text(source, encoding="utf-8") + for branch in ["x64", "other"]: + program = directory / (branch + ".exe") + flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else [] + subprocess.run( + [ + "cl", + "/nologo", + "/EHsc", + "/W4", + "/WX", + "/std:c++17", + "/DNTDDI_VERSION=0x0A000008", + "/DNTDDI_WIN10=0x0A000000", + "/I" + str(directory), + *flags, + str(Path(__file__).with_name("ci-validation-tests.cpp")), + "/Fe" + str(program), + "/Fo" + str(directory / (branch + ".obj")), + ], + cwd=directory, + check=True, + ) subprocess.run([str(program)], check=True) diff --git a/tests/run-common-tests.py b/tests/run-common-tests.py index 3867665..cce1bf7 100644 --- a/tests/run-common-tests.py +++ b/tests/run-common-tests.py @@ -1,18 +1,35 @@ """Run production export and file-read helpers with deterministic user-mode doubles.""" + from pathlib import Path import subprocess import tempfile + root = Path(__file__).resolve().parents[1] -source = (root / 'src/Domito.cpp').read_text(encoding='utf-8-sig') -export_start = source.index('// This helper handles directory RVAs') -export_end = source.index('_Success_', source.index('DomitoFindExportedFunctionAddress(') + 1) -read_start = source.rfind('_Success_', 0, source.index('DomitoReadFileAt(')) -read_end = source.index('_Success_', source.index('DomitoReadFile(') + 1) -with tempfile.TemporaryDirectory(prefix='domito-common-tests-') as temp: +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) + (build / "common-under-test.inc").write_text( + source[export_start:export_end] + "\n" + source[read_start:read_end], encoding="utf-8" + ) + exe = build / "common-tests.exe" + subprocess.run( + [ + "cl", + "/nologo", + "/EHsc", + "/W4", + "/WX", + "/std:c++17", + "/I" + str(build), + str(root / "tests/common-tests.cpp"), + "/Fe" + str(exe), + "/Fo" + str(build / "common-tests.obj"), + ], + cwd=build, + check=True, + ) subprocess.run([str(exe)], cwd=build, check=True) diff --git a/tests/run-consumer-helper-tests.py b/tests/run-consumer-helper-tests.py new file mode 100644 index 0000000..1c0952c --- /dev/null +++ b/tests/run-consumer-helper-tests.py @@ -0,0 +1,71 @@ +"""Production consumer helpers with real CNG/Crypt32 and deterministic kernel doubles.""" + +from pathlib import Path +import subprocess +import tempfile + +root = Path(__file__).resolve().parents[1] +public = (root / "include/Domito.h").read_text(encoding="utf-8-sig") +mincrypt = (root / "include/Domito.MinCrypt.h").read_text(encoding="utf-8-sig") +structures = mincrypt[ + mincrypt.index("#include ") : mincrypt.index("#include ") + + len("#include ") +] +validation = public[ + public.index("#if (NTDDI_VERSION >= NTDDI_WIN10)\n// Native forwards") : public.index( + "#define DOMITO_CI_SOURCE_EMBEDDED" + ) +] +helpers = ( + (root / "include/Domito.Consumer.h") + .read_text(encoding="utf-8-sig") + .replace('#include "Domito.h"', "") +) +algorithm = public[ + public.index("#if !defined(CALG_SHA1)") : public.index( + "/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM(") + ) +] +common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig") +process_start = common.rfind("_Success_", 0, common.index("DomitoGetProcessImageName(")) +process = ( + common[process_start:] + .replace('#pragma code_seg("PAGED")', "") + .replace("#pragma code_seg()", "") +) +with tempfile.TemporaryDirectory(prefix="domito-consumer-tests-") as temp: + build = Path(temp) + (build / "consumer-api.inc").write_text( + algorithm + "\n" + structures + "\n" + validation + "\n" + helpers + ) + bodies = "\n".join( + (root / ("src/" + name)) + .read_text(encoding="utf-8-sig") + .replace('#include "Domito.Internal.h"', "") + for name in ["Domito.Certificate.cpp", "Domito.Consumer.cpp"] + ) + (build / "consumer-under-test.inc").write_text(bodies + "\n" + process) + for branch in ["x64", "other"]: + exe = build / (branch + ".exe") + flags = ["/DDOMITO_TEST_OTHER_ARCH"] if branch == "other" else [] + subprocess.run( + [ + "cl", + "/nologo", + "/EHsc", + "/W4", + "/WX", + "/std:c++17", + "/I" + str(build), + *flags, + str(root / "tests/consumer-helper-tests.cpp"), + "/Fe" + str(exe), + "/Fo" + str(build / (branch + ".obj")), + "/link", + "bcrypt.lib", + "crypt32.lib", + ], + cwd=build, + check=True, + ) + subprocess.run([str(exe)], check=True) diff --git a/tests/run-deprecation-tests.py b/tests/run-deprecation-tests.py new file mode 100644 index 0000000..7e9e620 --- /dev/null +++ b/tests/run-deprecation-tests.py @@ -0,0 +1,94 @@ +"""Compile actual public headers and verify MSVC C4996 migration diagnostics.""" + +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--arch", choices=["x64", "ARM64"], default="x64") +args = parser.parse_args() +root = Path(__file__).resolve().parents[1] +vc = Path(os.environ["VCToolsInstallDir"]) +kit = Path(os.environ["WindowsSdkDir"]) / "Include" / os.environ["WindowsSDKVersion"].strip("\\/") +compiler = vc / "bin/Hostx64" / args.arch.lower() / "cl.exe" +architecture = ["/D_AMD64_", "/D_AMD64", "/Gz"] if args.arch == "x64" else ["/D_ARM64_", "/DARM64"] +common = [ + "/nologo", + "/c", + "/kernel", + "/W4", + "/WX", + "/we4996", + "/D_WIN64", + "/DWINNT=1", + "/DNTDDI_VERSION=0x0A000008", + "/D_WIN32_WINNT=0x0A00", + *architecture, + "/I" + str(vc / "include"), + "/I" + str(kit / "km"), + "/I" + str(kit / "shared"), + "/I" + str(kit / "ucrt"), + "/I" + str(root / "include"), +] +old = { + "DomitoGetPortableExecutableDigestKind": "DomitoGetPortableExecutableDigestKindEx", + "DomitoCalculatePortableExecutableDigest": "DomitoCalculatePortableExecutableDigestEx", + "DomitoValidateFileLegacyMode": "DomitoValidateFileLegacyModeEx", + "DomitoGetProcessImageName": "DomitoGetProcessImageNameEx", +} +modern = [ + *old.values(), + "DomitoFreePortableExecutableDigest", + "DomitoFreeProcessImageName", + "DomitoValidateFileObjectEx", + "DomitoValidateFileHandleEx", + "DomitoValidateFilePathEx", + "DomitoGetCiChainView", + "DomitoGetCertificatePublisherName", + "DomitoFindModuleBaseAddress", + "DomitoFindExportedFunctionAddress", + "DomitoReadFile", + "DomitoReadFileAt", + "CiValidateFileObject", +] +prefix = "#include \n#include \n#include \n#include \n#include \n" +checks = 0 +with tempfile.TemporaryDirectory(prefix="domito-deprecation-") as temp: + build = Path(temp) + for language in ["c", "cpp"]: + for index, (name, replacement) in enumerate([("modern", None), *old.items()]): + source = build / (f"check-{index}." + language) + refs = modern if replacement is None else [name] + source.write_text( + prefix + + "void Check(void) {\n" + + "".join(f" (void)&{ref};\n" for ref in refs) + + "}\n" + ) + result = subprocess.run( + [ + str(compiler), + *common, + "/TC" if language == "c" else "/TP", + "/Fo" + str(build / "check.obj"), + str(source), + ], + text=True, + capture_output=True, + ) + output = result.stdout + result.stderr + if replacement is None: + assert result.returncode == 0, output + else: + assert ( + result.returncode != 0 + and "C4996" in output + and name in output + and replacement in output + ), output + errors = [line for line in output.splitlines() if "error C" in line] + assert errors and all("C4996" in line for line in errors), output + checks += 1 +print(f"PASS: {checks} {args.arch} C/C++ public-header deprecation checks") diff --git a/tests/run-digest-tests.py b/tests/run-digest-tests.py index 6656d4f..b59e062 100644 --- a/tests/run-digest-tests.py +++ b/tests/run-digest-tests.py @@ -4,31 +4,54 @@ Extracts the actual production functions without changing their bodies, replacin only kernel dependencies with the test harness. No driver is loaded. This tests parsing, hashing and cleanup, not real CI policy or kernel I/O synchronization. """ + import argparse from pathlib import Path import subprocess import tempfile parser = argparse.ArgumentParser() -parser.add_argument('--source-root', type=Path, default=Path(__file__).resolve().parents[1]) -parser.add_argument('--digest-source', type=Path) +parser.add_argument("--source-root", type=Path, default=Path(__file__).resolve().parents[1]) +parser.add_argument("--digest-source", type=Path) args = parser.parse_args() root = args.source_root -digest = (args.digest_source or root / 'src/Domito.CodeIntegrity.cpp').read_text(encoding='utf-8-sig') -digest = digest.replace('#include "Domito.Internal.h"', '').replace('#include "Domito.MinCrypt.h"', '') -common = (root / 'src/Domito.cpp').read_text(encoding='utf-8-sig') -start = common.rfind('_Success_', 0, common.index('DomitoMemorySearchPattern(')) -end = common.index('_Success_', common.index('DomitoMemorySearchPattern(')) +digest = (args.digest_source or root / "src/Domito.CodeIntegrity.cpp").read_text( + encoding="utf-8-sig" +) +digest = digest.replace('#include "Domito.Internal.h"', "").replace( + '#include "Domito.MinCrypt.h"', "" +) +common = (root / "src/Domito.cpp").read_text(encoding="utf-8-sig") +start = common.rfind("_Success_", 0, common.index("DomitoMemorySearchPattern(")) +end = common.index("_Success_", common.index("DomitoMemorySearchPattern(")) search = common[start:end] -public = (root / 'include/Domito.h').read_text(encoding='utf-8-sig') -start = public.index('#if !defined(CALG_SHA1)') -end = public.index('/*', public.index('DOMITO_CALG_TO_BCRYPT_ALGORITHM(')) +public = (root / "include/Domito.h").read_text(encoding="utf-8-sig") +start = public.index("#if !defined(CALG_SHA1)") +end = public.index("/*", public.index("DOMITO_CALG_TO_BCRYPT_ALGORITHM(")) algorithm = public[start:end] -with tempfile.TemporaryDirectory(prefix='domito-digest-tests-') as temp: +with tempfile.TemporaryDirectory(prefix="domito-digest-tests-") as temp: build = Path(temp) - (build / 'digest-under-test.inc').write_text(algorithm + '\n' + search + '\n' + digest, encoding='utf-8') - test_source = Path(__file__).with_name('digest-tests.cpp') - subprocess.run(['cl', '/nologo', '/EHsc', '/W4', '/WX', '/std:c++17', - '/I' + str(build), str(test_source), '/Fe' + str(build/'digest-tests.exe'), - '/Fo' + str(build/'digest-tests.obj'), '/link', 'bcrypt.lib', 'crypt32.lib'], cwd=build, check=True) - subprocess.run([str(build/'digest-tests.exe')], cwd=build, check=True) + (build / "digest-under-test.inc").write_text( + algorithm + "\n" + search + "\n" + digest, encoding="utf-8" + ) + test_source = Path(__file__).with_name("digest-tests.cpp") + subprocess.run( + [ + "cl", + "/nologo", + "/EHsc", + "/W4", + "/WX", + "/std:c++17", + "/I" + str(build), + str(test_source), + "/Fe" + str(build / "digest-tests.exe"), + "/Fo" + str(build / "digest-tests.obj"), + "/link", + "bcrypt.lib", + "crypt32.lib", + ], + cwd=build, + check=True, + ) + subprocess.run([str(build / "digest-tests.exe")], cwd=build, check=True)