Harden CI digest parsing and failure cleanup
This commit is contained in:
1 parent
7985b23be5
commit
195b9fccf3
6 files changed
+492
-239
No files matched your search
@@ -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
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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, §ion, 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, ×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);
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
Reference in new issue
Block a user