Add explicit Authenticode workflows and correct CI helper contracts
This commit is contained in:
1 parent
1026c48324
commit
7aa23f50be
18 files changed
+1952
-185
No files matched your search
+49
-17
@@ -1,30 +1,62 @@
|
||||
# Code Integrity regression tests
|
||||
# Regression tests
|
||||
|
||||
From an **x64 Visual Studio developer shell**, with the Windows SDK and Python 3:
|
||||
|
||||
```powershell
|
||||
python tests\run-digest-tests.py
|
||||
python tests\run-common-tests.py
|
||||
python tests\run-ci-probe-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.
|
||||
Build outputs are temporary. No driver is installed or loaded. Test runners
|
||||
extract the actual production functions or include the probe body unchanged;
|
||||
kernel calls, CI calls, and pool allocation are replaced with test doubles.
|
||||
The digest harness uses real BCrypt, Crypt32 and the Windows PE SIP provider.
|
||||
|
||||
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.
|
||||
## Digest and legacy verification tests
|
||||
|
||||
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.
|
||||
Coverage includes PE32/PE32+ files, all four supported hash algorithms, checksum
|
||||
and certificate exclusions, overlays, reverse section-header order, multiple
|
||||
certificate entries, certificate padding, unsupported section layouts, bounded
|
||||
DER algorithm discovery, SHA-1 pattern decoys, truncated ASN.1, explicit catalog
|
||||
policy forwarding and cleanup between failed/successful CI attempts.
|
||||
|
||||
The test compares digests against `CryptSIPCreateIndirectData` and reads the
|
||||
installed system `ci.dll`, `ntdll.dll`, `kernel32.dll`, and `notepad.exe` as a
|
||||
real-file corpus. For embedded signatures, Crypt32 independently decodes the SPC
|
||||
algorithm and expected file digest. These checks do not verify signature trust.
|
||||
|
||||
Guard pages cover truncated headers and invalid ranges in both compatibility
|
||||
and strict paths. Fault injection covers allocation/hash failures, deterministic
|
||||
outputs, short certificate reads and digest ownership. A DOS stub larger than
|
||||
4 MiB exercises the removed scratch-buffer limit. Older entry points retain
|
||||
legacy selection/layout behavior; strict Ex APIs enforce their documented subset.
|
||||
|
||||
## File and export helper tests
|
||||
|
||||
The common harness checks explicit and zero offsets, exact/short/failed reads,
|
||||
pending completion, event/object cleanup, and error propagation. Export fixtures
|
||||
cover nonidentity name-ordinal mappings, forwarders, empty slots, invalid spans,
|
||||
unbounded strings, and deterministic failure outputs. The export API requires a
|
||||
valid loader-mapped module; it is not an arbitrary file parser.
|
||||
|
||||
## File-object validation flag probe
|
||||
|
||||
See [the flag documentation and VM procedure](../docs/ci-validate-file-object.md)
|
||||
for `CiValidateFileObject` flag `0x20`. Run `python tests\run-ci-probe-tests.py`
|
||||
from the same developer shell to test probe plumbing with mock CI. The kernel
|
||||
probe must still be run in a suitable VM to verify actual policy behavior.
|
||||
for `CiValidateFileObject` flag `0x20`. The host runner tests probe plumbing with
|
||||
mock CI. The kernel probe must still run in a suitable VM to verify actual
|
||||
policy behavior.
|
||||
|
||||
## Kernel compile checks and remaining validation
|
||||
|
||||
`mincrypt-layout-tests.cpp` asserts default modern x64/ARM64 and pre-Windows-10
|
||||
compile layouts. On x64 it also includes the opt-in observed 26100 header's 14
|
||||
layout assertions. Compile it with the same WDK/kernel architecture flags and
|
||||
include paths as the library. This checks structure definitions, not runtime
|
||||
compatibility on those systems. `ci-file-object-probe.cpp` is separately compiled
|
||||
as kernel code for x64/ARM64; its real CI behavior is still untested.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,152 @@
|
||||
// Tests extract the unchanged production bodies. Kernel I/O is simulated.
|
||||
#define WIN32_NO_STATUS
|
||||
#include <windows.h>
|
||||
#undef WIN32_NO_STATUS
|
||||
#include <ntstatus.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <cctype>
|
||||
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
||||
#define MAXUSHORT 0xffff
|
||||
#define Executive 0
|
||||
#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; };
|
||||
typedef ANSI_STRING* PANSI_STRING;
|
||||
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) {
|
||||
++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;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static void* exportPointer;
|
||||
static ULONG exportBytes;
|
||||
static PVOID Directory(PVOID, BOOLEAN mapped, USHORT index, PULONG bytes) {
|
||||
Check(mapped && index == IMAGE_DIRECTORY_ENTRY_EXPORT, "mapped export lookup");
|
||||
*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;
|
||||
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;
|
||||
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");
|
||||
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() {
|
||||
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 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};
|
||||
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);
|
||||
return image;
|
||||
}
|
||||
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;
|
||||
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");
|
||||
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");
|
||||
}
|
||||
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);
|
||||
}
|
||||
+394
-14
@@ -6,10 +6,12 @@
|
||||
#include <ntstatus.h>
|
||||
#include <bcrypt.h>
|
||||
#include <wintrust.h>
|
||||
#include <mssip.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#define NT_SUCCESS(s) (((NTSTATUS)(s)) >= 0)
|
||||
#define ASSERT(x) Check(!!(x), #x)
|
||||
@@ -19,11 +21,21 @@
|
||||
#define PAGED_CODE() ((void)0)
|
||||
#define MINCRYPT_SHA1_LENGTH 20
|
||||
#define MINCRYPT_SHA256_LENGTH 32
|
||||
#define DOMITO_CI_LEGACY_CATALOG_POLICY_BITS 0x0002007FUL
|
||||
#define DOMITO_CI_SOURCE_EMBEDDED 1UL
|
||||
#define DOMITO_CI_SOURCE_CATALOG 2UL
|
||||
typedef const UCHAR* PCUCHAR;
|
||||
|
||||
static unsigned checks, allocations, ciCalls, hashCalls;
|
||||
static int failOperation, failHashCall;
|
||||
static ULONG_PTR bytesRead;
|
||||
static bool allocatePolicies;
|
||||
static NTSTATUS embeddedResult = STATUS_SUCCESS;
|
||||
static NTSTATUS catalogResults[2] = {STATUS_SUCCESS, STATUS_SUCCESS};
|
||||
static unsigned catalogCalls;
|
||||
static ULONG expectedCatalogMask = DOMITO_CI_LEGACY_CATALOG_POLICY_BITS;
|
||||
static BOOLEAN expectedTrustedMarker;
|
||||
static LONGLONG observedFileSize = 1056;
|
||||
static void Check(bool ok, const char* message)
|
||||
{
|
||||
++checks;
|
||||
@@ -61,33 +73,67 @@ static NTSTATUS TestFinishHash(BCRYPT_HASH_HANDLE h, PUCHAR b, ULONG n, ULONG f)
|
||||
}
|
||||
|
||||
// Minimal dependencies for the legacy wrapper; no actual CI code is invoked.
|
||||
struct MINCRYPT_POLICY_INFO { ULONG Size; };
|
||||
struct MINCRYPT_POLICY_INFO { ULONG Size; PVOID ChainInfo; };
|
||||
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 CiFreePolicyInfo(MINCRYPT_POLICY_INFO* p) {
|
||||
if (p->ChainInfo) ExFreePoolWithTag(p->ChainInfo, DOMITO_POOL_TAG);
|
||||
*p = {};
|
||||
}
|
||||
static void PopulatePolicies(MINCRYPT_POLICY_INFO* policy, MINCRYPT_POLICY_INFO* timestamp)
|
||||
{
|
||||
Check(!policy->ChainInfo && !timestamp->ChainInfo, "previous CI attempt released before reuse");
|
||||
if (allocatePolicies) {
|
||||
policy->Size = timestamp->Size = sizeof(*policy);
|
||||
policy->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG);
|
||||
timestamp->ChainInfo = ExAllocatePoolWithTag(PagedPool, 1, DOMITO_POOL_TAG);
|
||||
}
|
||||
}
|
||||
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)
|
||||
typedef PVOID PFILE_OBJECT;
|
||||
static PVOID fileObjectType;
|
||||
static PVOID* IoFileObjectType = &fileObjectType;
|
||||
#define KernelMode 0
|
||||
static NTSTATUS ObReferenceObjectByHandle(HANDLE, ULONG, PVOID, int, PVOID* object, PVOID)
|
||||
{
|
||||
io->Status = STATUS_SUCCESS;
|
||||
io->Information = bytesRead;
|
||||
*object = &fileObjectType;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
static void ObDereferenceObject(PVOID) {}
|
||||
static NTSTATUS FsRtlGetFileSize(PFILE_OBJECT, LARGE_INTEGER* size)
|
||||
{
|
||||
size->QuadPart = observedFileSize;
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
static NTSTATUS DomitoReadFileAt(HANDLE, PVOID buffer, ULONG size, const LARGE_INTEGER* offset)
|
||||
{
|
||||
Check(offset->QuadPart == 1024, "explicit certificate read offset");
|
||||
std::memset(buffer, 0, (size_t)min(bytesRead, (ULONG_PTR)size));
|
||||
return STATUS_SUCCESS;
|
||||
return bytesRead == size ? STATUS_SUCCESS : STATUS_END_OF_FILE;
|
||||
}
|
||||
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*)
|
||||
MINCRYPT_POLICY_INFO* policy, LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
|
||||
{
|
||||
++ciCalls;
|
||||
return STATUS_SUCCESS;
|
||||
Check(time->QuadPart == 0, "fresh signing time for embedded validation");
|
||||
PopulatePolicies(policy, timestamp);
|
||||
time->QuadPart = 123;
|
||||
return embeddedResult;
|
||||
}
|
||||
static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN, UINT32, UINT32,
|
||||
MINCRYPT_POLICY_INFO*, PVOID, LARGE_INTEGER*, MINCRYPT_POLICY_INFO*)
|
||||
static NTSTATUS CiVerifyHashInCatalog(PVOID, UINT32, ALG_ID, BOOLEAN reload,
|
||||
UINT32 trusted, UINT32 mask, MINCRYPT_POLICY_INFO* policy, PVOID,
|
||||
LARGE_INTEGER* time, MINCRYPT_POLICY_INFO* timestamp)
|
||||
{
|
||||
++ciCalls;
|
||||
return STATUS_SUCCESS;
|
||||
Check(mask == expectedCatalogMask && trusted == expectedTrustedMarker, "explicit catalog policy forwarded");
|
||||
Check(reload == (catalogCalls != 0), "catalog reload sequence");
|
||||
Check(time->QuadPart == 0, "fresh signing time for catalog attempt");
|
||||
PopulatePolicies(policy, timestamp);
|
||||
time->QuadPart = 456;
|
||||
Check(catalogCalls < 2, "bounded catalog retries");
|
||||
return catalogResults[catalogCalls++];
|
||||
}
|
||||
|
||||
#define BCryptCreateHash TestCreateHash
|
||||
@@ -108,21 +154,32 @@ static std::vector<UCHAR> Image(bool pe64, ULONG nt = 128, bool certificate = fa
|
||||
std::memcpy(b.data() + nt, &signature, sizeof(signature));
|
||||
IMAGE_FILE_HEADER f = {};
|
||||
f.NumberOfSections = 1;
|
||||
f.Machine = pe64 ? IMAGE_FILE_MACHINE_AMD64 : IMAGE_FILE_MACHINE_I386;
|
||||
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.SectionAlignment = h32.SectionAlignment = 4096;
|
||||
h64.FileAlignment = h32.FileAlignment = 512;
|
||||
h64.SizeOfImage = h32.SizeOfImage = 8192;
|
||||
h64.Subsystem = h32.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI;
|
||||
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));
|
||||
USHORT revision = 0x0200, type = WIN_CERT_TYPE_PKCS_SIGNED_DATA;
|
||||
std::memcpy(b.data() + nt + 900, &revision, 2);
|
||||
std::memcpy(b.data() + nt + 902, &type, 2);
|
||||
}
|
||||
std::memcpy(b.data() + nt + 24, pe64 ? (void*)&h64 : (void*)&h32, f.SizeOfOptionalHeader);
|
||||
IMAGE_SECTION_HEADER section = {};
|
||||
section.VirtualAddress = 4096;
|
||||
section.Misc.VirtualSize = 512;
|
||||
section.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ;
|
||||
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);
|
||||
@@ -187,10 +244,295 @@ static void GuardedInvalid(const std::vector<UCHAR>& b, NTSTATUS expected)
|
||||
UCHAR* input = region + span - b.size();
|
||||
std::memcpy(input, b.data(), b.size());
|
||||
Digest(input, (ULONG)b.size(), expected);
|
||||
ULONG strictSize = 0, securitySize = 0;
|
||||
PVOID strictDigest = nullptr;
|
||||
LPWIN_CERTIFICATE certificate = nullptr;
|
||||
Check(!NT_SUCCESS(DomitoCalculatePortableExecutableDigestEx(input, (ULONG)b.size(), CALG_SHA256,
|
||||
&strictSize, &strictDigest, &certificate, &securitySize)), "strict guard-page malformed input rejected");
|
||||
Check(!strictSize && !strictDigest && !certificate && !securitySize, "strict malformed outputs cleared");
|
||||
VirtualFree(region, 0, MEM_RELEASE);
|
||||
}
|
||||
static std::vector<UCHAR> ExplicitDigest(const std::vector<UCHAR>& b, UINT32 calg,
|
||||
NTSTATUS expected = STATUS_SUCCESS)
|
||||
{
|
||||
ULONG length = 0xdead, certSize = 0xdead;
|
||||
PVOID digest = (PVOID)1;
|
||||
LPWIN_CERTIFICATE cert = (LPWIN_CERTIFICATE)1;
|
||||
NTSTATUS status = DomitoCalculatePortableExecutableDigestEx(b.data(), (ULONG)b.size(),
|
||||
calg, &length, &digest, &cert, &certSize);
|
||||
Check(status == expected, "explicit digest status");
|
||||
std::vector<UCHAR> result;
|
||||
if (NT_SUCCESS(status))
|
||||
{
|
||||
Check(length == (calg == CALG_SHA1 ? 20u : calg == CALG_SHA256 ? 32u :
|
||||
calg == CALG_SHA384 ? 48u : 64u), "explicit algorithm digest length");
|
||||
result.assign((UCHAR*)digest, (UCHAR*)digest + length);
|
||||
DomitoFreePortableExecutableDigest(digest);
|
||||
}
|
||||
else Check(!length && !digest && !cert && !certSize, "explicit failure outputs cleared");
|
||||
Check(allocations == 0, "explicit digest allocation cleanup");
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<UCHAR> SipDigest(const std::vector<UCHAR>& b, UINT32 calg)
|
||||
{
|
||||
WCHAR tempPath[MAX_PATH], tempName[MAX_PATH];
|
||||
Check(GetTempPathW(MAX_PATH, tempPath) != 0, "temp path");
|
||||
Check(GetTempFileNameW(tempPath, L"dci", 0, tempName) != 0, "temp name");
|
||||
HANDLE file = CreateFileW(tempName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
|
||||
nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
|
||||
Check(file != INVALID_HANDLE_VALUE, "SIP fixture file");
|
||||
DWORD written = 0;
|
||||
Check(WriteFile(file, b.data(), (DWORD)b.size(), &written, nullptr) && written == b.size(), "write SIP fixture");
|
||||
GUID subject;
|
||||
Check(CryptSIPRetrieveSubjectGuid(tempName, file, &subject) != FALSE, "PE SIP subject");
|
||||
SIP_SUBJECTINFO info = {};
|
||||
info.cbSize = sizeof(info);
|
||||
info.pgSubjectType = &subject;
|
||||
info.hFile = file;
|
||||
info.pwsFileName = tempName;
|
||||
info.dwEncodingType = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
|
||||
info.DigestAlgorithm.pszObjId = const_cast<char*>(calg == CALG_SHA1 ? szOID_OIWSEC_sha1 :
|
||||
calg == CALG_SHA256 ? szOID_NIST_sha256 : calg == CALG_SHA384 ? szOID_NIST_sha384 : szOID_NIST_sha512);
|
||||
DWORD size = 0;
|
||||
Check(CryptSIPCreateIndirectData(&info, &size, nullptr) != FALSE, "SIP size query");
|
||||
std::vector<UCHAR> buffer(size);
|
||||
auto indirect = (SIP_INDIRECT_DATA*)buffer.data();
|
||||
Check(CryptSIPCreateIndirectData(&info, &size, indirect) != FALSE, "Windows SIP digest");
|
||||
std::vector<UCHAR> result(indirect->Digest.pbData, indirect->Digest.pbData + indirect->Digest.cbData);
|
||||
CloseHandle(file);
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<UCHAR> Der(UCHAR tag, const std::vector<UCHAR>& value)
|
||||
{
|
||||
std::vector<UCHAR> out = {tag};
|
||||
if (value.size() < 128) out.push_back((UCHAR)value.size());
|
||||
else if (value.size() < 256) { out.push_back(0x81); out.push_back((UCHAR)value.size()); }
|
||||
else { out.push_back(0x82); out.push_back((UCHAR)(value.size() >> 8)); out.push_back((UCHAR)value.size()); }
|
||||
out.insert(out.end(), value.begin(), value.end());
|
||||
return out;
|
||||
}
|
||||
static std::vector<UCHAR> Join(std::initializer_list<std::vector<UCHAR>> values)
|
||||
{
|
||||
std::vector<UCHAR> out;
|
||||
for (auto& value : values) out.insert(out.end(), value.begin(), value.end());
|
||||
return out;
|
||||
}
|
||||
static std::vector<UCHAR> SignedContent(UINT32 calg, bool nullParameters = true)
|
||||
{
|
||||
std::vector<UCHAR> oid = calg == CALG_SHA1 ? std::vector<UCHAR>{0x2B,0x0E,0x03,0x02,0x1A} :
|
||||
std::vector<UCHAR>{0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,
|
||||
(UCHAR)(calg == CALG_SHA256 ? 1 : calg == CALG_SHA384 ? 2 : 3)};
|
||||
auto algorithm = Der(0x30, Join({Der(0x06, oid), nullParameters ? Der(0x05, {}) : std::vector<UCHAR>{}}));
|
||||
auto digest = Der(0x30, Join({algorithm, Der(0x04, std::vector<UCHAR>(
|
||||
calg == CALG_SHA1 ? 20 : calg == CALG_SHA256 ? 32 : calg == CALG_SHA384 ? 48 : 64, 0xAA))}));
|
||||
// A complete SHA-1 DigestInfo decoy in the ignored data attribute must not
|
||||
// override the selected SpcIndirectDataContent digest AlgorithmIdentifier.
|
||||
auto data = Der(0x30, Der(0x04, {0x30,0x21,0x30,0x09,0x06,0x05,0x2B,0x0E,0x03,0x02,0x1A,0x05,0,0x04,0x14}));
|
||||
auto indirect = Der(0x30, Join({data, digest}));
|
||||
auto content = Der(0x30, Join({Der(0x06, {0x2B,0x06,0x01,0x04,0x01,0x82,0x37,0x02,0x01,0x04}), Der(0xA0, indirect)}));
|
||||
auto signedData = Der(0x30, Join({Der(0x02, {1}), Der(0x31, algorithm), content, Der(0x31, {})}));
|
||||
return Der(0x30, Join({Der(0x06, {0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x07,0x02}), Der(0xA0, signedData)}));
|
||||
}
|
||||
static void ExtendedTests()
|
||||
{
|
||||
for (bool pe64 : {false, true})
|
||||
for (bool cert : {false, true})
|
||||
for (UINT32 calg : std::initializer_list<UINT32>{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
|
||||
{
|
||||
auto b = Image(pe64, 128, cert);
|
||||
Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP differential fixture");
|
||||
if (!cert)
|
||||
{
|
||||
b.insert(b.end(), 48, 0x42); // Non-section overlay.
|
||||
Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "Windows SIP overlay differential");
|
||||
}
|
||||
}
|
||||
for (bool pe64 : {false, true})
|
||||
for (ULONG count : {0UL, 4UL})
|
||||
{
|
||||
auto absent = Image(pe64);
|
||||
Set32(absent, pe64 ? 260 : 244, count);
|
||||
ExplicitDigest(absent, CALG_SHA256, STATUS_NOT_SUPPORTED);
|
||||
}
|
||||
auto b = Image(true);
|
||||
// Two contiguous sections whose headers are in reverse raw-offset order.
|
||||
Set16(b, 134, 2);
|
||||
IMAGE_SECTION_HEADER first = {}, second = {};
|
||||
first.PointerToRawData = 768; first.SizeOfRawData = 256;
|
||||
second.PointerToRawData = 512; second.SizeOfRawData = 256;
|
||||
std::memcpy(b.data() + 392, &first, sizeof(first));
|
||||
std::memcpy(b.data() + 432, &second, sizeof(second));
|
||||
Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "section header order differential");
|
||||
Set32(b, 392 + 20, 800);
|
||||
ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT); // extends beyond EOF
|
||||
Set32(b, 392 + 20, 640);
|
||||
ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // overlap
|
||||
b = Image(true);
|
||||
Set32(b, 392 + 20, 528); Set32(b, 392 + 16, 496);
|
||||
ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // gap
|
||||
b = Image(true, 128, true);
|
||||
b.insert(b.end(), 8, 0);
|
||||
ExplicitDigest(b, CALG_SHA256, STATUS_NOT_SUPPORTED); // nonterminal certificate
|
||||
b = Image(true);
|
||||
Set32(b, 392 + 20, 0xFFFFFFFF);
|
||||
ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT);
|
||||
ExplicitDigest(Image(true), 0x1234, STATUS_NOT_SUPPORTED);
|
||||
b = Image(true, 128, true);
|
||||
b.resize(1088);
|
||||
Set32(b, 300, 64);
|
||||
Set32(b, 1056, 32); Set16(b, 1060, 0x0200); Set16(b, 1062, 2);
|
||||
Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "multiple certificate entries excluded");
|
||||
Set32(b, 1056, 0);
|
||||
ExplicitDigest(b, CALG_SHA256, STATUS_INVALID_IMAGE_FORMAT);
|
||||
b = Image(true, 128, true);
|
||||
Set32(b, 1024, 31); // Last entry's alignment padding is outside dwLength.
|
||||
Check(ExplicitDigest(b, CALG_SHA256) == SipDigest(b, CALG_SHA256), "certificate alignment padding");
|
||||
for (int failure : {1, 2, 3, 4})
|
||||
{
|
||||
failOperation = failure;
|
||||
failHashCall = 1;
|
||||
hashCalls = 0;
|
||||
ExplicitDigest(Image(true), CALG_SHA256,
|
||||
failure == 4 ? STATUS_INSUFFICIENT_RESOURCES : STATUS_UNSUCCESSFUL);
|
||||
}
|
||||
failOperation = 0;
|
||||
for (UINT32 calg : std::initializer_list<UINT32>{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
|
||||
{
|
||||
auto content = SignedContent(calg);
|
||||
auto signedImage = Image(true);
|
||||
ULONG certLength = 8 + (ULONG)content.size();
|
||||
ULONG tableSize = (certLength + 7) & ~7u;
|
||||
signedImage.resize(1024 + tableSize);
|
||||
Set32(signedImage, 296, 1024); Set32(signedImage, 300, tableSize);
|
||||
Set32(signedImage, 1024, certLength);
|
||||
Set16(signedImage, 1028, 0x0200); Set16(signedImage, 1030, 2);
|
||||
std::memcpy(signedImage.data() + 1032, content.data(), content.size());
|
||||
IMAGE_DATA_DIRECTORY directories[5] = {};
|
||||
directories[4] = {1024, tableSize};
|
||||
UINT32 actual = 0;
|
||||
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||
directories, &actual) == STATUS_SUCCESS && actual == calg, "bounded SPC digest algorithm, decoy ignored");
|
||||
for (ULONG cut = 0; cut < content.size(); ++cut)
|
||||
{
|
||||
Set32(signedImage, 1024, cut + 8);
|
||||
actual = 0xDEAD;
|
||||
Check(!NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx(signedImage.data(),
|
||||
(ULONG)signedImage.size(), directories, &actual)) && actual == 0, "truncated DER rejected");
|
||||
}
|
||||
Set32(signedImage, 1024, tableSize);
|
||||
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||
directories, &actual) == STATUS_SUCCESS && actual == calg, "bounded zero padding inside dwLength");
|
||||
if (tableSize > certLength)
|
||||
{
|
||||
signedImage.back() = 0x7F;
|
||||
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||
directories, &actual) == STATUS_INVALID_IMAGE_FORMAT && actual == 0, "nonzero DER trailing bytes rejected");
|
||||
signedImage.back() = 0;
|
||||
}
|
||||
Set32(signedImage, 1024, certLength);
|
||||
signedImage[1033] = 0x80; // Indefinite length forbidden in this DER subset.
|
||||
Check(DomitoGetPortableExecutableDigestKindEx(signedImage.data(), (ULONG)signedImage.size(),
|
||||
directories, &actual) == STATUS_INVALID_IMAGE_FORMAT, "indefinite DER rejected");
|
||||
}
|
||||
for (bool nullParameters : {false, true})
|
||||
{
|
||||
auto content = SignedContent(CALG_SHA256, nullParameters);
|
||||
auto pe = Image(true);
|
||||
const ULONG length = 8 + (ULONG)content.size();
|
||||
const ULONG tableSize = (length + 7) & ~7u;
|
||||
pe.resize(1024 + tableSize);
|
||||
Set32(pe, 1024, length); Set16(pe, 1028, 0x0200); Set16(pe, 1030, 2);
|
||||
std::memcpy(pe.data() + 1032, content.data(), content.size());
|
||||
IMAGE_DATA_DIRECTORY dirs[5] = {};
|
||||
dirs[4] = {1024, tableSize};
|
||||
UINT32 algorithm = 0;
|
||||
Check(DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) == STATUS_SUCCESS &&
|
||||
algorithm == CALG_SHA256, "AlgorithmIdentifier NULL optional");
|
||||
// Change both copies of the SHA-256 OID to an unsupported family member.
|
||||
const UCHAR oid[] = {0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x02,0x01};
|
||||
for (size_t i = 1032; i + sizeof(oid) <= pe.size(); ++i)
|
||||
if (!std::memcmp(pe.data() + i, oid, sizeof(oid))) pe[i + sizeof(oid) - 1] = 0x7F;
|
||||
Check(DomitoGetPortableExecutableDigestKindEx(pe.data(), (ULONG)pe.size(), dirs, &algorithm) == STATUS_NOT_SUPPORTED &&
|
||||
!algorithm, "unsupported SPC algorithm does not default to SHA1");
|
||||
}
|
||||
IMAGE_DATA_DIRECTORY dirs[5] = {};
|
||||
UINT32 actual = 123;
|
||||
Check(DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual) == STATUS_NOT_FOUND &&
|
||||
actual == 0, "absent DER does not silently select SHA1");
|
||||
}
|
||||
|
||||
static void SystemImageTests()
|
||||
{
|
||||
WCHAR system[MAX_PATH];
|
||||
Check(GetSystemDirectoryW(system, MAX_PATH) != 0, "system directory");
|
||||
for (const WCHAR* name : {L"ci.dll", L"ntdll.dll", L"kernel32.dll", L"notepad.exe"})
|
||||
{
|
||||
std::wstring path = std::wstring(system) + L"\\" + name;
|
||||
HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE,
|
||||
nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
Check(file != INVALID_HANDLE_VALUE, "system PE corpus file");
|
||||
LARGE_INTEGER size;
|
||||
Check(GetFileSizeEx(file, &size) && size.QuadPart > 0 && size.QuadPart < 32 * 1024 * 1024, "bounded system PE size");
|
||||
std::vector<UCHAR> b((size_t)size.QuadPart);
|
||||
DWORD read;
|
||||
Check(ReadFile(file, b.data(), (DWORD)b.size(), &read, nullptr) && read == b.size(), "read system PE fixture");
|
||||
CloseHandle(file);
|
||||
for (UINT32 calg : std::initializer_list<UINT32>{CALG_SHA1, CALG_SHA256, CALG_SHA384, CALG_SHA512})
|
||||
Check(ExplicitDigest(b, calg) == SipDigest(b, calg), "real system PE Windows SIP differential");
|
||||
IMAGE_DOS_HEADER dos;
|
||||
std::memcpy(&dos, b.data(), sizeof(dos));
|
||||
USHORT magic;
|
||||
const ULONG optional = (ULONG)dos.e_lfanew + 24;
|
||||
std::memcpy(&magic, b.data() + optional, 2);
|
||||
const ULONG directory = magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC ?
|
||||
FIELD_OFFSET(IMAGE_OPTIONAL_HEADER64, DataDirectory) : FIELD_OFFSET(IMAGE_OPTIONAL_HEADER32, DataDirectory);
|
||||
IMAGE_DATA_DIRECTORY dirs[5];
|
||||
std::memcpy(dirs, b.data() + optional + directory, sizeof(dirs));
|
||||
if (dirs[4].Size)
|
||||
{
|
||||
UINT32 actual;
|
||||
Check(NT_SUCCESS(DomitoGetPortableExecutableDigestKindEx(b.data(), (ULONG)b.size(), dirs, &actual)),
|
||||
"real embedded SPC digest algorithm");
|
||||
// Independent Windows ASN.1 decoder: extract signed content with
|
||||
// CryptQueryObject/CryptMsg, then decode SPC_INDIRECT_DATA_OBJID.
|
||||
ULONG certLength;
|
||||
std::memcpy(&certLength, b.data() + dirs[4].VirtualAddress, 4);
|
||||
CRYPT_DATA_BLOB blob = {certLength - 8, b.data() + dirs[4].VirtualAddress + 8};
|
||||
HCRYPTMSG message = nullptr;
|
||||
Check(CryptQueryObject(CERT_QUERY_OBJECT_BLOB, &blob, CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED,
|
||||
CERT_QUERY_FORMAT_FLAG_BINARY, 0, nullptr, nullptr, nullptr, nullptr, &message, nullptr) != FALSE,
|
||||
"Windows decode PKCS7");
|
||||
DWORD contentSize = 0;
|
||||
Check(CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, nullptr, &contentSize) != FALSE,
|
||||
"Windows SPC content size");
|
||||
std::vector<UCHAR> content(contentSize);
|
||||
Check(CryptMsgGetParam(message, CMSG_CONTENT_PARAM, 0, content.data(), &contentSize) != FALSE,
|
||||
"Windows SPC content");
|
||||
SIP_INDIRECT_DATA* indirect = nullptr;
|
||||
DWORD decodedSize = 0;
|
||||
Check(CryptDecodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID,
|
||||
content.data(), contentSize, CRYPT_DECODE_ALLOC_FLAG, nullptr, &indirect, &decodedSize) != FALSE,
|
||||
"Windows decode indirect data");
|
||||
const char* oid = indirect->DigestAlgorithm.pszObjId;
|
||||
const UINT32 expected = !std::strcmp(oid, szOID_OIWSEC_sha1) ? CALG_SHA1 :
|
||||
!std::strcmp(oid, szOID_NIST_sha256) ? CALG_SHA256 :
|
||||
!std::strcmp(oid, szOID_NIST_sha384) ? CALG_SHA384 : CALG_SHA512;
|
||||
Check(actual == expected, "SPC digest algorithm matches Windows ASN1 decoder");
|
||||
auto digest = ExplicitDigest(b, actual);
|
||||
Check(digest.size() == indirect->Digest.cbData &&
|
||||
!std::memcmp(digest.data(), indirect->Digest.pbData, digest.size()), "computed digest matches real embedded SPC digest");
|
||||
LocalFree(indirect);
|
||||
CryptMsgClose(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
SystemImageTests();
|
||||
ExtendedTests();
|
||||
for (bool pe64 : {false, true})
|
||||
{
|
||||
for (bool certificate : {false, true})
|
||||
@@ -255,5 +597,43 @@ int main()
|
||||
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);
|
||||
// Failure paths must release previous attempts before embedded/catalog/reload reuse.
|
||||
allocatePolicies = true;
|
||||
embeddedResult = STATUS_INVALID_IMAGE_HASH;
|
||||
catalogResults[0] = STATUS_INVALID_IMAGE_HASH;
|
||||
catalogResults[1] = STATUS_SUCCESS;
|
||||
catalogCalls = 0;
|
||||
expectedCatalogMask = 0x8000;
|
||||
expectedTrustedMarker = TRUE;
|
||||
ULONG source = 0xDEAD;
|
||||
Check(NT_SUCCESS(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA1,
|
||||
&directory, &policy, &signing, ×tamp, expectedCatalogMask, TRUE, &source)), "explicit catalog fallback");
|
||||
Check(source == DOMITO_CI_SOURCE_CATALOG && catalogCalls == 2 && allocations == 2,
|
||||
"final policy ownership and catalog source");
|
||||
CiFreePolicyInfo(&policy); CiFreePolicyInfo(×tamp);
|
||||
Check(allocations == 0, "final results freed once");
|
||||
source = 123;
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA256,
|
||||
&directory, &policy, &signing, ×tamp, 0, FALSE, &source) == STATUS_INVALID_PARAMETER && !source,
|
||||
"algorithm and length mismatch rejected");
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA384,
|
||||
&directory, &policy, &signing, ×tamp, 0, FALSE, &source) == STATUS_NOT_SUPPORTED,
|
||||
"legacy catalog algorithm capability explicit");
|
||||
observedFileSize = 1040;
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA1,
|
||||
&directory, &policy, &signing, ×tamp, 0, FALSE, &source) == STATUS_INVALID_IMAGE_FORMAT,
|
||||
"certificate file range checked before allocation");
|
||||
Check(allocations == 0, "invalid file range never allocated");
|
||||
observedFileSize = 1056;
|
||||
expectedCatalogMask = 0x8000;
|
||||
expectedTrustedMarker = TRUE;
|
||||
catalogCalls = 0;
|
||||
catalogResults[1] = STATUS_INVALID_IMAGE_HASH;
|
||||
Check(DomitoValidateFileLegacyModeEx((HANDLE)1, hash, 20, CALG_SHA1,
|
||||
&directory, &policy, &signing, ×tamp, expectedCatalogMask, TRUE, &source) == STATUS_INVALID_IMAGE_HASH,
|
||||
"final catalog failure retained");
|
||||
Check(!source && allocations == 2, "final failed-call policies remain caller-owned");
|
||||
CiFreePolicyInfo(&policy); CiFreePolicyInfo(×tamp);
|
||||
Check(allocations == 0, "failed final policies freed once");
|
||||
std::printf("PASS: %u digest/legacy checks (Windows SIP, Crypt32, guard pages, failures, ownership).\n", checks);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Compile with the WDK for supported 64-bit targets. No driver is linked/loaded.
|
||||
#include <ntifs.h>
|
||||
#include "Domito.MinCrypt.h"
|
||||
|
||||
#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(sizeof(MINCRYPT_CHAIN_INFO) == 72);
|
||||
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);
|
||||
|
||||
#if defined(_M_AMD64)
|
||||
// These opt-in declarations establish observations only for the named x64 build.
|
||||
#include "Domito.MinCrypt.26100.h"
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
"""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:
|
||||
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)
|
||||
subprocess.run([str(exe)], cwd=build, check=True)
|
||||
@@ -30,5 +30,5 @@ with tempfile.TemporaryDirectory(prefix='domito-digest-tests-') as temp:
|
||||
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)
|
||||
'/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