Harden CI digest parsing and failure cleanup

This commit is contained in:
nefarius committed 2026-10-26 15:04:22 +01:00
1 parent 7985b23be5
commit 195b9fccf3
6 files changed
+492 -239

No files matched your search

+5 -2
View File
@@ -352,7 +352,7 @@ typedef struct _MINCRYPT_POLICY_INFO
#endif // NTDDI_VERSION >= NTDDI_WIN10 #endif // NTDDI_VERSION >= NTDDI_WIN10
}MINCRYPT_POLICY_INFO, PMINCRYPT_POLICY_INFO; }MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO;
#include <poppack.h> #include <poppack.h>
@@ -491,7 +491,10 @@ typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE;
* *
* @param PublisherName[out] - publisher name. * @param PublisherName[out] - publisher name.
* *
* @return buffer length. * @return NTSTATUS. The string byte count is returned in PublisherName->Length.
* Free PublisherName->Buffer with the allocator paired with AllocateRoutine,
* including on failure if a buffer was allocated. This routine extracts a name;
* it does not verify the certificate or its signer.
*/ */
MINCRYPTAPI MINCRYPTAPI
NTSTATUS NTSTATUS
+10 -3
View File
@@ -170,8 +170,10 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM(
*/ */
// //
// Extracts the CALG_ID from a signed PE that was used to // Heuristically detects the CALG_ID in the first WIN_CERTIFICATE using byte patterns.
// calculate the message digest when it was signed // Returns CALG_SHA1 when absent, malformed, or unrecognized; this is not ASN.1
// validation or signature verification. The directory array must contain the
// IMAGE_DIRECTORY_ENTRY_SECURITY entry.
// //
_IRQL_requires_max_(DISPATCH_LEVEL) _IRQL_requires_max_(DISPATCH_LEVEL)
EXTERN_C EXTERN_C
@@ -183,7 +185,12 @@ DomitoGetPortableExecutableDigestKind(
); );
// //
// Extracts Authenticode signing information and calculates the file digest of a PE file. // Calculates a PE digest using the existing linear hashing convention.
// Validates accessed header/certificate ranges and clears outputs on failure
// when all output pointers are valid. The input buffer must remain stable.
// The returned certificate pointer borrows the input buffer; free only the digest
// with DomitoFreePortableExecutableDigest. Algorithm detection is heuristic;
// general section-ordered Authenticode hashing is not implemented here.
// //
_Success_(return == STATUS_SUCCESS) _Success_(return == STATUS_SUCCESS)
_Must_inspect_result_ _Must_inspect_result_
+161 -234
View File
@@ -24,21 +24,26 @@ DomitoGetPortableExecutableDigestKind(
_In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory _In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory
) )
{ {
const ULONG securityRva = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress; if (!pPeBytes || !pImgDataDirectory)
if (!securityRva)
{ {
return CALG_SHA1; return CALG_SHA1;
} }
const IMAGE_DATA_DIRECTORY security = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY];
if (securityRva > PeSize || (PeSize - securityRva) < sizeof(WIN_CERTIFICATE)) const ULONG securityRva = security.VirtualAddress;
const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate);
if (!securityRva || securityRva > PeSize || security.Size > PeSize - securityRva ||
security.Size < certificateHeaderSize)
{
return CALG_SHA1;
}
ULONG certificateSize;
memcpy(&certificateSize, pPeBytes + securityRva, sizeof(certificateSize));
if (certificateSize < certificateHeaderSize || certificateSize > security.Size)
{ {
return CALG_SHA1; return CALG_SHA1;
} }
const PVOID pBase = pPeBytes + securityRva; const PVOID pBase = pPeBytes + securityRva;
const LPWIN_CERTIFICATE pCert = (WIN_CERTIFICATE*)pBase; const ULONG searchLen = certificateSize;
const ULONG searchLen = min(pCert->dwLength, PeSize - securityRva);
PUCHAR pMatch = NULL; PUCHAR pMatch = NULL;
if (NT_SUCCESS(DomitoMemorySearchPattern( if (NT_SUCCESS(DomitoMemorySearchPattern(
@@ -98,121 +103,148 @@ DomitoCalculatePortableExecutableDigest(
_Out_ PULONG pSizeOfSecurityDirectory _Out_ PULONG pSizeOfSecurityDirectory
) )
{ {
PIMAGE_DATA_DIRECTORY pImgDataDirectory; if (!pDigestCalgOut || !pDigestSizeOut || !pDigestOut ||
BOOLEAN is64Bit; !pCertOut || !pSizeOfSecurityDirectory)
PUCHAR pHash = nullptr; {
NTSTATUS status; return STATUS_INVALID_PARAMETER;
BCRYPT_ALG_HANDLE hbAlg = nullptr; }
BCRYPT_HASH_HANDLE hbHash = nullptr;
UINT32 hashLength = 0;
ULONG resultLength;
ULONG fileOffset = 0;
*pDigestCalgOut = 0;
*pDigestSizeOut = 0;
*pDigestOut = nullptr;
*pCertOut = nullptr;
*pSizeOfSecurityDirectory = 0;
if (!pPeBytes)
{
return STATUS_INVALID_PARAMETER;
}
if (PeSize < sizeof(IMAGE_DOS_HEADER)) if (PeSize < sizeof(IMAGE_DOS_HEADER))
{ {
return STATUS_INVALID_IMAGE_NOT_MZ; return STATUS_INVALID_IMAGE_NOT_MZ;
} }
const PIMAGE_DOS_HEADER phDos = (IMAGE_DOS_HEADER*)pPeBytes; // Copy fields rather than assuming the caller's buffer is aligned.
if (phDos->e_magic != IMAGE_DOS_SIGNATURE) IMAGE_DOS_HEADER dos = {};
memcpy(&dos, pPeBytes, sizeof(dos));
if (dos.e_magic != IMAGE_DOS_SIGNATURE)
{ {
// Not an executable
return STATUS_INVALID_IMAGE_NOT_MZ; return STATUS_INVALID_IMAGE_NOT_MZ;
} }
if (dos.e_lfanew < 0)
if (phDos->e_lfanew > PeSize - sizeof(IMAGE_NT_HEADERS))
{ {
return STATUS_INVALID_IMAGE_FORMAT; return STATUS_INVALID_IMAGE_FORMAT;
} }
const PIMAGE_NT_HEADERS phNt = (PIMAGE_NT_HEADERS)(pPeBytes + phDos->e_lfanew); const ULONG ntOffset = (ULONG)dos.e_lfanew;
if (phNt->Signature != IMAGE_NT_SIGNATURE) const ULONG ntPrefixSize = sizeof(ULONG) + sizeof(IMAGE_FILE_HEADER);
if (ntOffset > PeSize || ntPrefixSize > PeSize - ntOffset)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
ULONG signature;
IMAGE_FILE_HEADER fileHeader;
memcpy(&signature, pPeBytes + ntOffset, sizeof(signature));
memcpy(&fileHeader, pPeBytes + ntOffset + sizeof(signature), sizeof(fileHeader));
if (signature != IMAGE_NT_SIGNATURE)
{ {
// Not a PE image
return STATUS_INVALID_IMAGE_FORMAT; return STATUS_INVALID_IMAGE_FORMAT;
} }
// const ULONG optionalOffset = ntOffset + ntPrefixSize;
// Check if PE is 32 or 64 bits const ULONG optionalSize = fileHeader.SizeOfOptionalHeader;
// if (optionalSize < sizeof(USHORT) || optionalSize > PeSize - optionalOffset)
switch (phNt->OptionalHeader.Magic)
{ {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC: return STATUS_INVALID_IMAGE_FORMAT;
is64Bit = FALSE;
break;
case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
is64Bit = TRUE;
break;
default:
return STATUS_INVALID_IMAGE_FORMAT; // Unsupported architecture
} }
USHORT magic;
// memcpy(&magic, pPeBytes + optionalOffset, sizeof(magic));
// TODO: Not sure if 16 * 512 * 512 is right. Do something better! ULONG directoryOffset;
// ULONG checksumOffset;
ULONG copySize = phDos->e_lfanew + sizeof(IMAGE_FILE_HEADER) + 4 + 0x40; ULONG directoryCountOffset;
#pragma warning(disable:4996) if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC)
const PUCHAR pBuf = (PUCHAR)ExAllocatePoolWithTag(
NonPagedPoolNx,
16 * 512 * 512,
DOMITO_POOL_TAG
);
#pragma warning(default:4996)
if (!pBuf)
{ {
return STATUS_INSUFFICIENT_RESOURCES; directoryOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory);
checksumOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, CheckSum);
directoryCountOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, NumberOfRvaAndSizes);
} }
else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
//
// Fetch the data directory from the PE.
// Note that we can also use RtlImageDirectoryEntryToData to fetch
// only the security directory entry but why not do it manually
// when we have the full PE image anyway
//
if (is64Bit == TRUE)
{ {
pImgDataDirectory = ((PIMAGE_NT_HEADERS64)phNt)->OptionalHeader.DataDirectory; directoryOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory);
checksumOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, CheckSum);
directoryCountOffset = FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, NumberOfRvaAndSizes);
} }
else else
{ {
pImgDataDirectory = ((PIMAGE_NT_HEADERS32)phNt)->OptionalHeader.DataDirectory; return STATUS_INVALID_IMAGE_FORMAT;
}
if (optionalSize < directoryOffset)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
ULONG directoryCount;
memcpy(&directoryCount, pPeBytes + optionalOffset + directoryCountOffset, sizeof(directoryCount));
if (directoryCount > (optionalSize - directoryOffset) / sizeof(IMAGE_DATA_DIRECTORY))
{
return STATUS_INVALID_IMAGE_FORMAT;
} }
// checksumOffset += optionalOffset;
// Check if the PE file contains a signature const BOOLEAN hasSecurityEntry = directoryCount > IMAGE_DIRECTORY_ENTRY_SECURITY;
// IMAGE_DATA_DIRECTORY directories[IMAGE_DIRECTORY_ENTRY_SECURITY + 1] = {};
const BOOLEAN hasEmbeddedSig = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress != 0; ULONG securityEntryOffset = PeSize;
ULONG dataOffset = PeSize;
if (hasSecurityEntry)
{
securityEntryOffset = optionalOffset + directoryOffset +
IMAGE_DIRECTORY_ENTRY_SECURITY * sizeof(IMAGE_DATA_DIRECTORY);
memcpy(&directories[IMAGE_DIRECTORY_ENTRY_SECURITY], pPeBytes + securityEntryOffset,
sizeof(IMAGE_DATA_DIRECTORY));
dataOffset = securityEntryOffset + sizeof(IMAGE_DATA_DIRECTORY);
}
const IMAGE_DATA_DIRECTORY security = directories[IMAGE_DIRECTORY_ENTRY_SECURITY];
const BOOLEAN hasEmbeddedSig = security.VirtualAddress != 0;
if (hasEmbeddedSig != (security.Size != 0))
{
return STATUS_INVALID_IMAGE_FORMAT;
}
if (hasEmbeddedSig)
{
// The security directory contains a FILE OFFSET, not an image RVA.
const ULONG certificateHeaderSize = FIELD_OFFSET(WIN_CERTIFICATE, bCertificate);
if (security.VirtualAddress < dataOffset || security.VirtualAddress > PeSize ||
security.Size > PeSize - security.VirtualAddress ||
security.Size < certificateHeaderSize)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
ULONG certificateSize;
memcpy(&certificateSize, pPeBytes + security.VirtualAddress, sizeof(certificateSize));
if (certificateSize < certificateHeaderSize || certificateSize > security.Size)
{
return STATUS_INVALID_IMAGE_FORMAT;
}
}
// PUCHAR pHash = nullptr;
// Boring bcrypt boilerplate code BCRYPT_ALG_HANDLE hbAlg = nullptr;
// BCRYPT_HASH_HANDLE hbHash = nullptr;
const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, pImgDataDirectory); ULONG hashLength = 0;
ULONG resultLength = 0;
if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider( const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories);
&hbAlg, NTSTATUS status = BCryptOpenAlgorithmProvider(
DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), // bcrypt doesn't understand standard WinCrypt CALG IDs &hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0);
MS_PRIMITIVE_PROVIDER, if (!NT_SUCCESS(status))
0 {
))) goto cleanup;
}
status = BCryptGetProperty(hbAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength,
sizeof(hashLength), &resultLength, 0);
if (!NT_SUCCESS(status))
{ {
goto cleanup; goto cleanup;
} }
if (!NT_SUCCESS(status = BCryptGetProperty(
hbAlg,
BCRYPT_HASH_LENGTH,
(PUCHAR)&hashLength, // put the length of the hash into hashLength
sizeof(hashLength),
&resultLength,
0
)))
{
goto cleanup;
}
//
// Allocate a buffer to store the resulting hash
//
#pragma warning(disable:4996) #pragma warning(disable:4996)
pHash = (PUCHAR)ExAllocatePoolWithTag(NonPagedPoolNx, hashLength, DOMITO_POOL_TAG); pHash = (PUCHAR)ExAllocatePoolWithTag(NonPagedPoolNx, hashLength, DOMITO_POOL_TAG);
#pragma warning(default:4996) #pragma warning(default:4996)
@@ -221,179 +253,68 @@ DomitoCalculatePortableExecutableDigest(
status = STATUS_INSUFFICIENT_RESOURCES; status = STATUS_INSUFFICIENT_RESOURCES;
goto cleanup; goto cleanup;
} }
RtlZeroMemory(pHash, hashLength); status = BCryptCreateHash(hbAlg, &hbHash, nullptr, 0, nullptr, 0, 0);
//
// Create a handle to the resulting hash
//
if (!NT_SUCCESS(status = BCryptCreateHash(
hbAlg,
&hbHash,
NULL,
0,
NULL,
0,
0
)))
{
goto cleanup;
}
//
// Ok, if we haven't BSODed yet, we're ready to continue
// parsing the PE/COFF and hash the needed values
//
memcpy(pBuf, pPeBytes, copySize);
fileOffset += copySize;
//
// Hash everything up to the Checksum then skip it
//
status = BCryptHashData(
hbHash,
pBuf,
copySize,
0
);
if (!NT_SUCCESS(status)) if (!NT_SUCCESS(status))
{ {
goto cleanup; goto cleanup;
} }
fileOffset += 4; // Skipping the checksum field here // Keep the existing linear hashing convention, but hash validated ranges
// directly: no fixed-size scratch buffer and no unchecked memcpy lengths.
// // General section-ordered Authenticode hashing is a separate change.
// Reach the security directory information. status = BCryptHashData(hbHash, pPeBytes, checksumOffset, 0);
// For x64 PEs it's 10 bytes further if (!NT_SUCCESS(status))
//
copySize = 0x3C;
if (is64Bit == TRUE)
{ {
copySize += 0x10; goto cleanup;
} }
status = BCryptHashData(hbHash, pPeBytes + checksumOffset + sizeof(ULONG),
memcpy(pBuf, pPeBytes + fileOffset, copySize); securityEntryOffset - checksumOffset - sizeof(ULONG), 0);
fileOffset += copySize; if (!NT_SUCCESS(status))
{
// goto cleanup;
// Again, hash everything up to here then skip the ignored field. }
// if (hasSecurityEntry)
status = BCryptHashData( {
hbHash, status = BCryptHashData(hbHash, pPeBytes + dataOffset,
pBuf, (hasEmbeddedSig ? security.VirtualAddress : PeSize) - dataOffset, 0);
copySize, if (!NT_SUCCESS(status))
0 {
); goto cleanup;
}
}
status = BCryptFinishHash(hbHash, pHash, hashLength, 0);
if (!NT_SUCCESS(status)) if (!NT_SUCCESS(status))
{ {
goto cleanup; goto cleanup;
} }
fileOffset += 8; // Skipping an other ignored field here
//
// Now hash everything else in the file up to the certificate data.
//
ULONG remaining;
if (hasEmbeddedSig)
{
const ULONG secRva = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress;
if (secRva < fileOffset)
{
status = STATUS_INVALID_IMAGE_FORMAT;
goto cleanup;
}
remaining = secRva - fileOffset;
}
else
{
remaining = (fileOffset <= PeSize) ? (PeSize - fileOffset) : 0;
}
while (remaining > 0)
{
const ULONG chunkSize = min(remaining, 4096);
ULONG readBytes;
memset(pBuf, 0, chunkSize);
if (fileOffset + chunkSize > PeSize)
{
readBytes = PeSize - chunkSize;
memcpy(pBuf, pPeBytes + fileOffset, readBytes);
fileOffset += readBytes;
break;
}
else
{
memcpy(pBuf, pPeBytes + fileOffset, chunkSize);
readBytes = chunkSize;
fileOffset += readBytes;
}
if (!NT_SUCCESS(status = BCryptHashData(
hbHash,
pBuf,
readBytes,
0
)))
{
goto cleanup;
}
remaining -= readBytes;
}
//
// Finish up the hash here
//
if (!NT_SUCCESS(status = BCryptFinishHash(
hbHash,
pHash,
hashLength,
0
)))
{
goto cleanup;
}
//
// Supply the results to the caller
//
*pDigestCalgOut = peDigestKind; *pDigestCalgOut = peDigestKind;
*pDigestSizeOut = hashLength; *pDigestSizeOut = hashLength;
*pDigestOut = pHash; *pDigestOut = pHash;
if (hasEmbeddedSig == TRUE) pHash = nullptr; // Transfer ownership only after all work has succeeded.
if (hasEmbeddedSig)
{ {
*pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress); *pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + security.VirtualAddress);
*pSizeOfSecurityDirectory = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size; *pSizeOfSecurityDirectory = security.Size;
} }
else
{
*pCertOut = NULL;
*pSizeOfSecurityDirectory = 0;
}
status = STATUS_SUCCESS;
cleanup: cleanup:
if (pBuf)
{
ExFreePoolWithTag(pBuf, DOMITO_POOL_TAG);
}
if (hbHash) if (hbHash)
{ {
BCryptDestroyHash(hbHash); BCryptDestroyHash(hbHash);
} }
if (pHash)
{
ExFreePoolWithTag(pHash, DOMITO_POOL_TAG);
}
if (hbAlg) if (hbAlg)
{ {
BCryptCloseAlgorithmProvider(hbAlg, 0); BCryptCloseAlgorithmProvider(hbAlg, 0);
} }
return status; return status;
} }
_IRQL_requires_max_(DISPATCH_LEVEL) _IRQL_requires_max_(DISPATCH_LEVEL)
void void
DomitoFreePortableExecutableDigest( DomitoFreePortableExecutableDigest(
@@ -493,6 +414,12 @@ DomitoValidateFileLegacyMode(
break; break;
} }
if (ioStatusBlock.Information != SecurityDirectory->Size)
{
status = STATUS_END_OF_FILE;
break;
}
KeStackAttachProcess(PsInitialSystemProcess, &systemContext); KeStackAttachProcess(PsInitialSystemProcess, &systemContext);
{ {
status = CiCheckSignedFile( status = CiCheckSignedFile(
+23
View File
@@ -0,0 +1,23 @@
# Code Integrity regression tests
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
```powershell
python tests\run-digest-tests.py
```
The runner extracts the production digest, pattern-search, algorithm-mapping,
and legacy-validation functions without changing their bodies. The C++ harness
uses real user-mode BCrypt hashing and test doubles for pool allocation, file
reads, and CI. No driver is installed or loaded. Build outputs are temporary.
Coverage includes PE32/PE32+ digest fixtures, checksum and certificate exclusions,
a DOS stub larger than 4 MiB, truncated headers next to inaccessible guard pages,
invalid offsets and lengths, absent security-directory entries, injected hashing
and allocation failures, deterministic failure outputs, allocation ownership,
and rejection of successful-but-short certificate reads before invoking CI.
The tests do not validate CI trust policy, asynchronous kernel I/O behavior, or
general Authenticode hashing of unusual section layouts. The digest implementation
still uses its existing linear byte-order convention and heuristic signature
algorithm detection; replacing those requires a separate compatibility corpus.
+259
View File
@@ -0,0 +1,259 @@
// User-mode regression harness. The runner extracts the production functions
// unchanged; pool, file-I/O, and CI calls are test doubles. BCrypt is real CNG.
#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS
#include <ntstatus.h>
#include <bcrypt.h>
#include <wintrust.h>
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <cstring>
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
#define ASSERT(x) Check(!!(x), #x)
#define DOMITO_POOL_TAG 0x446f6d69
#define NonPagedPoolNx 0
#define PagedPool 1
#define PAGED_CODE() ((void)0)
#define MINCRYPT_SHA1_LENGTH 20
#define MINCRYPT_SHA256_LENGTH 32
typedef const UCHAR* PCUCHAR;
static unsigned checks, allocations, ciCalls, hashCalls;
static int failOperation, failHashCall;
static ULONG_PTR bytesRead;
static void Check(bool ok, const char* message)
{
++checks;
if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); }
}
static PVOID ExAllocatePoolWithTag(int, SIZE_T size, ULONG)
{
if (failOperation == 4) return nullptr;
void* p = HeapAlloc(GetProcessHeap(), 0, size);
if (p) ++allocations;
return p;
}
static void ExFreePoolWithTag(PVOID p, ULONG)
{
Check(p != nullptr && allocations != 0, "allocation ownership");
--allocations;
Check(HeapFree(GetProcessHeap(), 0, p) != FALSE, "heap free");
}
static NTSTATUS TestCreateHash(BCRYPT_ALG_HANDLE a, BCRYPT_HASH_HANDLE* h,
PUCHAR o, ULONG os, PUCHAR s, ULONG ss, ULONG f)
{
if (failOperation == 1) return STATUS_UNSUCCESSFUL;
return BCryptCreateHash(a, h, o, os, s, ss, f);
}
static NTSTATUS TestHashData(BCRYPT_HASH_HANDLE h, PUCHAR b, ULONG n, ULONG f)
{
++hashCalls;
if (failOperation == 2 && hashCalls == (unsigned)failHashCall) return STATUS_UNSUCCESSFUL;
return BCryptHashData(h, b, n, f);
}
static NTSTATUS TestFinishHash(BCRYPT_HASH_HANDLE h, PUCHAR b, ULONG n, ULONG f)
{
if (failOperation == 3) return STATUS_UNSUCCESSFUL;
return BCryptFinishHash(h, b, n, f);
}
// Minimal dependencies for the legacy wrapper; no actual CI code is invoked.
struct MINCRYPT_POLICY_INFO { ULONG Size; };
struct KAPC_STATE { ULONG unused; };
struct IO_STATUS_BLOCK { NTSTATUS Status; ULONG_PTR Information; };
static PVOID PsInitialSystemProcess = nullptr;
static void CiFreePolicyInfo(MINCRYPT_POLICY_INFO* p) { p->Size = 0; }
static void KeStackAttachProcess(PVOID, KAPC_STATE*) {}
static void KeUnstackDetachProcess(KAPC_STATE*) {}
static NTSTATUS ZwReadFile(HANDLE, PVOID, PVOID, PVOID, IO_STATUS_BLOCK* io,
PVOID buffer, ULONG size, LARGE_INTEGER*, PVOID)
{
io->Status = STATUS_SUCCESS;
io->Information = bytesRead;
std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size));
return STATUS_SUCCESS;
}
static NTSTATUS ZwWaitForSingleObject(HANDLE, BOOLEAN, PVOID) { return STATUS_SUCCESS; }
static NTSTATUS CiCheckSignedFile(PVOID, UINT32, ALG_ID, PVOID, UINT32,
MINCRYPT_POLICY_INFO*, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*)
{
++ciCalls;
return STATUS_SUCCESS;
}
static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN, UINT32, UINT32,
MINCRYPT_POLICY_INFO*, PVOID, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*)
{
++ciCalls;
return STATUS_SUCCESS;
}
#define BCryptCreateHash TestCreateHash
#define BCryptHashData TestHashData
#define BCryptFinishHash TestFinishHash
#include "digest-under-test.inc"
#undef BCryptCreateHash
#undef BCryptHashData
#undef BCryptFinishHash
static std::vector<UCHAR> Image(bool pe64, ULONG nt = 128, bool certificate = false)
{
std::vector<UCHAR> b((size_t)nt + 896 + (certificate ? 32 : 0), 0);
IMAGE_DOS_HEADER dos = {};
dos.e_magic = IMAGE_DOS_SIGNATURE; dos.e_lfanew = (LONG)nt;
std::memcpy(b.data(), &dos, sizeof(dos));
ULONG signature = IMAGE_NT_SIGNATURE;
std::memcpy(b.data() + nt, &signature, sizeof(signature));
IMAGE_FILE_HEADER f = {};
f.NumberOfSections = 1;
f.SizeOfOptionalHeader = pe64 ? sizeof(IMAGE_OPTIONAL_HEADER64) : sizeof(IMAGE_OPTIONAL_HEADER32);
std::memcpy(b.data() + nt + 4, &f, sizeof(f));
IMAGE_OPTIONAL_HEADER64 h64 = {};
IMAGE_OPTIONAL_HEADER32 h32 = {};
h64.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC; h32.Magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
h64.NumberOfRvaAndSizes = h32.NumberOfRvaAndSizes = 16;
h64.SizeOfHeaders = h32.SizeOfHeaders = nt + 384;
if (certificate)
{
h64.DataDirectory[4] = h32.DataDirectory[4] = { nt + 896, 32 };
ULONG length = 32;
std::memcpy(b.data() + nt + 896, &length, sizeof(length));
}
std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader);
IMAGE_SECTION_HEADER section = {};
section.PointerToRawData = nt + 384; section.SizeOfRawData = 512;
std::memcpy(b.data() + nt + 24 + f.SizeOfOptionalHeader, &section, sizeof(section));
for (ULONG i = nt + 384; i < nt + 896; ++i) b[i] = (UCHAR)(i * 31);
return b;
}
static void Set32(std::vector<UCHAR>& b, size_t at, ULONG value)
{
std::memcpy(b.data() + at, &value, sizeof(value));
}
static void Set16(std::vector<UCHAR>& b, size_t at, USHORT value)
{
std::memcpy(b.data() + at, &value, sizeof(value));
}
static std::vector<UCHAR> Digest(PUCHAR b, ULONG size, NTSTATUS expected = STATUS_SUCCESS)
{
UINT32 algorithm = 0xdead;
ULONG length = 0xdead, certSize = 0xdead;
PVOID digest = (void*)1;
LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1;
hashCalls = 0;
NTSTATUS s = DomitoCalculatePortableExecutableDigest(b, size, &algorithm, &length,
&digest, &cert, &certSize);
Check(s == expected, "digest status");
std::vector<UCHAR> result;
if (NT_SUCCESS(s))
{
Check(algorithm == CALG_SHA1 && length == 20 && digest != nullptr, "SHA1 output");
Check((cert == nullptr) == (certSize == 0), "certificate outputs agree");
if (cert) Check((PUCHAR)cert >= b && (PUCHAR)cert + certSize <= b + size, "borrowed certificate span");
result.assign((PUCHAR)digest, (PUCHAR)digest + length);
DomitoFreePortableExecutableDigest(digest);
}
else
{
Check(!algorithm && !length && !digest && !cert && !certSize, "failure outputs cleared");
}
Check(allocations == 0, "no leaked pool allocations");
return result;
}
static std::vector<UCHAR> Reference(const std::vector<UCHAR>& b, ULONG checksum,
ULONG securitySlot, ULONG end)
{
// Explicit fixture ranges, independent of the production parser.
std::vector<UCHAR> bytes(b.begin(), b.begin() + checksum);
bytes.insert(bytes.end(), b.begin() + checksum + 4, b.begin() + securitySlot);
if (securitySlot != end) bytes.insert(bytes.end(), b.begin() + securitySlot + 8, b.begin() + end);
std::vector<UCHAR> digest(20);
BCRYPT_ALG_HANDLE algorithm = nullptr;
Check(NT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA1_ALGORITHM, nullptr, 0)), "reference algorithm");
Check(NT_SUCCESS(BCryptHash(algorithm, nullptr, 0, bytes.data(), (ULONG)bytes.size(), digest.data(), 20)), "reference digest");
BCryptCloseAlgorithmProvider(algorithm, 0);
return digest;
}
static void GuardedInvalid(const std::vector<UCHAR>& b, NTSTATUS expected)
{
SYSTEM_INFO si; GetSystemInfo(&si);
const SIZE_T span = ((b.size() + si.dwPageSize - 1) / si.dwPageSize) * si.dwPageSize;
UCHAR* region = (UCHAR*)VirtualAlloc(nullptr, span + si.dwPageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
Check(region != nullptr, "guard allocation");
DWORD old;
Check(VirtualProtect(region + span, si.dwPageSize, PAGE_NOACCESS, &old) != FALSE, "guard protection");
UCHAR* input = region + span - b.size();
std::memcpy(input, b.data(), b.size());
Digest(input, (ULONG)b.size(), expected);
VirtualFree(region, 0, MEM_RELEASE);
}
int main()
{
for (bool pe64 : {false, true})
{
for (bool certificate : {false, true})
{
auto b = Image(pe64, 128, certificate);
auto result = Digest(b.data(), (ULONG)b.size());
Check(result == Reference(b, 216, pe64 ? 296 : 280, 1024), "fixture digest equals independent CNG reference");
Set32(b, 216, 0x12345678);
Check(result == Digest(b.data(), (ULONG)b.size()), "checksum excluded");
if (certificate)
{
b.back() ^= 0x55;
Check(result == Digest(b.data(), (ULONG)b.size()), "certificate contents excluded");
}
}
}
auto large = Image(true, 4 * 1024 * 1024);
Check(Digest(large.data(), (ULONG)large.size()) == Reference(large, 4194392, 4194472, (ULONG)large.size()), "large DOS stub");
auto b = Image(true);
for (ULONG size = 0; size < 392; ++size)
{
std::vector<UCHAR> prefix(b.begin(), b.begin() + size);
if (size == 0) Digest(b.data(), 0, STATUS_INVALID_IMAGE_NOT_MZ);
else GuardedInvalid(prefix, size < 64 ? STATUS_INVALID_IMAGE_NOT_MZ : STATUS_INVALID_IMAGE_FORMAT);
}
auto bad = b; Set32(bad, 60, 0xffffffff); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = b; Set32(bad, 60, 0x7fffffff); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = b; Set16(bad, 148, 1); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = b; Set16(bad, 152, 0x999); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = b; Set32(bad, 260, 17); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
for (ULONG offset : {200UL, 2048UL, 0xffffffffUL})
{
bad = b; Set32(bad, 296, offset); Set32(bad, 300, 32); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
}
bad = b; Set32(bad, 300, 32); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = b; Set32(bad, 296, 800); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = b; Set32(bad, 296, 800); Set32(bad, 300, 0xffffffff); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = Image(true, 128, true); Set32(bad, 1024, 33); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = Image(true, 128, true); Set32(bad, 1024, 7); GuardedInvalid(bad, STATUS_INVALID_IMAGE_FORMAT);
bad = Image(true); Set32(bad, 260, 0); // No directory entry: don't exclude its physical bytes.
Check(Digest(bad.data(), (ULONG)bad.size()) == Reference(bad, 216, 1024, 1024), "absent security directory");
for (int failure : {1, 2, 3, 4})
{
failOperation = failure;
for (int call = 1; call <= (failure == 2 ? 3 : 1); ++call)
{
failHashCall = call;
Digest(b.data(), (ULONG)b.size(), failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL);
}
}
failOperation = 0;
Digest(nullptr, 1024, STATUS_INVALID_PARAMETER);
IMAGE_DATA_DIRECTORY directory = {1024, 32};
MINCRYPT_POLICY_INFO policy = {}, timestamp = {};
LARGE_INTEGER signing = {};
UCHAR hash[20] = {};
bytesRead = 16; ciCalls = 0;
Check(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory,
&policy, &signing, &timestamp) == STATUS_END_OF_FILE, "short certificate read rejected");
Check(ciCalls == 0 && allocations == 0, "short read does not reach CI or leak");
bytesRead = 32;
Check(NT_SUCCESS(DomitoValidateFileLegacyMode(nullptr, hash, 20, CALG_SHA1, &directory,
&policy, &signing, &timestamp)), "complete certificate read accepted");
Check(ciCalls == 1 && allocations == 0, "complete read reaches CI once");
std::printf("PASS: %u checks (guarded malformed inputs, PE32/PE32+, CNG digests, failure cleanup, short reads).\n", checks);
}
+34
View File
@@ -0,0 +1,34 @@
"""Run in an x64 VS developer shell with Windows SDK and Python 3 installed.
Extracts the actual production functions without changing their bodies, replacing
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)
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('))
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('))
algorithm = public[start:end]
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'], cwd=build, check=True)
subprocess.run([str(build/'digest-tests.exe')], cwd=build, check=True)