【例子】显示PE文件数字签名的资源管理器插件

xiaotong2017-08-18 05:09:32
项目列表:
下载源代码:

PE文件数字签名,即代码签名是指开发者能对其软件代码进行的数字签名。用户可以通过代码签名服务鉴别软件的发布者及软件在传输过程中是否被篡改。软件开发者和Web管理者,利用代码签名的抗伪造性,可为其商标和产品建立一定信誉。

要想实现代码签名必须首先获得数字证书才能使用这些功能。数字证书一般是由证书颁发机构颁发给经过身份认证(通常需经第三方进行认证)的开发商的,常见的证书颁发机构包括Comodo,Thawte,VeriSign等等。

我们的计算机系统中很多应用程序都有代码签名,可通过文件属性页查看。

例如著名的Google Chrome浏览器,数字签名信息为:

我们的插件

这篇文章中,我们将用VC6.0开发一个资源管理器插件,通过详细资料视图中的列直接显示每个PE文件的数字签名信息,而无需逐个打开每个文件的属性页。此外还可以通过文件关联菜单项给PE文件添加数字签名。

使用插件

用VC6.0编译文章附带的项目,在命令行调用 regsvr32 /v "生成的dll" 安装此插件。

最终效果如下图所示,添加文件夹(下图中是C:\Tools\gtalkabout_1_2_0a)的数字签名列(Signer issuer是前面提到的证书颁发机构,Signing time是签名时间)后,此文件夹中的所有PE文件,如下图中的dll就会显示其签名信息。右键打开其中一个dll的关联菜单,菜单项Sign it...也是由此插件提供的,点击会出现右下角对话框,可调用你系统中存储的证书给选中文件进行签名。

显示代码签名

显示文件的数字签名用的是Wincrypt API。

