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:
1 parent
1f4d62ae95
commit
a491bbd3f8
32 files changed
+5404
-1136
No files matched your search
@@ -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.
|
||||
@@ -10,34 +10,76 @@
|
||||
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
||||
#define DOMITO_CI_PROBE_HOST_TEST
|
||||
typedef void* PFILE_OBJECT;
|
||||
struct MINCRYPT_POLICY_INFO {
|
||||
ULONG Size, VerificationStatus, PolicyBits;
|
||||
|
||||
struct MINCRYPT_POLICY_INFO
|
||||
{
|
||||
ULONG Size;
|
||||
ULONG VerificationStatus;
|
||||
ULONG PolicyBits;
|
||||
void* ChainInfo;
|
||||
};
|
||||
static unsigned calls, frees, outstanding, checks;
|
||||
|
||||
static unsigned calls;
|
||||
static unsigned frees;
|
||||
static unsigned outstanding;
|
||||
static unsigned checks;
|
||||
static bool failureMode;
|
||||
static int fileToken, chainToken;
|
||||
static void Check(bool value, const char* label)
|
||||
static int fileToken;
|
||||
static int chainToken;
|
||||
|
||||
static void
|
||||
Check(
|
||||
bool value,
|
||||
const char* label
|
||||
)
|
||||
{
|
||||
++checks;
|
||||
if (!value) { std::fprintf(stderr, "FAIL: %s\n", label); std::exit(1); }
|
||||
|
||||
if (!value)
|
||||
{
|
||||
std::fprintf(stderr, "FAIL: %s\n", label);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
static NTSTATUS CiValidateFileObject(PFILE_OBJECT file, ULONG flags, ULONG level,
|
||||
MINCRYPT_POLICY_INFO* policy, MINCRYPT_POLICY_INFO* timestamp,
|
||||
LARGE_INTEGER* time, UCHAR* hash, UINT32* size, ALG_ID* algorithm)
|
||||
|
||||
static NTSTATUS
|
||||
CiValidateFileObject(
|
||||
PFILE_OBJECT file,
|
||||
ULONG flags,
|
||||
ULONG level,
|
||||
MINCRYPT_POLICY_INFO* policy,
|
||||
MINCRYPT_POLICY_INFO* timestamp,
|
||||
LARGE_INTEGER* time,
|
||||
UCHAR* hash,
|
||||
UINT32* size,
|
||||
ALG_ID* algorithm
|
||||
)
|
||||
{
|
||||
const ULONG expectedFlags[] = {0, 0x20, 0x20, 0, 0, 0x20};
|
||||
Check(calls < 6 && flags == expectedFlags[calls], "paired flags and order");
|
||||
Check(file == &fileToken && level == 0, "same file and level zero");
|
||||
Check(outstanding == 0, "previous outputs freed before next call");
|
||||
Check(policy->Size == sizeof(*policy) && timestamp->Size == sizeof(*timestamp), "policy sizes");
|
||||
Check(!policy->ChainInfo && !timestamp->ChainInfo && !policy->PolicyBits &&
|
||||
!timestamp->PolicyBits, "fresh policy objects");
|
||||
Check(
|
||||
!policy->ChainInfo && !timestamp->ChainInfo && !policy->PolicyBits && !timestamp->PolicyBits,
|
||||
"fresh policy objects"
|
||||
);
|
||||
Check(*size == (calls < 4 ? 64u : 63u), "capacity reset for every call");
|
||||
Check(*algorithm == 0 && time->QuadPart == 0, "fresh scalar outputs");
|
||||
for (unsigned i = 0; i < 64; ++i) Check(hash[i] == 0, "fresh hash buffer");
|
||||
|
||||
for (unsigned i = 0; i < 64; ++i)
|
||||
{
|
||||
Check(hash[i] == 0, "fresh hash buffer");
|
||||
}
|
||||
|
||||
++calls;
|
||||
if (*size < 64) { *size = 64; return STATUS_BUFFER_TOO_SMALL; }
|
||||
|
||||
if (*size < 64)
|
||||
{
|
||||
*size = 64;
|
||||
return STATUS_BUFFER_TOO_SMALL;
|
||||
}
|
||||
|
||||
policy->ChainInfo = timestamp->ChainInfo = &chainToken;
|
||||
outstanding = 2;
|
||||
policy->PolicyBits = 0xA5;
|
||||
@@ -45,22 +87,38 @@ static NTSTATUS CiValidateFileObject(PFILE_OBJECT file, ULONG flags, ULONG level
|
||||
policy->VerificationStatus = failureMode ? STATUS_INVALID_IMAGE_HASH : STATUS_SUCCESS;
|
||||
timestamp->VerificationStatus = STATUS_SUCCESS;
|
||||
// Also allocate on simulated failure to exercise unconditional cleanup.
|
||||
if (failureMode) return STATUS_INVALID_IMAGE_HASH;
|
||||
if (failureMode)
|
||||
{
|
||||
return STATUS_INVALID_IMAGE_HASH;
|
||||
}
|
||||
|
||||
std::memset(hash, 0xCC, 32);
|
||||
*size = 32;
|
||||
*algorithm = 0x800C;
|
||||
time->QuadPart = 123;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* policy)
|
||||
|
||||
static void
|
||||
CiFreePolicyInfo(
|
||||
MINCRYPT_POLICY_INFO* policy
|
||||
)
|
||||
{
|
||||
++frees;
|
||||
if (policy->ChainInfo) { Check(outstanding > 0, "owned chain"); --outstanding; }
|
||||
|
||||
if (policy->ChainInfo)
|
||||
{
|
||||
Check(outstanding > 0, "owned chain");
|
||||
--outstanding;
|
||||
}
|
||||
|
||||
*policy = {};
|
||||
}
|
||||
|
||||
#include "ci-file-object-probe.cpp"
|
||||
|
||||
int main()
|
||||
int
|
||||
main()
|
||||
{
|
||||
for (unsigned pass = 0; pass < 2; ++pass)
|
||||
{
|
||||
@@ -70,27 +128,48 @@ int main()
|
||||
std::memset(results, 0xFF, sizeof(results));
|
||||
DomitoProbeCiValidateFileObject(&fileToken, results);
|
||||
Check(calls == 6 && frees == 12 && outstanding == 0, "all calls and frees");
|
||||
|
||||
for (unsigned i = 0; i < 6; ++i)
|
||||
{
|
||||
const auto& r = results[i];
|
||||
Check(r.HashGuardIntact, "buffer guard");
|
||||
const NTSTATUS expected = i >= 4 ? STATUS_BUFFER_TOO_SMALL :
|
||||
failureMode ? STATUS_INVALID_IMAGE_HASH : STATUS_SUCCESS;
|
||||
const NTSTATUS expected = i >= 4 ? STATUS_BUFFER_TOO_SMALL
|
||||
: failureMode ? STATUS_INVALID_IMAGE_HASH
|
||||
: STATUS_SUCCESS;
|
||||
Check(r.Status == expected, "status preserved");
|
||||
if (i < 4) {
|
||||
|
||||
if (i < 4)
|
||||
{
|
||||
Check(r.PolicyBits == 0xA5 && r.TimeStampPolicyBits == 0x5A, "policy capture");
|
||||
Check(r.HasChain && r.HasTimeStampChain, "chain presence captured before free");
|
||||
}
|
||||
if (NT_SUCCESS(r.Status)) {
|
||||
Check(r.HashSize == 32 && r.HashAlgId == 0x800C && r.SigningTime.QuadPart == 123,
|
||||
"digest metadata and signing time");
|
||||
|
||||
if (NT_SUCCESS(r.Status))
|
||||
{
|
||||
Check(
|
||||
r.HashSize == 32 && r.HashAlgId == 0x800C && r.SigningTime.QuadPart == 123,
|
||||
"digest metadata and signing time"
|
||||
);
|
||||
|
||||
for (unsigned j = 0; j < 64; ++j)
|
||||
{
|
||||
Check(r.Hash[j] == (j < 32 ? 0xCC : 0), "digest copied and tail cleared");
|
||||
} else {
|
||||
for (auto byte : r.Hash) Check(byte == 0, "no digest from failed call");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto byte : r.Hash)
|
||||
{
|
||||
Check(byte == 0, "no digest from failed call");
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= 4)
|
||||
{
|
||||
Check(r.HashSize == 64, "required capacity captured");
|
||||
}
|
||||
if (i >= 4) Check(r.HashSize == 64, "required capacity captured");
|
||||
}
|
||||
}
|
||||
|
||||
std::printf("PASS: %u probe checks (mock CI; no runtime trust validation)\n", checks);
|
||||
}
|
||||
@@ -6,33 +6,51 @@
|
||||
#include "ci-file-object-probe.h"
|
||||
|
||||
_IRQL_requires_max_(PASSIVE_LEVEL)
|
||||
void DomitoProbeCiValidateFileObject(
|
||||
PFILE_OBJECT FileObject, DOMITO_CI_PROBE_RESULT* Results)
|
||||
void
|
||||
DomitoProbeCiValidateFileObject(
|
||||
PFILE_OBJECT FileObject,
|
||||
DOMITO_CI_PROBE_RESULT* Results
|
||||
)
|
||||
{
|
||||
const ULONG flags[DOMITO_CI_PROBE_RESULT_COUNT] = {
|
||||
0, DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
|
||||
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE, 0,
|
||||
0, DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE
|
||||
0,
|
||||
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
|
||||
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE,
|
||||
0,
|
||||
0,
|
||||
DOMITO_CI_VALIDATE_FLAG_BROADEN_ROOT_ACCEPTANCE
|
||||
};
|
||||
|
||||
for (ULONG i = 0; i < DOMITO_CI_PROBE_RESULT_COUNT; ++i)
|
||||
{
|
||||
MINCRYPT_POLICY_INFO policy = {};
|
||||
MINCRYPT_POLICY_INFO timestamp = {};
|
||||
policy.Size = sizeof(policy);
|
||||
timestamp.Size = sizeof(timestamp);
|
||||
struct {
|
||||
|
||||
struct
|
||||
{
|
||||
UCHAR Bytes[MINCRYPT_MAX_HASH_LENGTH];
|
||||
ULONG Guard;
|
||||
} hash = {};
|
||||
|
||||
hash.Guard = 0xD01170AB;
|
||||
DOMITO_CI_PROBE_RESULT& result = Results[i];
|
||||
result = {};
|
||||
result.Flags = flags[i];
|
||||
result.InputCapacity = i < 4 ? sizeof(hash.Bytes) : sizeof(hash.Bytes) - 1;
|
||||
result.HashSize = result.InputCapacity;
|
||||
result.Status = CiValidateFileObject(FileObject, result.Flags, 0,
|
||||
&policy, ×tamp, &result.SigningTime, hash.Bytes,
|
||||
&result.HashSize, &result.HashAlgId);
|
||||
result.Status = CiValidateFileObject(
|
||||
FileObject,
|
||||
result.Flags,
|
||||
0,
|
||||
&policy,
|
||||
×tamp,
|
||||
&result.SigningTime,
|
||||
hash.Bytes,
|
||||
&result.HashSize,
|
||||
&result.HashAlgId
|
||||
);
|
||||
result.HashGuardIntact = hash.Guard == 0xD01170AB;
|
||||
// Keep failure outputs as raw observations; only successful calls have
|
||||
// a usable digest. Do not dereference the incompletely mapped chains.
|
||||
@@ -42,10 +60,12 @@ void DomitoProbeCiValidateFileObject(
|
||||
result.TimeStampVerificationStatus = (NTSTATUS)timestamp.VerificationStatus;
|
||||
result.HasChain = policy.ChainInfo != nullptr;
|
||||
result.HasTimeStampChain = timestamp.ChainInfo != nullptr;
|
||||
|
||||
if (NT_SUCCESS(result.Status) && result.HashSize <= sizeof(result.Hash))
|
||||
{
|
||||
RtlCopyMemory(result.Hash, hash.Bytes, result.HashSize);
|
||||
}
|
||||
|
||||
CiFreePolicyInfo(&policy);
|
||||
CiFreePolicyInfo(×tamp);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
Clear(&result);
|
||||
fakeDigestSize = DomitoCiDigestSize(algorithm);
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), algorithm, &result) ==
|
||||
STATUS_SUCCESS,
|
||||
"embedded success"
|
||||
);
|
||||
Check(
|
||||
observedEmbeddedAlgorithm == algorithm && observedSecuritySize == 123 && observedCertificate == image,
|
||||
"embedded certificate/algorithm forwarded"
|
||||
);
|
||||
Check(
|
||||
result.HashSize == fakeDigestSize && result.HashAlgId == algorithm && result.Hash[0] == 0xa5 &&
|
||||
result.SigningTime.QuadPart == 123,
|
||||
"embedded result owns copied digest"
|
||||
);
|
||||
Check(
|
||||
!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
|
||||
"embedded metadata and digest cleanup"
|
||||
);
|
||||
Clear(&result);
|
||||
}
|
||||
partialDigestOnFailure=true;
|
||||
unsigned beforeFree=digestFrees;
|
||||
Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INSUFFICIENT_RESOURCES,"partial digest failure");
|
||||
Check(digestFrees==beforeFree+1,"partial digest allocation freed"); Clear(&result);
|
||||
partialDigestOnFailure=false; digestStatus=STATUS_SUCCESS;
|
||||
omitCertificate=true;
|
||||
unsigned beforeEmbedded=embeddedCalls;
|
||||
Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_IMAGE_HASH,"unsigned PE rejected");
|
||||
Check(embeddedCalls==beforeEmbedded,"unsigned PE does not call CI"); Clear(&result); omitCertificate=false;
|
||||
omitDigest=true;
|
||||
Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_BUFFER_SIZE,"missing digest rejected"); Clear(&result); omitDigest=false;
|
||||
fakeDigestSize=31;
|
||||
Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_BUFFER_SIZE,"digest helper length mismatch"); Clear(&result); fakeDigestSize=32;
|
||||
embeddedStatus=STATUS_INVALID_IMAGE_HASH;
|
||||
Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_INVALID_IMAGE_HASH,"CI embedded failure propagated without fallback");
|
||||
CheckEmptyDigest(result); Check(live==2 && result.TimeStampPolicyInfo.PolicyBits==0x456,"embedded failure diagnostics owned"); Clear(&result); embeddedStatus=STATUS_SUCCESS;
|
||||
for (unsigned i=0;i<16;++i) {
|
||||
Check(DomitoValidateFileObjectEx(object,0x20,0,DomitoCiSignatureSelectionNative,&result)==STATUS_SUCCESS,"repeated native");
|
||||
Check(DomitoValidatePortableExecutableEmbeddedSignature(image,sizeof(image),CALG_SHA_256,&result)==STATUS_SUCCESS,"reuse native result as embedded result");
|
||||
Check(!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live==2,"reuse resets metadata without leaking");
|
||||
|
||||
nativeAlgorithm = CALG_SHA_256;
|
||||
nativeSize = 32;
|
||||
fakeDigestSize = 32;
|
||||
nativeStatus = STATUS_MORE_ENTRIES;
|
||||
Check(
|
||||
DomitoValidateFileObjectEx(object, 0, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_MORE_ENTRIES,
|
||||
"nonnegative native status preserved"
|
||||
);
|
||||
Clear(&result);
|
||||
nativeStatus = STATUS_SUCCESS;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(nullptr, 0, CALG_SHA_256, &result) ==
|
||||
STATUS_INVALID_PARAMETER,
|
||||
"null PE"
|
||||
);
|
||||
unsigned beforeDigest = digestCalls;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), 0xdead, &result) ==
|
||||
STATUS_NOT_SUPPORTED,
|
||||
"unsupported algorithm"
|
||||
);
|
||||
Check(digestCalls == beforeDigest, "unsupported algorithm does not hash");
|
||||
|
||||
for (NTSTATUS failure : {STATUS_NOT_SUPPORTED, STATUS_INVALID_IMAGE_FORMAT, STATUS_INSUFFICIENT_RESOURCES})
|
||||
{
|
||||
digestStatus = failure;
|
||||
unsigned beforeEmbedded = embeddedCalls;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) == failure,
|
||||
"digest failure propagation"
|
||||
);
|
||||
Check(embeddedCalls == beforeEmbedded, "digest failure never calls CI");
|
||||
CheckEmptyDigest(result);
|
||||
Clear(&result);
|
||||
}
|
||||
|
||||
partialDigestOnFailure = true;
|
||||
unsigned beforeFree = digestFrees;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
|
||||
STATUS_INSUFFICIENT_RESOURCES,
|
||||
"partial digest failure"
|
||||
);
|
||||
Check(digestFrees == beforeFree + 1, "partial digest allocation freed");
|
||||
Clear(&result);
|
||||
partialDigestOnFailure = false;
|
||||
digestStatus = STATUS_SUCCESS;
|
||||
omitCertificate = true;
|
||||
unsigned beforeEmbedded = embeddedCalls;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
|
||||
STATUS_INVALID_IMAGE_HASH,
|
||||
"unsigned PE rejected"
|
||||
);
|
||||
Check(embeddedCalls == beforeEmbedded, "unsigned PE does not call CI");
|
||||
Clear(&result);
|
||||
omitCertificate = false;
|
||||
omitDigest = true;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
|
||||
STATUS_INVALID_BUFFER_SIZE,
|
||||
"missing digest rejected"
|
||||
);
|
||||
Clear(&result);
|
||||
omitDigest = false;
|
||||
fakeDigestSize = 31;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
|
||||
STATUS_INVALID_BUFFER_SIZE,
|
||||
"digest helper length mismatch"
|
||||
);
|
||||
Clear(&result);
|
||||
fakeDigestSize = 32;
|
||||
embeddedStatus = STATUS_INVALID_IMAGE_HASH;
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
|
||||
STATUS_INVALID_IMAGE_HASH,
|
||||
"CI embedded failure propagated without fallback"
|
||||
);
|
||||
CheckEmptyDigest(result);
|
||||
Check(live == 2 && result.TimeStampPolicyInfo.PolicyBits == 0x456, "embedded failure diagnostics owned");
|
||||
Clear(&result);
|
||||
embeddedStatus = STATUS_SUCCESS;
|
||||
|
||||
for (unsigned i = 0; i < 16; ++i)
|
||||
{
|
||||
Check(
|
||||
DomitoValidateFileObjectEx(object, 0x20, 0, DomitoCiSignatureSelectionNative, &result) == STATUS_SUCCESS,
|
||||
"repeated native"
|
||||
);
|
||||
Check(
|
||||
DomitoValidatePortableExecutableEmbeddedSignature(image, sizeof(image), CALG_SHA_256, &result) ==
|
||||
STATUS_SUCCESS,
|
||||
"reuse native result as embedded result"
|
||||
);
|
||||
Check(
|
||||
!result.FileObjectValidationFlags && !result.RequestedSigningLevel && live == 2,
|
||||
"reuse resets metadata without leaking"
|
||||
);
|
||||
}
|
||||
|
||||
// Invalid calls must release a previously valid result, too.
|
||||
Check(DomitoValidateFileObjectEx(nullptr,0,0,DomitoCiSignatureSelectionNative,&result)==STATUS_INVALID_PARAMETER,"invalid reuse cleans old result"); Clear(&result);
|
||||
std::printf("PASS: %u CI validation checks (%s architecture branch), no live allocations\n",checks,
|
||||
Check(
|
||||
DomitoValidateFileObjectEx(nullptr, 0, 0, DomitoCiSignatureSelectionNative, &result) ==
|
||||
STATUS_INVALID_PARAMETER,
|
||||
"invalid reuse cleans old result"
|
||||
);
|
||||
Clear(&result);
|
||||
std::printf(
|
||||
"PASS: %u CI validation checks (%s architecture branch), no live allocations\n",
|
||||
checks,
|
||||
#ifdef DOMITO_TEST_OTHER_ARCH
|
||||
"non-x64"
|
||||
#else
|
||||
|
||||
+340
-97
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
+721
-190
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
Reference in new issue
Block a user