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.
This commit is contained in:
nefarius committed 2026-10-27 00:16:42 +01:00
1 parent 1f4d62ae95
commit a491bbd3f8
32 files changed
+5190 -922

No files matched your search

+3 -2
View File
@@ -4,14 +4,15 @@
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<_PropertySheetDisplayName>Domito Kernel Library</_PropertySheetDisplayName>
<DomitoRootPath Condition="'$(DomitoRootPath)' == ''">$(SolutionDir)..\Domito</DomitoRootPath>
<DomitoRootPath Condition="'$(DomitoRootPath)' == ''">$(MSBuildThisFileDirectory)</DomitoRootPath>
<DomitoLibraryPlatform Condition="'$(DomitoLibraryPlatform)' == ''">$(Platform)</DomitoLibraryPlatform>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(DomitoRootPath)\include;$(IntDir);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies Condition="'$(DriverType)'=='KMDF'">$(DomitoRootPath)\lib\$(DDKPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DDKPlatform)\ci.lib;$(DDK_LIB_PATH)cng.lib;%(AdditionalDependencies);</AdditionalDependencies>
<AdditionalDependencies Condition="'$(ConfigurationType)' != 'StaticLibrary' And ('$(DriverType)' == 'KMDF' Or '$(DriverType)' == 'WDM')">$(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\$(Configuration)\Domito.lib;$(DomitoRootPath)\lib\$(DomitoLibraryPlatform)\ci.lib;cng.lib;%(AdditionalDependencies);</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup />
+13 -1
View File
@@ -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
<!-- The rest of the project file -->
+7 -5
View File
@@ -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);
+247
View File
@@ -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.
+20 -1
View File
@@ -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.
+200
View File
@@ -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
+97 -42
View File
@@ -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. */
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 {
typedef struct _DOMITO_CI26100_CHAIN_INFO
{
ULONG Size; /* Entire allocation, including trailing data. */
DOMITO_CI26100_BLOB *PublicKeys;
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. */
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. */
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,23 +159,25 @@ 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.
+56 -9
View File
@@ -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
);
+57 -75
View File
@@ -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
@@ -205,39 +205,39 @@ typedef unsigned int ALG_ID;
#if (NTDDI_VERSION >= NTDDI_WINXP)
#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_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_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_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)
@@ -259,23 +259,17 @@ typedef unsigned int ALG_ID;
#endif //(NTDDI_VERSION >= NTDDI_WIN10_RS1)
#include <pshpack8.h>
typedef struct _MINCRYPTOAPI_BLOB
{
UINT32 Size;
_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.
@@ -284,13 +278,13 @@ typedef struct _MINCRYPT_NAME_BLOB
_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_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
{
@@ -303,7 +297,7 @@ typedef struct _MINCRYPT_CHAIN_ELEMENT
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.
@@ -346,7 +340,7 @@ typedef struct _MINCRYPT_CHAIN_INFO
// ChainElements[NumberOfChainElement]
}MINCRYPT_CHAIN_INFO, * PMINCRYPT_CHAIN_INFO;
} MINCRYPT_CHAIN_INFO, *PMINCRYPT_CHAIN_INFO;
typedef struct _MINCRYPT_POLICY_INFO
{
@@ -365,11 +359,10 @@ typedef struct _MINCRYPT_POLICY_INFO
#endif // NTDDI_VERSION >= NTDDI_WIN10
}MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO;
} MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO;
#include <poppack.h>
/**
* 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,8 +420,7 @@ CiFreePolicyInfo(
*/
_IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI
NTSTATUS
NTAPI
NTSTATUS NTAPI
CiCheckSignedFile(
_In_reads_bytes_(HashSize) PVOID Hash,
_In_ UINT32 HashSize,
@@ -442,7 +432,6 @@ CiCheckSignedFile(
_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,8 +482,7 @@ CiCheckSignedFile(
*/
_IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI
NTSTATUS
NTAPI
NTSTATUS NTAPI
CiVerifyHashInCatalog(
_In_reads_bytes_(HashSize) PVOID Hash,
_In_ UINT32 HashSize,
@@ -508,18 +496,15 @@ CiVerifyHashInCatalog(
_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,8 +594,7 @@ CiSetTrustedOriginClaimId(
*/
_IRQL_requires_max_(PASSIVE_LEVEL)
MINCRYPTAPI
NTSTATUS
NTAPI
NTSTATUS NTAPI
CiValidateFileObject(
_In_ FILE_OBJECT* FileObject,
_In_ UINT32 ValidationFlags,
+38 -33
View File
@@ -2,7 +2,6 @@
#include <Domito.MinCrypt.h>
/* ___
* / __|___ _ __ _ __ ___ _ _
* | (__/ _ \ ' \| ' \/ _ \ ' \
@@ -25,7 +24,6 @@ EXTERN_C
void
DomitoShutdown();
/* __ __ __ __ _
* | \/ |___ _ __ ___ _ _ _ _ | \/ |__ _ _ _ __ _ __ _ ___ _ __ ___ _ _| |_
* | |\/| / -_) ' \/ _ \ '_| || | | |\/| / _` | ' \/ _` / _` / -_) ' \/ -_) ' \ _|
@@ -36,28 +34,20 @@ 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;
//
@@ -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
{
@@ -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,
@@ -233,7 +226,9 @@ _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,
@@ -366,7 +361,9 @@ _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
);
/* __ __ _
* | \/ (_)___ __
* | |\/| | (_-</ _|_
@@ -386,9 +382,9 @@ DomitoValidateFileLegacyMode(
*
*/
//
// Finds the base address of a driver module.
//
//
// Finds the base address of a driver module. Clears the optional output first.
//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
@@ -469,14 +465,23 @@ DomitoReadFile(
);
//
// Gets the name of the main image of the process identified by PID.
// Gets the main image name after resolving the ULONG PID once to a process handle.
// Requires DomitoInit. Output is NULL on failure; successful result is one Domito
// allocation containing UNICODE_STRING and its buffer. Use DomitoFreeProcessImageName.
// Do not change memory callbacks with outstanding allocations. PID reuse can race
// the initial open; a later path reopen is not proof of original image identity.
//
_Success_(return == STATUS_SUCCESS)
_Must_inspect_result_
_IRQL_requires_max_(PASSIVE_LEVEL)
EXTERN_C
NTSTATUS
__declspec(deprecated(
"Use DomitoGetProcessImageNameEx with a caller-referenced PEPROCESS to avoid a fresh PID lookup; free the result with DomitoFreeProcessImageName."
)) NTSTATUS
DomitoGetProcessImageName(
_In_ ULONG ProcessId,
_Inout_ PUNICODE_STRING* ProcessImageName
_Outptr_result_maybenull_ PUNICODE_STRING* ProcessImageName
);
// Additive consumer helpers, included here for existing Domito.h clients.
#include "Domito.Consumer.h"
+396
View File
@@ -0,0 +1,396 @@
#include "Domito.Internal.h"
typedef struct _DOMITO_CERTIFICATE_DER_VIEW
{
const UCHAR* Data;
ULONG Size;
} DOMITO_CERTIFICATE_DER_VIEW;
static BOOLEAN
DomitoCertificateDerTake(
DOMITO_CERTIFICATE_DER_VIEW* Input,
UCHAR Tag,
DOMITO_CERTIFICATE_DER_VIEW* Content,
DOMITO_CERTIFICATE_DER_VIEW* Encoded = nullptr
)
{
if (Input->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<PUCHAR>(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<UCHAR>((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;
}
+140 -33
View File
@@ -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
File diff suppressed because it is too large. Load diff
+395
View File
@@ -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<PUNICODE_STRING>(G_Memory.Allocate(size));
if (!imageName)
{
status = STATUS_INSUFFICIENT_RESOURCES;
}
else
{
imageName->Buffer = reinterpret_cast<PWCH>(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<PVOID*>(&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<PUNICODE_STRING>(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<ULONG_PTR>(Chain);
const ULONG_PTR blobAddress = reinterpret_cast<ULONG_PTR>(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<SIZE_T>(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<const UCHAR*>(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<GET_PUBLISHER>(publisherRoutine);
status = getPublisher(const_cast<MINCERT_BLOB*>(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
+152 -105
View File
@@ -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)
@@ -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, &current, TRUE) != 0)
{
continue;
}
ULONG functionRva;
memcpy(&functionRva, base + exports.AddressOfFunctions + ordinal * sizeof(ULONG), sizeof(functionRva));
if (!functionRva)
{
return STATUS_NOT_FOUND; // Empty EAT slot.
}
if (functionRva >= imageSize)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
if (functionRva >= directoryOffset && functionRva - directoryOffset < exportSize)
{
return STATUS_NOT_SUPPORTED; // Forwarder string, not a callable address.
}
if (FunctionAddress)
{
*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;
}
/* Query the actual size of the process path */
status = G_Common.ZwQueryInformationProcess(
hProcess,
ProcessImageFileName,
NULL, // buffer
0, // buffer size
&returnedLength
);
if (STATUS_INFO_LENGTH_MISMATCH != status)
if (size < sizeof(UNICODE_STRING))
{
goto cleanUp;
status = STATUS_INVALID_BUFFER_SIZE;
}
else
{
auto image = static_cast<PUNICODE_STRING>(G_Memory.Allocate(size));
*ProcessImageName = (PUNICODE_STRING)G_Memory.Allocate(returnedLength);
if (*ProcessImageName == NULL)
if (!image)
{
status = STATUS_INSUFFICIENT_RESOURCES;
goto cleanUp;
}
/* Retrieve the process path from the handle to the process */
if (!NT_SUCCESS(status = G_Common.ZwQueryInformationProcess(
hProcess,
ProcessImageFileName,
*ProcessImageName,
returnedLength,
&returnedLength
)))
else
{
G_Memory.Free(*ProcessImageName);
}
ULONG returned = 0;
status = G_Common.ZwQueryInformationProcess(process, ProcessImageFileName, image, size, &returned);
cleanUp:
if (hProcess)
if (NT_SUCCESS(status))
{
ZwClose(hProcess);
const ULONG_PTR base = reinterpret_cast<ULONG_PTR>(image);
const ULONG_PTR buffer = reinterpret_cast<ULONG_PTR>(image->Buffer);
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 (NT_SUCCESS(status))
{
*ProcessImageName = image;
}
else
{
G_Memory.Free(image);
}
}
}
}
else if (NT_SUCCESS(status))
{
status = STATUS_INVALID_BUFFER_SIZE;
}
ZwClose(process);
return status;
}
#pragma code_seg()
+3
View File
@@ -121,6 +121,8 @@
<FilesToPackage Include="$(TargetPath)" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Domito.Certificate.cpp" />
<ClCompile Include="Domito.Consumer.cpp" />
<ClCompile Include="Domito.CodeIntegrity.cpp" />
<ClCompile Include="Domito.CodeIntegrity.Validation.cpp" />
<ClCompile Include="Domito.cpp" />
@@ -130,6 +132,7 @@
<ClInclude Include="..\include\Domito.MinCrypt.h" />
<ClInclude Include="..\include\Domito.MinCrypt.26100.h" />
<ClInclude Include="..\include\Domito.MinCrypt.Historical.h" />
<ClInclude Include="..\include\Domito.Consumer.h" />
<ClInclude Include="..\include\Domito.h" />
<ClInclude Include="Domito.Internal.h" />
</ItemGroup>
+9
View File
@@ -31,6 +31,12 @@
<ClCompile Include="Domito.CodeIntegrity.Validation.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Domito.Certificate.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Domito.Consumer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Domito.CodeIntegrity.cpp">
<Filter>Source Files</Filter>
</ClCompile>
@@ -39,6 +45,9 @@
<ClInclude Include="Domito.Internal.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\include\Domito.Consumer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\include\Domito.h">
<Filter>Header Files\Public</Filter>
</ClInclude>
+27
View File
@@ -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.
+106 -27
View File
@@ -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");
}
if (i >= 4) Check(r.HashSize == 64, "required capacity captured");
}
}
else
{
for (auto byte : r.Hash)
{
Check(byte == 0, "no digest from failed call");
}
}
if (i >= 4)
{
Check(r.HashSize == 64, "required capacity captured");
}
}
}
std::printf("PASS: %u probe checks (mock CI; no runtime trust validation)\n", checks);
}
+29 -9
View File
@@ -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, &timestamp, &result.SigningTime, hash.Bytes,
&result.HashSize, &result.HashAlgId);
result.Status = CiValidateFileObject(
FileObject,
result.Flags,
0,
&policy,
&timestamp,
&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(&timestamp);
}
+4 -2
View File
@@ -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
);
+421 -117
View File
@@ -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<UCHAR*>(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<size;++i) Check(static_cast<UCHAR*>(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<UCHAR*>(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<PFILE_OBJECT>(static_cast<ULONG_PTR>(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<DOMITO_CI_SIGNATURE_SELECTION>(-1),&result)==STATUS_INVALID_PARAMETER,"invalid selection");
Check(validationCalls==beforeInvalid,"invalid requests never call validation"); Clear(&result);
for (NTSTATUS failure : {STATUS_INVALID_IMAGE_HASH,STATUS_BUFFER_TOO_SMALL,STATUS_INSUFFICIENT_RESOURCES}) {
nativeStatus=failure;
Check(DomitoValidateFileObjectEx(object,0x20,0,DomitoCiSignatureSelectionNative,&result)==failure,"native failure preserved");
CheckEmptyDigest(result); Check(live==2 && result.PolicyInfo.PolicyBits==0x123,"failure policy diagnostics remain owned"); Clear(&result);
}
nativeStatus=STATUS_SUCCESS;
nativeSize=65;
Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_BUFFER_SIZE,"oversized returned hash rejected");
CheckEmptyDigest(result); Clear(&result);
nativeSize=31;
Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_BUFFER_SIZE,"algorithm length mismatch rejected"); Clear(&result);
nativeSize=32; nativeAlgorithm=0xdead;
Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_NOT_SUPPORTED,"unknown returned algorithm rejected"); Clear(&result);
for (ALG_ID algorithm : {CALG_SHA1,CALG_SHA_256,CALG_SHA_384,CALG_SHA_512}) {
nativeAlgorithm=algorithm; nativeSize=DomitoCiDigestSize(algorithm);
Check(DomitoValidateFileObjectEx(object,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_SUCCESS,"known native digest sizes");
for (UINT32 i=result.HashSize;i<sizeof(result.Hash);++i) Check(!result.Hash[i],"unused native digest bytes cleared");
const unsigned beforeInvalid = validationCalls;
Check(
DomitoValidateFileObjectEx(nullptr, 0x20, 0, DomitoCiSignatureSelectionNative, &result) ==
STATUS_INVALID_PARAMETER,
"null file object"
);
Check(
DomitoValidateFileObjectEx(object, 0x20, 0, static_cast<DOMITO_CI_SIGNATURE_SELECTION>(-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");
}
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");
Clear(&result);
fakeDigestSize = DomitoCiDigestSize(algorithm);
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), algorithm, &result) ==
STATUS_SUCCESS,
"embedded success"
);
Check(
observedEmbeddedAlgorithm == algorithm && observedSecuritySize == 123 && observedCertificate == image,
"embedded certificate/algorithm forwarded"
);
Check(
result.HashSize == fakeDigestSize && result.HashAlgId == algorithm && result.Hash[0] == 0xa5 &&
result.SigningTime.QuadPart == 123,
"embedded result owns copied digest"
);
Check(
!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
"embedded metadata and digest cleanup"
);
Clear(&result);
}
nativeAlgorithm = CALG_SHA_256;
nativeSize = 32;
fakeDigestSize = 32;
nativeStatus = STATUS_MORE_ENTRIES;
Check(
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_MORE_ENTRIES,
"nonnegative native status preserved"
);
Clear(&result);
nativeStatus = STATUS_SUCCESS;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(nullptr, 0, CALG_SHA_256, &result) ==
STATUS_INVALID_PARAMETER,
"null PE"
);
unsigned beforeDigest = digestCalls;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), 0xdead, &result) ==
STATUS_NOT_SUPPORTED,
"unsupported algorithm"
);
Check(digestCalls == beforeDigest, "unsupported algorithm does not hash");
for (NTSTATUS failure : {STATUS_NOT_SUPPORTED, STATUS_INVALID_IMAGE_FORMAT, STATUS_INSUFFICIENT_RESOURCES})
{
digestStatus = failure;
unsigned beforeEmbedded = embeddedCalls;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == failure,
"digest failure propagation"
);
Check(embeddedCalls == beforeEmbedded, "digest failure never calls CI");
CheckEmptyDigest(result);
Clear(&result);
}
partialDigestOnFailure = true;
unsigned beforeFree = digestFrees;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INSUFFICIENT_RESOURCES,
"partial digest failure"
);
Check(digestFrees == beforeFree + 1, "partial digest allocation freed");
Clear(&result);
partialDigestOnFailure = false;
digestStatus = STATUS_SUCCESS;
omitCertificate = true;
unsigned beforeEmbedded = embeddedCalls;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_IMAGE_HASH,
"unsigned PE rejected"
);
Check(embeddedCalls == beforeEmbedded, "unsigned PE does not call CI");
Clear(&result);
omitCertificate = false;
omitDigest = true;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_BUFFER_SIZE,
"missing digest rejected"
);
Clear(&result);
omitDigest = false;
fakeDigestSize = 31;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_BUFFER_SIZE,
"digest helper length mismatch"
);
Clear(&result);
fakeDigestSize = 32;
embeddedStatus = STATUS_INVALID_IMAGE_HASH;
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_INVALID_IMAGE_HASH,
"CI embedded failure propagated without fallback"
);
CheckEmptyDigest(result);
Check(live == 2 && result.TimeStampPolicyInfo.PolicyBits == 0x456, "embedded failure diagnostics owned");
Clear(&result);
embeddedStatus = STATUS_SUCCESS;
for (unsigned i = 0; i < 16; ++i)
{
Check(
DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS,
"repeated native"
);
Check(
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
STATUS_SUCCESS,
"reuse native result as embedded result"
);
Check(
!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
"reuse resets metadata without leaking"
);
}
// Invalid calls must release a previously valid result, too.
Check(DomitoValidateFileObjectEx(nullptr,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_PARAMETER,"invalid reuse cleans old result"); Clear(&result);
std::printf("PASS: %u CI validation checks (%s architecture branch), no live allocations\n",checks,
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
+340 -97
View File
@@ -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<UCHAR> Image() {
static std::vector<UCHAR>
Image()
{
std::vector<UCHAR> image(4096);
auto dos = (IMAGE_DOS_HEADER*)image.data(); dos->e_magic = IMAGE_DOS_SIGNATURE; dos->e_lfanew = 128;
auto nt = (IMAGE_NT_HEADERS64*)(image.data() + 128); nt->Signature = IMAGE_NT_SIGNATURE;
nt->OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; nt->OptionalHeader.SizeOfImage = (ULONG)image.size();
auto 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<UCHAR>& image, const char* name, PVOID* output) {
static NTSTATUS
Lookup(
std::vector<UCHAR>& image,
const char* name,
PVOID* output
)
{
ANSI_STRING string = {(USHORT)std::strlen(name), (USHORT)std::strlen(name), (PCHAR)name};
exportPointer = image.data() + 512; exportBytes = 256;
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);
}
+967
View File
@@ -0,0 +1,967 @@
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <bcrypt.h>
#include <wincrypt.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <initializer_list>
#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<char*>(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<PWCHAR>(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<PVOID>(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<MINCRYPT_CHAIN_INFO*>(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<PWCHAR>(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<PUNICODE_STRING>(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<PUNICODE_STRING>(buffer));
if (failure == 7)
{
return STATUS_UNSUCCESSFUL;
}
if (failure == 11)
{
static_cast<PUNICODE_STRING>(buffer)->Buffer = reinterpret_cast<PWCHAR>(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<UCHAR>;
static Bytes
Der(
UCHAR tag,
const Bytes& contents
)
{
Bytes b = {tag};
if (contents.size() < 128)
{
b.push_back(static_cast<UCHAR>(contents.size()));
}
else
{
Check(contents.size() < 256, "fixture DER bound");
b.push_back(0x81);
b.push_back(static_cast<UCHAR>(contents.size()));
}
b.insert(b.end(), contents.begin(), contents.end());
return b;
}
static Bytes
Join(
std::initializer_list<Bytes> 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<ULONG>(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<DWORD>(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<ULONG>(cert.size()), DomitoCertificateHashSpkiDer, CALG_SHA_256, full, &a
)),
"full SPKI hash"
);
Check(
NT_SUCCESS(DomitoHashCertificate(
cert.data(),
static_cast<ULONG>(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<ULONG>(cert.size()), DomitoCertificateHashDer, CALG_SHA_256, whole, &c
)),
"DER thumbprint"
);
d = 64;
Check(
CryptHashCertificate2(
BCRYPT_SHA256_ALGORITHM, 0, nullptr, cert.data(), static_cast<DWORD>(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<ULONG>(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<ULONG>(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<UCHAR>(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<PUCHAR>(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<PWCHAR>(L"\\foo")};
for (int fail : {0, 2, 3})
{
failure = fail;
const auto r = refs, d = derefs, c = closed;
result.PolicyInfo.ChainInfo = static_cast<MINCRYPT_CHAIN_INFO*>(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<PUNICODE_STRING>(1);
NTSTATUS s = DomitoGetProcessImageNameEx(&processToken, &name);
Check(NT_SUCCESS(s) == (fail == 0), "PEPROCESS lookup failure");
if (NT_SUCCESS(s))
{
Check(
name->Buffer == reinterpret_cast<PWCHAR>(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<PUNICODE_STRING>(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<PUCHAR>(&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<PVOID>(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<PVOID>(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<MINCRYPT_CHAIN_INFO*>(storage);
chain->Size = sizeof(storage);
chain->ChainElements = reinterpret_cast<MINCRYPT_CHAIN_ELEMENT*>(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<PCHAR>(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);
}
+710 -179
View File
File diff suppressed because it is too large. Load diff
+36 -6
View File
@@ -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.
+25 -11
View File
@@ -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)
+45 -19
View File
@@ -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)
+28 -11
View File
@@ -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)
+71
View File
@@ -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 <pshpack8.h>") : mincrypt.index("#include <poppack.h>")
+ len("#include <poppack.h>")
]
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)
+94
View File
@@ -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 <ntifs.h>\n#include <ntintsafe.h>\n#include <ntimage.h>\n#include <bcrypt.h>\n#include <Domito.h>\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")
+40 -17
View File
@@ -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)