从文件中读取数字签名的证书颁发商,签名人姓名和签名时间。

        // Get message handle and store handle from the signed file.
        if( CryptQueryObject(CERT_QUERY_OBJECT_FILE, pscd->wszFile,
            CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, CERT_QUERY_FORMAT_FLAG_BINARY,
            0, &dwEncoding, &dwContentType, &dwFormatType, &hStore, &hMsg, NULL) )
        {
            // Get signer information size.
            DWORD dwSignerInfo;

            if( CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, NULL, &dwSignerInfo) )
            {
                // Allocate memory for signer information.
                PCMSG_SIGNER_INFO pSignerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, dwSignerInfo);

                if( pSignerInfo != NULL )
                {
                    if( CryptMsgGetParam(hMsg, CMSG_SIGNER_INFO_PARAM, 0, (PVOID)pSignerInfo, &dwSignerInfo) )
                    {
                        // Search for the signer certificate in the temporary certificate store.
                        CERT_INFO CertInfo;

                        CertInfo.Issuer = pSignerInfo->Issuer;
                        CertInfo.SerialNumber = pSignerInfo->SerialNumber;

                        PCCERT_CONTEXT pCertContext = CertFindCertificateInStore(hStore,
                            X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0,
                            CERT_FIND_SUBJECT_CERT, (PVOID)&CertInfo, NULL);

                        if( pCertContext != NULL )
                        {
                            CertGetNameString(pCertContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, CERT_NAME_ISSUER_FLAG, NULL, szSignerIssuer, 256);
                            CertGetNameString(pCertContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, NULL, szSignerSubject, 256);
                            CertFreeCertificateContext(pCertContext);

                            // Get the timestamp certificate signerinfo structure.
                            PCMSG_SIGNER_INFO pCounterSignerInfo = NULL;

                            // Loop through unathenticated attributes for szOID_RSA_counterSign OID.
                            for( int nAttr = 0; nAttr < pSignerInfo->UnauthAttrs.cAttr; nAttr++ )
                            {
                                if( lstrcmpA(pSignerInfo->UnauthAttrs.rgAttr[nAttr].pszObjId, szOID_RSA_counterSign) == 0 )
                                {
                                    DWORD dwSize;

                                    // Get size of CMSG_SIGNER_INFO structure.
                                    if( CryptDecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, PKCS7_SIGNER_INFO,
                                        pSignerInfo->UnauthAttrs.rgAttr[nAttr].rgValue[0].pbData,
                                        pSignerInfo->UnauthAttrs.rgAttr[nAttr].rgValue[0].cbData,
                                        0, NULL, &dwSize) )
                                    {
                                        // Allocate memory for CMSG_SIGNER_INFO.
                                        pCounterSignerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, dwSize);
                                        if( pCounterSignerInfo != NULL )
                                        {
                                            // Decode and get CMSG_SIGNER_INFO structure for timestamp certificate.
                                            CryptDecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, PKCS7_SIGNER_INFO,
                                                pSignerInfo->UnauthAttrs.rgAttr[nAttr].rgValue[0].pbData,
                                                pSignerInfo->UnauthAttrs.rgAttr[nAttr].rgValue[0].cbData,
                                                0, (PVOID)pCounterSignerInfo, &dwSize);
                                        }
                                    }
                                }
                            }
                            if( pCounterSignerInfo != NULL )
                            {
                                // Loop through authenticated attributes and find szOID_RSA_signingTime OID.
                                for( nAttr = 0; nAttr < pCounterSignerInfo->AuthAttrs.cAttr; nAttr++ )
                                {
                                    if( lstrcmpA(szOID_RSA_signingTime, pCounterSignerInfo->AuthAttrs.rgAttr[nAttr].pszObjId) == 0 )
                                    {
                                        // Decode and get FILETIME structure.
                                        SYSTEMTIME stSigingTime;
                                        FILETIME ftSigningTime, ftSigningLocalTime;
                                        DWORD dwSize = sizeof(ftSigningTime);

                                        if( CryptDecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, szOID_RSA_signingTime,
                                            pCounterSignerInfo->AuthAttrs.rgAttr[nAttr].rgValue[0].pbData,
                                            pCounterSignerInfo->AuthAttrs.rgAttr[nAttr].rgValue[0].cbData,
                                            0, (PVOID)&ftSigningTime, &dwSize) )
                                        {
                                            // Convert to local time.
                                            FileTimeToLocalFileTime(&ftSigningTime, &ftSigningLocalTime);
                                            FileTimeToSystemTime(&ftSigningLocalTime, &stSigingTime);

                                            wsprintf(szSigningTime, _T("%4d-%02d-%02d %02d:%02d:%02d"),
                                                stSigingTime.wYear, stSigingTime.wMonth, stSigingTime.wDay,
                                                stSigingTime.wHour, stSigingTime.wMinute, stSigingTime.wSecond);
                                        }
                                        break;
                                    }
                                }
                                LocalFree(pCounterSignerInfo);
                            }
                        }
                    }
                    LocalFree(pSignerInfo);
                }
            }
        }
        if( hStore != NULL )
            CertCloseStore(hStore, 0);
        if( hMsg != NULL )
            CryptMsgClose(hMsg);

接下来让数字签名显示在资源管理器中。

在注册表中注册资源管理器信息列。

    NoRemove Folder
    {
        NoRemove Shellex
        {
            NoRemove ColumnHandlers
            {
                ForceRemove {F9A759D1-96E5-41A1-BF48-E844611DD16F}
            }
        }
    }

让我们的扩展模块实现IColumnProvider接口。

// IColumnProvider 
public:
    STDMETHOD (Initialize)(LPCSHCOLUMNINIT psci) { return S_OK; } 
    STDMETHOD (GetColumnInfo)(DWORD dwIndex, SHCOLUMNINFO* psci); 
    STDMETHOD (GetItemData)(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT* pvarData); 

下图的第二列Signer issuer,第三列Signer subject,第四列Signing time都是由本插件提供的,分别用于显示数字签名的证书颁发商,签名人姓名和签名时间。

创建资源管理器中的证书颁发商列。

    case 0:
        psci->scid.fmtid = CLSID_CodeSignShell; // Use our CLSID as the format ID
        psci->scid.pid   = 0;                   // Use the column # as the ID
        psci->vt         = VT_BSTR;             // We'll return the data as a string
        psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned
        psci->csFlags    = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings
        psci->cChars     = 32;                  // Default col width in chars
        LoadString(_Module.GetResourceInstance(), IDS_SIGNER_ISSUER, szSignColumn, 256);
        lstrcpynW(psci->wszTitle, T2W(szSignColumn), MAX_COLUMN_NAME_LEN);
        break;

从文件中读取的数字签名信息显示在证书颁发商列中。

        case 0:
            varData = szSignerIssuer;
            break;

