Document CI validation flag 0x20 and add kernel probe
This commit is contained in:
1 parent
195b9fccf3
commit
1026c48324
8 files changed
+381
-7
No files matched your search
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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, ×tamp, &result.SigningTime, hash.Bytes,
|
||||
&result.HashSize, &result.HashAlgId);
|
||||
result.HashGuardIntact = hash.Guard == 0xD01170AB;
|
||||
// Keep failure outputs as raw observations; only successful calls have
|
||||
// a usable digest. Do not dereference the incompletely mapped chains.
|
||||
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(×tamp);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user