Getting WinVerifyTrust to work with catalog signed files such as cmd.exe

1.9k views Asked by At

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.

enter image description here

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

https://github.com/microsoft/Windows-classic-samples/blob/master/Samples/Security/CodeSigning/cpp/codesigning.cpp

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

cmd.exe hashes

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.

2

There are 2 answers

0
SrPanda On

As far as i know WinVerifyTrust does not actually deal with installed catalogs even tho WINTRUST_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 to pCatalog than using catalog that you are creating, this sample kinda hints that idea. The catalog finding process relies on you telling CryptCATAdminAcquireContext2 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 flag WTD_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.

#define _UNICODE 1
#define UNICODE 1

#include <windows.h>
#include <Softpub.h>
#include <wintrust.h>
#include <mscat.h>

#include <stdio.h>

#pragma comment (lib, "wintrust")

int verify_signature(const wchar_t * file_path, bool ui = false){
    int ret = 0;
    long status = 0;
    GUID policy_guid = WINTRUST_ACTION_GENERIC_VERIFY_V2;

    HANDLE file_handle = CreateFileW(
        file_path, GENERIC_READ, FILE_SHARE_READ,
        NULL, OPEN_EXISTING, 0, NULL
    );
    if (file_handle == INVALID_HANDLE_VALUE){
        ret = GetLastError();
        goto cleanup;
    }

    // [*] Shared strutcs

    WINTRUST_FILE_INFO file_info;
    ZeroMemory(&file_info, sizeof(file_info));
    file_info.cbStruct = sizeof(WINTRUST_FILE_INFO);
    file_info.pcwszFilePath = file_path;
    file_info.hFile = file_handle;

    /*/

        sign = ['RSA', 'DSA', 'ECDSA']
        hash = ['SHA256', 'SHA512']
        comb = []
        for a in sign:
            for b in hash:
                comb.append('{0}/{1}'.format(a, b))
                
        print(';'.join(comb))

    /*/

    wchar_t * sign_hash = L"RSA/SHA256;RSA/SHA512;DSA/SHA256;DSA/SHA512;ECDSA/SHA256;ECDSA/SHA512";

    CERT_STRONG_SIGN_SERIALIZED_INFO policy_rule;
    policy_rule.dwFlags = 0;
    policy_rule.pwszCNGSignHashAlgids = sign_hash;
    policy_rule.pwszCNGPubKeyMinBitLengths = nullptr;

    CERT_STRONG_SIGN_PARA policy;
    ZeroMemory(&policy, sizeof(policy));
    policy.cbSize = sizeof(CERT_STRONG_SIGN_PARA);
    policy.dwInfoChoice = CERT_STRONG_SIGN_SERIALIZED_INFO_CHOICE;
    policy.pSerializedInfo = &policy_rule;

    // [1] Check for catalogs

    HCATINFO  info_handle  = NULL;
    HCATADMIN admin_handle = NULL;

    // if (!CryptCATAdminAcquireContext2(&admin_handle, NULL, 0, &policy, 0)) {
    if (!CryptCATAdminAcquireContext2(&admin_handle, NULL, 0, NULL, 0)) {
        ret = GetLastError();
        goto cleanup;
    }

    DWORD hash_len = 0;
    BYTE * hash_data = nullptr;
    CryptCATAdminCalcHashFromFileHandle2(
        admin_handle, file_handle, &hash_len, NULL, 0
    
    );
    hash_data = new BYTE[hash_len];
    if (!CryptCATAdminCalcHashFromFileHandle2(
        admin_handle, file_handle, &hash_len, hash_data, 0 )) {

        ret = GetLastError();
        goto cleanup;
    }

    CATALOG_INFO catalog;
    ZeroMemory(&catalog, sizeof(CATALOG_INFO));
    do {
        info_handle = CryptCATAdminEnumCatalogFromHash(
            admin_handle, hash_data, hash_len, 0, &info_handle
        );

        if (CryptCATCatalogInfoFromContext(info_handle, &catalog, 0 )){
            wprintf(L" - Catalog %ls \n", catalog.wszCatalogFile);
        }

    } while (info_handle != NULL);

    // [2] Check for embeded ones

    WINTRUST_SIGNATURE_SETTINGS sign_settings;
    ZeroMemory(&sign_settings, sizeof(sign_settings));
    sign_settings.cbStruct = sizeof(WINTRUST_SIGNATURE_SETTINGS);
    sign_settings.dwFlags = WSS_VERIFY_SPECIFIC;
    sign_settings.dwIndex = 0;

    WINTRUST_DATA wintrust_data;
    ZeroMemory(&wintrust_data, sizeof(wintrust_data));
    wintrust_data.cbStruct = sizeof(WINTRUST_DATA);
    wintrust_data.dwUIChoice = ui ? WTD_UI_ALL : WTD_UI_NONE;
    wintrust_data.fdwRevocationChecks = WTD_REVOKE_NONE; 
    wintrust_data.dwUnionChoice = WTD_CHOICE_FILE;
    wintrust_data.dwProvFlags = WTD_HASH_ONLY_FLAG;

    wintrust_data.pFile = &file_info;
    wintrust_data.pSignatureSettings = &sign_settings;
    wintrust_data.pSignatureSettings->pCryptoPolicy = &policy;

    CRYPT_PROVIDER_DATA * prov_data = nullptr;
    CRYPT_PROVIDER_SGNR * prov_signer = nullptr;

    do {

        wintrust_data.dwStateAction = WTD_STATEACTION_VERIFY;
        status = WinVerifyTrust(
            // same ase GetDC, 0 means desktop
            ui ? 0 : (HWND)INVALID_HANDLE_VALUE, 
            &policy_guid, (LPVOID)&wintrust_data
        );

        // Check -> WinTruest.h:291
        if (status == ERROR_SUCCESS){
            prov_data =  WTHelperProvDataFromStateData(
                wintrust_data.hWVTStateData
            );
            prov_signer = WTHelperGetProvSignerFromChain(
                prov_data, wintrust_data.pSignatureSettings->dwIndex, FALSE, 0
            );

            if (prov_signer != nullptr){
                // prov_signer is just a nigtmare to use
                printf(" - Embeded %s \n", prov_signer->pChainContext->rgpChain[0]->rgpElement[0]->pCertContext->pCertInfo->SignatureAlgorithm.pszObjId);
            }

        }

        wintrust_data.dwStateAction = WTD_STATEACTION_CLOSE;
        WinVerifyTrust(
            ui ? 0 : (HWND)INVALID_HANDLE_VALUE, 
            &policy_guid, (LPVOID)&wintrust_data
        );

         wintrust_data.pSignatureSettings->dwIndex++;
    } while (
        wintrust_data.pSignatureSettings->dwIndex <= 
        wintrust_data.pSignatureSettings->cSecondarySigs
    );

    cleanup:
    if (hash_data != nullptr){
        delete [] hash_data;
    }
    if (admin_handle != NULL){
        CryptCATAdminReleaseContext(admin_handle, NULL);
    }
    if (file_handle != NULL){
        CloseHandle(file_handle);
    }
    return ret;
}