创建资源管理器中的签名人姓名列。

    case 1:
        psci->scid.fmtid = CLSID_CodeSignShell; // Use our CLSID as the format ID
        psci->scid.pid   = 1;                   // Use the column # as the ID
        psci->vt         = VT_BSTR;             // We'll return the data as a string
        psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned
        psci->csFlags    = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings
        psci->cChars     = 32;                  // Default col width in chars
        LoadString(_Module.GetResourceInstance(), IDS_SIGNER_SUBJECT, szSignColumn, 256);
        lstrcpynW(psci->wszTitle, T2W(szSignColumn), MAX_COLUMN_NAME_LEN);
        break;

从文件中读取得数字签名信息显示在签名人姓名列中。

        case 1:
            varData = szSignerSubject;
            break;

创建资源管理器中的签名时间列。

    case 2:
        psci->scid.fmtid = CLSID_CodeSignShell; // Use our CLSID as the format ID
        psci->scid.pid   = 2;                   // Use the column # as the ID
        psci->vt         = VT_BSTR;             // We'll return the data as a string
        psci->fmt        = LVCFMT_LEFT;         // Text will be left-aligned
        psci->csFlags    = SHCOLSTATE_TYPE_STR; // Data should be sorted as strings
        psci->cChars     = 24;                  // Default col width in chars
        LoadString(_Module.GetResourceInstance(), IDS_SIGNING_TIME, szSignColumn, 256);
        lstrcpynW(psci->wszTitle, T2W(szSignColumn), MAX_COLUMN_NAME_LEN);
        break;

从文件中读取的数字签名信息显示在签名时间列中。

        case 2:
            varData = szSigningTime;
            break;

这样就完成了本插件的显示数字签名部分。

添加代码签名

给PE文件添加代码签名有很多方法,这里我们用一个最简单的做法:调用微软的signcode命令行工具。

signcode.exe是微软的代码签名软件,专门给Windows平台下的PE文件添加数字签名,它并不包含在任何Windows的发行版本中,需要从微软官方网站下载。

这是一个纯命令行工具,有很多参数来指示它工作。在本插件中,提供了一个简单的对话框界面,通过调用signcode的一些主要参数来完成签名。

下面将结合代码来介绍这些参数的使用。

对话框界面如下:

从上至下的输入/选择框分别是:

signcode.exe:signcode.exe文件所在的位置,因为此文件并不是Windows系统文件,必须手工指示其位置。点击OK按钮后,程序将用WinExec函数调用此软件。

                    WinExec(pszSignCodeExec, SW_HIDE);

Certificate:选择证书,证书颁发商颁发的证书需要导入到"MY"证书存储区域,一般安装好后的可签名证书都存储在此区域内。

对应的代码是对话框初始化时,从"MY"证书存储区域中获取所有可签名证书,添加到此组合框中。

        int iCurrentCertificate = 0;
        HANDLE hStoreHandle;

        hStoreHandle = CertOpenSystemStore(NULL, _T("MY"));
        if( hStoreHandle != NULL )
        {
            int iCertificate;
            PCCERT_CONTEXT pCertContext = NULL;
            TCHAR szCertName[256];

            while( pCertContext = CertEnumCertificatesInStore(hStoreHandle, pCertContext) )
            {
                CertGetNameString(pCertContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, NULL, szCertName, 256);
                iCertificate = ::SendMessage(GetDlgItem(IDC_COMBO_CERTIFICATE), CB_ADDSTRING, (WPARAM)0, (LPARAM)szCertName);
                ::SendMessage(GetDlgItem(IDC_COMBO_CERTIFICATE), CB_SETITEMDATA, (WPARAM)iCertificate, (LPARAM)_tcsdup(szCertName));
                if( _tcscmp(szCertName, szCertificate) == 0 )
                    iCurrentCertificate = iCertificate;
            }
            CertCloseStore(hStoreHandle, 0);
        }

而签名时选中的证书名称将成为signcode的-cn参数。

                    sprintf(pszSignCodeExec, "%s -cn \"%s\" ", T2A(szFilePath), T2A(pszCertificate));

