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

3

18

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.

Hersch answered 1/7, 2021 at 19:13 Comment(4)
As the parameter pwszHashAlgorithm Of CryptCATAdminAcquireContext2 says, The default hashing algorithm may change in future Windows versions.Honeycomb
@YangXiaoPo, yes, and so using NULL is recommended. But it does not work. So what should be done?Hersch
but from what are you know what they do have in this catalog and what they have not?Lyudmila
@Алексей Неудачин once you find the catalog file, you can double-click it in Windows Explorer, and choose the Security Catalog tab to see the hashes. I assume it is showing all the hashes in the file, but that is just an assumption.Hersch
H
1

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;
        }
    }

    ...
}
Honeycomb answered 7/7, 2021 at 6:53 Comment(3)
Did you try this on C:\WIndows\System32\cmd.exe? Was UseStrongSigPolicy true or false? In the false case, BCRYPT_SHA256_ALGORITHM is still being used in the code above.Hersch
Command Arguments:-p -c C:\Windows\System32\cmd.exeHoneycomb
I hadn't noticed the szOID_CERT_STRONG_KEY_OS_1 change above (duh), but even with that change WinVerifyTrust never returns ERROR_SUCCESS with the found catalogs. Given that you mention using -p -c on the command line, I think you're using the codesigning.cpp from the link above. That verifies a file in a catalog, but doesn't actually call WinVerifyTrust.Hersch
K
0

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

Kazbek answered 9/4, 2022 at 22:21 Comment(0)
T
0

Q: How does CryptCATAdminEnumCatalogFromHash find the same catalog files using the PESHA1 hash, even though the hash isn't found in the catalog file?

A: In my tests both the SHA1 hash and SHA256 hash are found in the catalog file. The problem I had, was that the sysinternals sigcheck.exe tool is returning a different hash calculation than what CryptCATAdminCalcHashFromFileHandle2() returns.

When using a SHA1 and SHA256 hash of C:\Windows\Regedit.exe, this is the catalog file found:
c:\windows\system32\catroot\{f750e6c3-38ee-11d1-85e5-00c04fc295ee}\Microsoft-Windows-Client-Features-WOW64-Package0011~31bf3856ad364e35~amd64~~10.0.19041.4355.cat

Results from CryptCATAdminCalcHashFromFileHandle2:

nullptr:   792ff4db1013fec24ba856c302087a3b4d8448e8
"SHA256": 553f29e6d0f46bd287a19a241028e3f7dc7805e0c8f5973b1a9bb6f8f2775c5d  

Results from sigcheck:

C:\src>sigcheck -h -nobanner C:\Windows\regedit.exe
c:\windows\regedit.exe:
        Verified:       Signed
        Signing date:   11:47 AM 2024-05-08
        Publisher:      Microsoft Windows
        Company:        Microsoft Corporation
        Description:    Registry Editor
        Product:        Microsoft« Windows« Operating System
        Prod version:   10.0.19041.4355
        File version:   10.0.19041.4355 (WinBuild.160101.0800)
        MachineType:    64-bit
        MD5:    58F665A290253DFC14750CD9999DB293
        SHA1:   B176DA979CA5126535238D80C525FBF56F8A89D7
        PESHA1: C5E8ADDABBB1D7618B82E792E28D2AF557C8CD0A
        PE256:  669670CA90BDB1F1D945FC6C4A42A1544FA7E5B7E6100DB760BA9B3FBE044AFA
        SHA256: 86F6382CBCC4DD5346654FED069B6B4ADA74A878318EB9B3F16EAF4FA67B60E0
        IMP:    7FEBF576192E34BDF7D07877CBACC413

Output of "sigcheck -d":

C:\src>sigcheck -d -nobanner C:\Windows\system32\CatRoot\{F750E6C3-38EE-11D1-85E5-00C04FC295EE}\Microsoft-Windows-Client-Features-WOW64-Package0011~31bf3856ad364e35~amd64~~10.0.19041.4355.cat
c:\windows\system32\catroot\{f750e6c3-38ee-11d1-85e5-00c04fc295ee}\Microsoft-Windows-Client-Features-WOW64-Package0011~31bf3856ad364e35~amd64~~10.0.19041.4355.cat
    OS Attributes: 
      Windows 10
      Windows 8.1/Windows Server 2012 R2
      Windows 8/Windows Server 2012
      Windows 7/Windows Server 2008 R2
      Windows Vista/Windows Server 2008
      Windows Server 2003
      Windows XP
    PackageName: microsoft-windows-client-features-wow64-package0011
    PE: TRUSTED
...
    Algorithm: sha1NoSign
    Hash: 792FF4DB1013FEC24BA856C302087A3B4D8448E8
...
    Algorithm: sha256NoSign
    Hash: 553F29E6D0F46BD287A19A241028E3F7DC7805E0C8F5973B1A9BB6F8F2775C5D
Toy answered 1/8 at 16:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.