Thanks to a very old post available here
https://web.archive.org/web/20140217003950/http://forum.sysinternals.com/topic16893_post83634.html
I came across a function that will call WinVerifyTrust on a file to check an embedded signature, and if that fails, finds the appropriate system catalog file and checks it with another call to WinVerifyTrust.
However, testing with C:\Windows\System32\cmd.exe fails. Note that the test app is 64-bit so file redirection is not an issue.
Comparing the output of the function with Microsoft's Sigcheck utility, the function has the correct file hash, as well as finds the correct catalog file. However, when WinVerifyTrust is called with the catalog information, it still fails with
TRUST_E_BAD_DIGEST 0x80096010 //The digital signature of the object did not verify.
Interestingly, when the UI is enable with
dwUIChoice = WTD_UI_ALL
the failure code is different:
TRUST_E_SUBJECT_NOT_TRUSTED 0x800B0004 // The subject is not trusted for the specified action.
But Sigcheck.exe and Signtool.exe both say it is trusted.
Also, if dwUIChoice = WTD_UI_ALL is set, I get an error pop-up below, with a link to what looks like a very valid certificate.
So why is WinVerifyTrust indicating the signature is bad on cmd.exe?
The code is below and I welcome any input on what could be fixed:
BOOL VerifyEmbeddedSignature2(LPCWSTR pwszSourceFile)
{
BOOL bRetVal = FALSE;
LONG lStatus = 0;
GUID WintrustVerifyGuid = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA wd;
WINTRUST_FILE_INFO wfi;
////set up structs to verify files with cert signatures
memset(&wfi, 0, sizeof(wfi));
wfi.cbStruct = sizeof(WINTRUST_FILE_INFO);
wfi.pcwszFilePath = pwszSourceFile;
memset(&wd, 0, sizeof(wd));
wd.cbStruct = sizeof(WINTRUST_DATA);
wd.dwUnionChoice = WTD_CHOICE_FILE;
wd.pFile = &wfi;
wd.dwUIChoice = WTD_UI_NONE;
wd.fdwRevocationChecks = WTD_REVOKE_NONE;
wd.dwStateAction = WTD_STATEACTION_VERIFY;
wd.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL | WTD_USE_DEFAULT_OSVER_CHECK;
lStatus = WinVerifyTrust(NULL, &WintrustVerifyGuid, &wd);
//clean up the state variable
wd.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(NULL, &WintrustVerifyGuid, &wd);
////if failed, try to verify using catalog files
if (lStatus != ERROR_SUCCESS)
{
GUID DriverActionGuid = DRIVER_ACTION_VERIFY;
HANDLE hFile = INVALID_HANDLE_VALUE;
DWORD dwHash = 0;
BYTE bHash[100] = { 0 };
HCATINFO hCatInfo = NULL;
HCATADMIN hCatAdmin = NULL;
LPWSTR pszMemberTag = NULL;
//open the file
hFile = CreateFileW(pwszSourceFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
goto Cleanup;
if (!CryptCATAdminAcquireContext(&hCatAdmin, &DriverActionGuid, 0))
goto Cleanup;
dwHash = sizeof(bHash);
if (!CryptCATAdminCalcHashFromFileHandle(hFile, &dwHash, bHash, 0))
goto Cleanup;
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
//Create a string form of the hash (used later in pszMemberTag)
pszMemberTag = new WCHAR[dwHash * 2 + 1];
for (DWORD dw = 0; dw < dwHash; ++dw)
{
wsprintfW(&pszMemberTag[dw * 2], L"%02X", bHash[dw]);
}
//find the catalog which contains the hash
hCatInfo = CryptCATAdminEnumCatalogFromHash(hCatAdmin, bHash, dwHash, 0, NULL);
if (hCatInfo)
{
CATALOG_INFO ci = { 0 };
ci.cbStruct = sizeof(ci);
WINTRUST_CATALOG_INFO wci;
CryptCATCatalogInfoFromContext(hCatInfo, &ci, 0);
memset(&wci, 0, sizeof(wci));
wci.cbStruct = sizeof(wci);
wci.pcwszCatalogFilePath = ci.wszCatalogFile;
wci.pcwszMemberFilePath = pwszSourceFile;
wci.pcwszMemberTag = pszMemberTag;
memset(&wd, 0, sizeof(wd));
wd.cbStruct = sizeof(WINTRUST_DATA);
wd.dwUnionChoice = WTD_CHOICE_CATALOG;
wd.pCatalog = &wci;
wd.dwUIChoice = WTD_UI_ALL; //WTD_UI_NONE; //
wd.fdwRevocationChecks = WTD_REVOKE_NONE;
wd.dwStateAction = WTD_STATEACTION_VERIFY;
wd.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL | WTD_USE_DEFAULT_OSVER_CHECK;
lStatus = WinVerifyTrust(NULL, &WintrustVerifyGuid, &wd);
if(ERROR_SUCCESS == lStatus)
bRetVal = TRUE;
//clean up the state variable
wd.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(NULL, &WintrustVerifyGuid, &wd);
CryptCATAdminReleaseCatalogContext(hCatAdmin, hCatInfo, 0);
}
Cleanup:
if(NULL != hCatAdmin)
CryptCATAdminReleaseContext(hCatAdmin, 0);
hCatAdmin = NULL;
if(NULL != pszMemberTag)
delete[] pszMemberTag;
pszMemberTag = NULL;
if(INVALID_HANDLE_VALUE != hFile)
CloseHandle(hFile);
hFile = INVALID_HANDLE_VALUE;
}
else
bRetVal = TRUE;
return bRetVal;
}
Note that to use the above function you'll need:
#include <Softpub.h>
#include <wincrypt.h>
#include <wintrust.h>
#include <mscat.h>
// Link with the Wintrust.lib file.
#pragma comment (lib, "wintrust")
UPDATE: From a sample available here
I just discovered using
CryptCATAdminAcquireContext2(&hCatAdmin, NULL, BCRYPT_SHA256_ALGORITHM, NULL, 0))
instead of CryptCATAdminAcquireContext, AND CryptCATAdminCalcHashFromFileHandle2 instead of CryptCATAdminCalcHashFromFileHandle works on my Windows Server 2019.
So now the question becomes 'why?', and will BCRYPT_SHA256_ALGORITHM be an appropriate parameter for other OS versions that code might possibly run on (Win 7? Win 8? Server 2012 R2?)
UPDATE 2
Docs for CryptCATAdminAcquireContext2 say: "This function enables you to choose, or chooses for you, the hash algorithm to be used in functions that require the catalog administrator context. Although you can set the name of the hashing algorithm, we recommend that you let the function determine the algorithm. Doing so protects your application from hard coding algorithms that may become untrusted in the future."
However, setting NULL (as recommended in the docs) instead of BCRYPT_SHA256_ALGORITHM causes the previously seen failures. This is very brittle and seems to be OS-specific :(
Anyway to make this work reliably across OS versions?
UPDATE 3 It's now obvious why this doesn't work correctly. Here is a list of hashes from cmd.exe shown by sigcheck
When calling CryptCATAdminAcquireContext2 with NULL you get the PESHA1 hash from CryptCATAdminCalcHashFromFileHandle2. When calling with BCRYPT_SHA256_ALGORITHM instead, you get the PE256 hash.
That all makes sense. Unfortunately, the catalog files only contain the PE256 hash. So if you don't know what hashing algorithm the catalog files contain, the only solution I can think of is to loop through all this code with various algorithms for CryptCATAdminAcquireContext2, and keep hasing the file over and over, until a hash is found that exists in the catalog file.
What is NOT clear, is how does CryptCATAdminEnumCatalogFromHash find the same catalog files using the PESHA1 hash, even though the hash isn't found in the catalog file? There must be some additional information somewhere that allows that to work.
As far as i know
WinVerifyTrust
does not actually deal with installed catalogs even thoWINTRUST_DATA
has a catalog parameter, but looking how the data should be accesst tells that the args just for read only and i cant see any other purpose topCatalog
than using catalog that you are creating, this sample kinda hints that idea. The catalog finding process relies on you tellingCryptCATAdminAcquireContext2
what hash is expected, you can pass just null but if you want to not have a specific hash you need to mark every combination that can be available without those. The reason it tells to not check a specific one is because the ideal thing to do is to validate each one of them; When you flagWTD_UI_ALL
you get a certificate if you accept the dialog, but that is a "user override", it's similar to the request for elevated privilegies.It should be noted that as with some thing in windows, they work because they can and fail because they want (or at least its what i think), the .cat file may contain way more things but if the function says its there so, its there