Timestamp URL:签名时间戳URL,是互联网上提供的具有公信力的签名时间服务。因为签名时间并不是你想设为什么时间就设为什么时间,所以并不是从本地读取的时间。签名时间戳URL可以用verisign的,globalsign的,wosign的等等。此输入框中的信息最终会成为signcode的-t参数。

                        strcat(pszSignCodeExec, "-t \"");
                        strcat(pszSignCodeExec, T2A(szTimeStampURL));
                        strcat(pszSignCodeExec, "\" ");

Description:签名描述,此输入框中的信息最终会成为signcode的-n参数。

                        strcat(pszSignCodeExec, "-n \"");
                        strcat(pszSignCodeExec, T2A(szDescription));
                        strcat(pszSignCodeExec, "\" ");

Web location:签名网址,可以添上你的网站,此输入框中的信息最终会成为signcode的-i参数。

                        strcat(pszSignCodeExec, "-i \"");
                        strcat(pszSignCodeExec, T2A(szWebLocation));
                        strcat(pszSignCodeExec, "\" ");

这些就是完成一次代码签名的主要参数。点击对话框上的OK按钮,插件把构造好的命令行字符串作为参数传递给WinExec,即完成签名。

                    WinExec(pszSignCodeExec, SW_HIDE);

接下来看怎样在资源管理器中调用此签名对话框。

在插件的注册表中注册资源管理器右键菜单。

    NoRemove *
    {
        NoRemove shellex
        {
            NoRemove ContextMenuHandlers
            {
                ForceRemove CodeSignShell = s '{F9A759D1-96E5-41A1-BF48-E844611DD16F}'
            }
        }
    }

让我们的扩展模块实现IContextMenu接口。

// IContextMenu
public:
    STDMETHOD (GetCommandString)(UINT idCmd, UINT uType, UINT* pwReserved, LPSTR pszName, UINT cchMax);
    STDMETHOD (InvokeCommand)(LPCMINVOKECOMMANDINFO lpici); 
    STDMETHOD (QueryContextMenu)(HMENU hmenu, UINT indexMenu, UINT idCmdFirst, UINT idCmdLast, UINT uFlags);

此接口实现后,我们的插件将给资源管理器里的每一个文件提供关联菜单项。菜单项的信息由此接口的QueryContextMenu函数提供。

    TCHAR szSignMenu[256];

    if( m_vstrFiles.size() > 1 )
        LoadString(_Module.GetResourceInstance(), IDS_SIGNMENU_MORE_THAN_ONE_FILE, szSignMenu, 256);
    else if( m_vstrFiles.size() == 1 )
        LoadString(_Module.GetResourceInstance(), IDS_SIGNMENU_ONE_FILE, szSignMenu, 256);
    else
        return MAKE_HRESULT (SEVERITY_SUCCESS, FACILITY_NULL, 0);
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uidFirstCmd, szSignMenu);

菜单名根据选中的是一个文件还是多个文件有所不同。一个文件菜单名是Sign it...,多个文件菜单名是Sign them...。不管是什么名称,此菜单项完成的动作都是调用上述签名对话框。

    if( m_hbmSignMenu != NULL )
        SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_hbmSignMenu, NULL);

菜单项还带有一个证书小图标

,提高辨识度。

最终,在资源管理器中选中一个或多个文件,点击鼠标右键弹出其关联菜单,将出现我们插件中提供的菜单项。如下图选中了三个文件,从上往下第三个菜单项 Sign them...既是。

点击此菜单项,选中的文件将从插件的IShellExtInit::Initialize接口函数中取得。IShellExtInit接口是所有资源管理器插件必须要实现的接口,所以我们就把要处理的文件和处理过程,也就是签名对话框关联起来了。

    for( int nFile = 0; nFile < uNumFiles; nFile++ )
    {
        TCHAR szFile[MAX_PATH];

        DragQueryFile(hDrop, nFile, szFile, MAX_PATH);
        m_vstrFiles.push_back(szFile);
    }

把所有选中文件存储到m_vstrFiles成员变量中,并在启动启动对话框时作为构造函数参数传递给它。

    CSignFileDlg dlgSignFile(m_vstrFiles);

最后点击签名对话框OK按钮后会对m_vstrFiles中每一个文件进行签名。

                for( int nFile = 0; nFile < m_vstrFiles.size(); nFile++ )

通过资源管理器的文件关联菜单添加代码签名的主要流程就是这样。