Document CI validation flag 0x20 and add kernel probe

This commit is contained in:
nefarius committed 2026-10-26 15:30:03 +01:00
1 parent 195b9fccf3
commit 1026c48324
8 files changed
+381 -7

No files matched your search

+7
View File
@@ -21,3 +21,10 @@ The tests do not validate CI trust policy, asynchronous kernel I/O behavior, or
general Authenticode hashing of unusual section layouts. The digest implementation
still uses its existing linear byte-order convention and heuristic signature
algorithm detection; replacing those requires a separate compatibility corpus.
## File-object validation flag probe
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
for `CiValidateFileObject` flag `0x20`. Run `python tests\run-ci-probe-tests.py`
from the same developer shell to test probe plumbing with mock CI. The kernel
probe must still be run in a suitable VM to verify actual policy behavior.
+96
View File
@@ -0,0 +1,96 @@
// Host tests validate the probe plumbing, NOT CI trust behavior.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "ci-probe-constants.inc"
#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;
void* ChainInfo;
};
static unsigned calls, frees, outstanding, checks;
static bool failureMode;
static int fileToken, chainToken;
static void Check(bool value, const char* label)
{
++checks;
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)
{
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(*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");
++calls;
if (*size < 64) { *size = 64; return STATUS_BUFFER_TOO_SMALL; }
policy->ChainInfo = timestamp->ChainInfo = &chainToken;
outstanding = 2;
policy->PolicyBits = 0xA5;
timestamp->PolicyBits = 0x5A;
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;
std::memset(hash, 0xCC, 32);
*size = 32;
*algorithm = 0x800C;
time->QuadPart = 123;
return STATUS_SUCCESS;
}
static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* policy)
{
++frees;
if (policy->ChainInfo) { Check(outstanding > 0, "owned chain"); --outstanding; }
*policy = {};
}
#include "ci-file-object-probe.cpp"
int main()
{
for (unsigned pass = 0; pass < 2; ++pass)
{
failureMode = pass != 0;
calls = frees = outstanding = 0;
DOMITO_CI_PROBE_RESULT results[DOMITO_CI_PROBE_RESULT_COUNT];
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;
Check(r.Status == expected, "status preserved");
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");
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");
}
}
std::printf("PASS: %u probe checks (mock CI; no runtime trust validation)\n", checks);
}
+52
View File
@@ -0,0 +1,52 @@
// Compile into an existing signed test driver; this file has no DriverEntry.
#ifndef DOMITO_CI_PROBE_HOST_TEST
#include <ntifs.h>
#include "Domito.MinCrypt.h"
#endif
#include "ci-file-object-probe.h"
_IRQL_requires_max_(PASSIVE_LEVEL)
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
};
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 {
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.HashGuardIntact = hash.Guard == 0xD01170AB;
// Keep failure outputs as raw observations; only successful calls have
// a usable digest. Do not dereference the incompletely mapped chains.
result.PolicyBits = policy.PolicyBits;
result.TimeStampPolicyBits = timestamp.PolicyBits;
result.VerificationStatus = (NTSTATUS)policy.VerificationStatus;
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);
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Include ntifs.h and Domito.MinCrypt.h first. Test support, not a library API.
typedef struct _DOMITO_CI_PROBE_RESULT
{
ULONG Flags;
ULONG InputCapacity;
NTSTATUS Status;
UINT32 HashSize;
ALG_ID HashAlgId;
UCHAR Hash[MINCRYPT_MAX_HASH_LENGTH];
LARGE_INTEGER SigningTime;
ULONG PolicyBits;
ULONG TimeStampPolicyBits;
NTSTATUS VerificationStatus;
NTSTATUS TimeStampVerificationStatus;
BOOLEAN HasChain;
BOOLEAN HasTimeStampChain;
BOOLEAN HashGuardIntact;
} DOMITO_CI_PROBE_RESULT;
#define DOMITO_CI_PROBE_RESULT_COUNT 6
// 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(
_In_ PFILE_OBJECT FileObject,
_Out_writes_(DOMITO_CI_PROBE_RESULT_COUNT) DOMITO_CI_PROBE_RESULT* Results);
+20
View File
@@ -0,0 +1,20 @@
"""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:
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)
subprocess.run([str(exe)], cwd=build, check=True)