int main(void){
    // paths that use a single \ may not work
    wchar_t * paths [] = {
        L"C:/Windows/explorer.exe",                      // catalog / embeded
        L"C:/Windows/System32/cmd.exe",                  // only catalog
        L"C:/Program Files/AMD/CNext/CNext/AMDLink.exe"  // Only embeded
    };

    for (wchar_t * path : paths){
        wprintf(L"%ls \n", path);
        int ver = verify_signature(path);
        wprintf(L"\n");
    }
    return 0;
}

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

3
YangXiaoPo-MSFT On

I Tested the Following Code Which sets NULL (as recommended in the docs) instead of BCRYPT_SHA256_ALGORITHM. It's No Problem.
Although The Document says The default hashing algorithm may change in future Windows versions, It’s necessary to maintain consistent behavior For Microsoft.

DWORD VerifyCatalogSignature(_In_ HANDLE FileHandle,
    _In_ bool UseStrongSigPolicy)
{
    ...

    if (UseStrongSigPolicy != false)
    {
        SigningPolicy.cbSize = sizeof(CERT_STRONG_SIGN_PARA);
        SigningPolicy.dwInfoChoice = CERT_STRONG_SIGN_OID_INFO_CHOICE;
        //SigningPolicy.pszOID = const_cast<char*>(szOID_CERT_STRONG_SIGN_OS_CURRENT);
        SigningPolicy.pszOID = const_cast<char*>(szOID_CERT_STRONG_KEY_OS_1);
        if (!CryptCATAdminAcquireContext2(
            &CatAdminHandle,
            NULL,
            NULL,
            &SigningPolicy,
            0))
        {
            Error = GetLastError();
            goto Cleanup;
        }
    }
    else
    {
        if (!CryptCATAdminAcquireContext2(
            &CatAdminHandle,
            NULL,
            BCRYPT_SHA256_ALGORITHM,
            NULL,
            0))
        {
            Error = GetLastError();
            goto Cleanup;
        }
    }

    ...
}