让WinVerifyTrust与目录签名文件(如cmd.exe)一起工作的方法

18
感谢这里提供的非常旧的文章。

https://web.archive.org/web/20140217003950/http://forum.sysinternals.com/topic16893_post83634.html

我发现了一个函数,它将调用WinVerifyTrust来检查文件的嵌入式签名,如果失败,则找到适当的系统目录文件,并使用另一个WinVerifyTrust调用进行检查。
然而,对C:\Windows\System32\cmd.exe进行测试失败。请注意,测试应用程序是64位的,因此文件重定向不是问题。
将该函数的输出与Microsoft的Sigcheck实用程序进行比较,该函数具有正确的文件哈希值,并找到了正确的目录文件。但是,当使用目录信息调用WinVerifyTrust时,仍会出现以下错误:
TRUST_E_BAD_DIGEST 0x80096010 //对象的数字签名未经验证。
有趣的是,当启用UI时:
dwUIChoice = WTD_UI_ALL
失败代码是不同的:
TRUST_E_SUBJECT_NOT_TRUSTED 0x800B0004 //主题未经信任,无法执行指定操作。
但是,Sigcheck.exe和Signtool.exe都表示它是可信的。
此外,如果设置dwUIChoice = WTD_UI_ALL,则会弹出下面的错误弹出窗口,并提供一个链接,看起来像是一个非常有效的证书。

enter image description here

那么为什么WinVerifyTrust会指示cmd.exe的签名有问题呢?

以下是代码,欢迎任何意见和建议:

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

请注意,要使用上述函数,您需要:

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

// Link with the Wintrust.lib file.
#pragma comment (lib, "wintrust")

更新:这里提供了一个样例

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

我刚刚发现使用了\{\{某个IT技术\}\}。
CryptCATAdminAcquireContext2(&hCatAdmin, NULL, BCRYPT_SHA256_ALGORITHM, NULL, 0))

在我的Windows Server 2019上,使用CryptCATAdminCalcHashFromFileHandle2代替CryptCATAdminCalcHashFromFileHandle可以正常工作

现在问题变成了:“为什么?”BCRYPT_SHA256_ALGORITHM是否适用于可能在其他操作系统版本上运行的代码(Win7?Win8?Server 2012 R2?)

更新2:

CryptCATAdminAcquireContext2文档中写道:“此函数使您能够选择或为您选择在需要目录管理员上下文的功能中要使用的哈希算法。虽然您可以设置散列算法的名称,但我们建议您让函数确定算法。这样可以保护您的应用程序免受将来可能不受信任的硬编码算法的影响。”

然而,将NULL(如文档所建议)设置为BCRYPT_SHA256_ALGORITHM会导致先前出现的故障。这非常脆弱,似乎是特定于操作系统的 :(

有没有办法使其可靠地跨操作系统版本工作?

更新3:

现在很明显为什么这不能正确地工作。这里是由sigcheck显示的cmd.exe的哈希列表

cmd.exe hashes

当使用NULL参数调用CryptCATAdminAcquireContext2函数时,可以从CryptCATAdminCalcHashFromFileHandle2函数中获取PESHA1哈希值。当使用BCRYPT_SHA256_ALGORITHM参数调用该函数时,可以获取PE256哈希值。

这些都很容易理解。不幸的是,目录文件只包含PE256哈希值。因此,如果您不知道目录文件包含哪种散列算法,我能想到的唯一解决方法就是通过使用不同的算法循环运行所有这些代码,一遍又一遍地对文件进行哈希操作,直到找到一个存在于目录文件中的哈希。

然而,令人困惑的是CryptCATAdminEnumCatalogFromHash如何使用PESHA1哈希值找到相同的目录文件,即使在目录文件中没有找到该哈希值?这肯定是因为有其他额外信息使其能够正常工作。


根据CryptCATAdminAcquireContext2函数的参数pwszHashAlgorithm所述,默认哈希算法可能会在未来的Windows版本中更改。 - YangXiaoPo-MSFT
@YangXiaoPo,是的,因此建议使用NULL。但它并不起作用。那么应该怎么办? - DougN
但是你怎么知道他们在这个目录中有什么,又有什么没有呢? - Алексей Неудачин
@Алексей Неудачин,一旦您找到目录文件,您可以在Windows资源管理器中双击它,并选择安全目录选项卡以查看哈希值。我假设它显示了文件中的所有哈希值,但这只是一个假设。 - DougN
2个回答

1
我测试了以下代码,它将NULL设置为BCRYPT_SHA256_ALGORITHM(如文档中推荐)。这没有问题。 虽然文档说“默认的哈希算法可能会在未来的Windows版本中更改”,但对于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;
        }
    }

    ...
}

你尝试在C:\Windows\System32\cmd.exe上运行了吗?UseStrongSigPolicy是true还是false?如果是false,那么上面的代码仍然使用BCRYPT_SHA256_ALGORITHM。 - DougN
命令参数:-p -c C:\Windows\System32\cmd.exe - YangXiaoPo-MSFT
我之前没有注意到上面的szOID_CERT_STRONG_KEY_OS_1更改(哎呀),但即使进行了更改,WinVerifyTrust在找到目录时也从不返回ERROR_SUCCESS。考虑到您在命令行上使用-p -c,我认为您正在使用上面链接中的codesigning.cpp。那会验证目录中的文件,但并不会实际调用WinVerifyTrust。 - DougN

0
据我所知,WinVerifyTrust 实际上并不处理已安装的目录,即使 WINTRUST_DATA 有一个目录参数。但是查看数据应如何访问表明 args 只用于只读,而且我看不到其他使用 pCatalog 的目的,除了使用你正在创建的目录。这个sample有点暗示了这个想法。目录查找过程依赖于您告诉 CryptCATAdminAcquireContext2 希望验证的哈希值,您可以仅传递 null,但如果您不想具有特定哈希值,则需要标记可以在没有这些哈希值的情况下可用的每个组合。之所以建议不检查特定目录,是因为理想的做法是验证它们中的每一个;当您标记 WTD_UI_ALL 时,如果接受对话框,将获得证书,但这是“用户覆盖”,类似于请求提升的权限。
#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;
}

需要注意的是,与 Windows 中的某些东西一样,它们能够工作是因为它们可以,而失败是因为它们想要(或者至少我是这么认为的),.cat 文件可能包含更多的内容,但如果函数说它在那里,那么它就在那里。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接