diff --git a/include/Domito.MinCrypt.h b/include/Domito.MinCrypt.h index 63d1596..4c8b32b 100644 --- a/include/Domito.MinCrypt.h +++ b/include/Domito.MinCrypt.h @@ -352,7 +352,7 @@ typedef struct _MINCRYPT_POLICY_INFO #endif // NTDDI_VERSION >= NTDDI_WIN10 -}MINCRYPT_POLICY_INFO, PMINCRYPT_POLICY_INFO; +}MINCRYPT_POLICY_INFO, *PMINCRYPT_POLICY_INFO; #include @@ -491,7 +491,10 @@ typedef MINCRYPT_ALLOCATE_ROUTINE* PMINCRYPT_ALLOCATE_ROUTINE; * * @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 NTSTATUS diff --git a/include/Domito.h b/include/Domito.h index 68d22f4..4ada98c 100644 --- a/include/Domito.h +++ b/include/Domito.h @@ -170,8 +170,10 @@ DOMITO_CALG_TO_BCRYPT_ALGORITHM( */ // -// Extracts the CALG_ID from a signed PE that was used to -// calculate the message digest when it was signed +// Heuristically detects the CALG_ID in the first WIN_CERTIFICATE using byte patterns. +// 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) 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) _Must_inspect_result_ diff --git a/src/Domito.CodeIntegrity.cpp b/src/Domito.CodeIntegrity.cpp index 8a22b33..c0c2d02 100644 --- a/src/Domito.CodeIntegrity.cpp +++ b/src/Domito.CodeIntegrity.cpp @@ -24,21 +24,26 @@ DomitoGetPortableExecutableDigestKind( _In_ PIMAGE_DATA_DIRECTORY pImgDataDirectory ) { - const ULONG securityRva = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress; - - if (!securityRva) + if (!pPeBytes || !pImgDataDirectory) { return CALG_SHA1; } - - if (securityRva > PeSize || (PeSize - securityRva) < sizeof(WIN_CERTIFICATE)) + const IMAGE_DATA_DIRECTORY security = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY]; + 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; } - const PVOID pBase = pPeBytes + securityRva; - const LPWIN_CERTIFICATE pCert = (WIN_CERTIFICATE*)pBase; - const ULONG searchLen = min(pCert->dwLength, PeSize - securityRva); + const ULONG searchLen = certificateSize; PUCHAR pMatch = NULL; if (NT_SUCCESS(DomitoMemorySearchPattern( @@ -98,121 +103,148 @@ DomitoCalculatePortableExecutableDigest( _Out_ PULONG pSizeOfSecurityDirectory ) { - PIMAGE_DATA_DIRECTORY pImgDataDirectory; - BOOLEAN is64Bit; - PUCHAR pHash = nullptr; - NTSTATUS status; - BCRYPT_ALG_HANDLE hbAlg = nullptr; - BCRYPT_HASH_HANDLE hbHash = nullptr; - UINT32 hashLength = 0; - ULONG resultLength; - ULONG fileOffset = 0; + if (!pDigestCalgOut || !pDigestSizeOut || !pDigestOut || + !pCertOut || !pSizeOfSecurityDirectory) + { + return STATUS_INVALID_PARAMETER; + } + *pDigestCalgOut = 0; + *pDigestSizeOut = 0; + *pDigestOut = nullptr; + *pCertOut = nullptr; + *pSizeOfSecurityDirectory = 0; + + if (!pPeBytes) + { + return STATUS_INVALID_PARAMETER; + } if (PeSize < sizeof(IMAGE_DOS_HEADER)) { return STATUS_INVALID_IMAGE_NOT_MZ; } - const PIMAGE_DOS_HEADER phDos = (IMAGE_DOS_HEADER*)pPeBytes; - if (phDos->e_magic != IMAGE_DOS_SIGNATURE) + // Copy fields rather than assuming the caller's buffer is aligned. + 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; } - - if (phDos->e_lfanew > PeSize - sizeof(IMAGE_NT_HEADERS)) + if (dos.e_lfanew < 0) { return STATUS_INVALID_IMAGE_FORMAT; } - const PIMAGE_NT_HEADERS phNt = (PIMAGE_NT_HEADERS)(pPeBytes + phDos->e_lfanew); - if (phNt->Signature != IMAGE_NT_SIGNATURE) + const ULONG ntOffset = (ULONG)dos.e_lfanew; + 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; } - // - // Check if PE is 32 or 64 bits - // - switch (phNt->OptionalHeader.Magic) + const ULONG optionalOffset = ntOffset + ntPrefixSize; + const ULONG optionalSize = fileHeader.SizeOfOptionalHeader; + if (optionalSize < sizeof(USHORT) || optionalSize > PeSize - optionalOffset) { - case IMAGE_NT_OPTIONAL_HDR32_MAGIC: - is64Bit = FALSE; - break; - case IMAGE_NT_OPTIONAL_HDR64_MAGIC: - is64Bit = TRUE; - break; - default: - return STATUS_INVALID_IMAGE_FORMAT; // Unsupported architecture + return STATUS_INVALID_IMAGE_FORMAT; } - - // - // TODO: Not sure if 16 * 512 * 512 is right. Do something better! - // - ULONG copySize = phDos->e_lfanew + sizeof(IMAGE_FILE_HEADER) + 4 + 0x40; -#pragma warning(disable:4996) - const PUCHAR pBuf = (PUCHAR)ExAllocatePoolWithTag( - NonPagedPoolNx, - 16 * 512 * 512, - DOMITO_POOL_TAG - ); -#pragma warning(default:4996) - if (!pBuf) + USHORT magic; + memcpy(&magic, pPeBytes + optionalOffset, sizeof(magic)); + ULONG directoryOffset; + ULONG checksumOffset; + ULONG directoryCountOffset; + if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { - 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); } - - // - // 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) + else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { - 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 { - 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; } - // - // Check if the PE file contains a signature - // - const BOOLEAN hasEmbeddedSig = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress != 0; + checksumOffset += optionalOffset; + const BOOLEAN hasSecurityEntry = directoryCount > IMAGE_DIRECTORY_ENTRY_SECURITY; + IMAGE_DATA_DIRECTORY directories[IMAGE_DIRECTORY_ENTRY_SECURITY + 1] = {}; + 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; + } + } - // - // Boring bcrypt boilerplate code - // - const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, pImgDataDirectory); - - if (!NT_SUCCESS(status = BCryptOpenAlgorithmProvider( - &hbAlg, - DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), // bcrypt doesn't understand standard WinCrypt CALG IDs - MS_PRIMITIVE_PROVIDER, - 0 - ))) + PUCHAR pHash = nullptr; + BCRYPT_ALG_HANDLE hbAlg = nullptr; + BCRYPT_HASH_HANDLE hbHash = nullptr; + ULONG hashLength = 0; + ULONG resultLength = 0; + const UINT32 peDigestKind = DomitoGetPortableExecutableDigestKind(pPeBytes, PeSize, directories); + NTSTATUS status = BCryptOpenAlgorithmProvider( + &hbAlg, DOMITO_CALG_TO_BCRYPT_ALGORITHM(peDigestKind), MS_PRIMITIVE_PROVIDER, 0); + if (!NT_SUCCESS(status)) + { + goto cleanup; + } + status = BCryptGetProperty(hbAlg, BCRYPT_HASH_LENGTH, (PUCHAR)&hashLength, + sizeof(hashLength), &resultLength, 0); + if (!NT_SUCCESS(status)) { 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) pHash = (PUCHAR)ExAllocatePoolWithTag(NonPagedPoolNx, hashLength, DOMITO_POOL_TAG); #pragma warning(default:4996) @@ -221,179 +253,68 @@ DomitoCalculatePortableExecutableDigest( status = STATUS_INSUFFICIENT_RESOURCES; goto cleanup; } - RtlZeroMemory(pHash, hashLength); - - // - // 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 - ); + status = BCryptCreateHash(hbAlg, &hbHash, nullptr, 0, nullptr, 0, 0); if (!NT_SUCCESS(status)) { goto cleanup; } - fileOffset += 4; // Skipping the checksum field here - - // - // Reach the security directory information. - // For x64 PEs it's 10 bytes further - // - copySize = 0x3C; - if (is64Bit == TRUE) + // 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. + status = BCryptHashData(hbHash, pPeBytes, checksumOffset, 0); + if (!NT_SUCCESS(status)) { - copySize += 0x10; + goto cleanup; } - - memcpy(pBuf, pPeBytes + fileOffset, copySize); - fileOffset += copySize; - - // - // Again, hash everything up to here then skip the ignored field. - // - status = BCryptHashData( - hbHash, - pBuf, - copySize, - 0 - ); + status = BCryptHashData(hbHash, pPeBytes + checksumOffset + sizeof(ULONG), + securityEntryOffset - checksumOffset - sizeof(ULONG), 0); + if (!NT_SUCCESS(status)) + { + goto cleanup; + } + if (hasSecurityEntry) + { + status = BCryptHashData(hbHash, pPeBytes + dataOffset, + (hasEmbeddedSig ? security.VirtualAddress : PeSize) - dataOffset, 0); + if (!NT_SUCCESS(status)) + { + goto cleanup; + } + } + status = BCryptFinishHash(hbHash, pHash, hashLength, 0); if (!NT_SUCCESS(status)) { 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; *pDigestSizeOut = hashLength; *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); - *pSizeOfSecurityDirectory = pImgDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].Size; + *pCertOut = (LPWIN_CERTIFICATE)(pPeBytes + security.VirtualAddress); + *pSizeOfSecurityDirectory = security.Size; } - else - { - *pCertOut = NULL; - *pSizeOfSecurityDirectory = 0; - } - - status = STATUS_SUCCESS; cleanup: - if (pBuf) - { - ExFreePoolWithTag(pBuf, DOMITO_POOL_TAG); - } - if (hbHash) { BCryptDestroyHash(hbHash); } - + if (pHash) + { + ExFreePoolWithTag(pHash, DOMITO_POOL_TAG); + } if (hbAlg) { BCryptCloseAlgorithmProvider(hbAlg, 0); } - return status; } + _IRQL_requires_max_(DISPATCH_LEVEL) void DomitoFreePortableExecutableDigest( @@ -493,6 +414,12 @@ DomitoValidateFileLegacyMode( break; } + if (ioStatusBlock.Information != SecurityDirectory->Size) + { + status = STATUS_END_OF_FILE; + break; + } + KeStackAttachProcess(PsInitialSystemProcess, &systemContext); { status = CiCheckSignedFile( diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..75d18f9 --- /dev/null +++ b/tests/README.md @@ -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. diff --git a/tests/digest-tests.cpp b/tests/digest-tests.cpp new file mode 100644 index 0000000..1a85994 --- /dev/null +++ b/tests/digest-tests.cpp @@ -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 +#undef WIN32_NO_STATUS +#include +#include +#include +#include +#include +#include +#include + +#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 Image(bool pe64, ULONG nt = 128, bool certificate = false) +{ + std::vector 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, §ion, sizeof(section)); + for (ULONG i = nt + 384; i < nt + 896; ++i) b[i] = (UCHAR)(i * 31); + return b; +} +static void Set32(std::vector& b, size_t at, ULONG value) +{ + std::memcpy(b.data() + at, &value, sizeof(value)); +} +static void Set16(std::vector& b, size_t at, USHORT value) +{ + std::memcpy(b.data() + at, &value, sizeof(value)); +} +static std::vector 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 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 Reference(const std::vector& b, ULONG checksum, + ULONG securitySlot, ULONG end) +{ + // Explicit fixture ranges, independent of the production parser. + std::vector 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 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& 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 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, ×tamp) == 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, ×tamp)), "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); +} diff --git a/tests/run-digest-tests.py b/tests/run-digest-tests.py new file mode 100644 index 0000000..0a4eac9 --- /dev/null +++ b/tests/run-digest-tests.py @@ -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)