最近看到一款开源软件SigFlip[1],可以向签名后的PE文件中添加恶意代码,但是不会影响签名的有效性。这有点反直觉,因为一般认为PE文件签名后就不能再更改了,否则签名就会失效,所以对这个工具产生了些好奇,研究后发现,这是源于Windows的CVE-2013-3900漏洞,这个工具可以看作是该漏洞的一个POC。本文就结合这个漏洞看一看其背后的原理以及如何进行主动规避。
一、CVE-2013-3900
在数字安全领域,PE文件的代码签名长期被视为软件完整性与可信度的"安全封条"。通过哈希算法与证书链验证,这种机制理论上可确保文件自签名后未经篡改,且来源可追溯至可信发布者。然而,CVE-2013-3900漏洞的发现彻底暴露了这一信任体系的致命缺陷------攻击者可在不破坏原有数字签名的前提下,向合法签名的PE文件植入任意数据。
CVE-2013-3900[2]是一个关于Windows Authenticode代码签名机制的漏洞,主要影响 WinVerifyTrust
函数对PE 文件的签名验证功能。该漏洞源于WinVerifyTrust
在验证PE文件的数字签名时,没有校验签名数据后的附加数据。攻击者可以利用此漏洞,在签名数据后追加恶意代码并修改签名数据的长度字段,基于文件hash的算法,追加的恶意代码会被看作是签名数据的一部分,会被排除在文件hash的计算之外,文件hash计算结果一样,不会破坏原有签名的有效性。
此漏洞的危险性在于,它使得恶意软件能够伪装成经过验证的合法软件(具有有效的代码签名),从而绕过系统的安全检查。攻击者可以通过电子邮件、恶意网站或供应链攻击等方式,将特制的 PE 文件传播给用户,诱导用户运行这些文件。微软在 2013 年首次披露该漏洞,并提供了修复方案,但该修复方案需要用户手动启用。修复方法是通过修改注册表,添加并启用 EnableCertPaddingCheck
注册表项。
Plain
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config]
"EnableCertPaddingCheck"=dword:1
[HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Cryptography\Wintrust\Config]
"EnableCertPaddingCheck"=dword:1
二、漏洞的原理
2.1 理论基础

如上图[3]所示,当使用 Authenticode 对 Windows PE 文件进行签名时,计算文件 Authenticode hash的算法会排除PE中的三部分内容,这样在将签名嵌入文件时,就可以修改这些内容,而不会影响文件的hash。
OptionalHeader
中的CheckSum
字段
即OptionalHeader.CheckSum
,会计算整个文件数据的校验和[4],所以加签名后,这个值会发生变动,不能包含在签名hash计算中。
- 数据目录表中的
Certificate Table Entry
也叫Security Directory Entry
,在目录表的第4项(从0开始),即OptionalHeader.DataDirectory[4]
。目录表中每一个Entry标识一类数据的位置和大小,Entry的结构都一样:VirtualAddress
字段标识对应数据在文件中的位置,Size
字段标识对应数据的大小。注意:数据目录表中Entry的VirtualAddress
字段通常是指RVA ,但对于Certificate Table Entry
,VirtualAddress
对应的是文件偏移,不是RVA。

C++
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- 签名数据区
即Certificate Table Entry
指向的内容,也叫Attribute Certificate Table
[5],包括文件hash、证书信息、签名时间等。Attribute Certificate Table
对应的是一个WIN_CERTIFICATE
的结构:
C++
typedef struct _WIN_CERTIFICATE {
DWORD dwLength;
WORD wRevision;
WORD wCertificateType; // WIN_CERT_TYPE_xxx
BYTE bCertificate[ANYSIZE_ARRAY];
} WIN_CERTIFICATE, *LPWIN_CERTIFICATE;
dwLength
:整个WIN_CERTIFICATE
的大小,包括变长的bCertificate
,按8字节对齐 ,不足的在bCertificate
末尾填充0wRevision
:WIN_CERTIFICATE
结构的版本,一般是WIN_CERT_REVISION_2_0
,数值为0x0200wCertificateType
:证书类型,用于标识后面的bCertificate
中存储的签名数据结构,Authenticode只支持WIN_CERT_TYPE_PKCS_SIGNED_DATA
,即PKCS#7 SignedData
bCertificate
:变长数组,存放签名数据,结构为PKCS#7 SignedData

Checksum
和Certificate Table Entry
的空间大小是固定的,不能用来添加恶意代码,而Attribute Certificate Table
的大小是不固定的,而且大小是写在PE的某些字段中的,可以被修改,这就给了我们进行"合法"篡改签名文件的可能:我们把恶意代码追加到Attribute Certificate Table
签名数据之后,然后更新PE中描述其数据长度的字段,在计算PE hash时,就可以略过我们追加的代码,保证计算的hash不变。
2.2 篡改签名文件的步骤
- 通过
Certificate table Entry
,找到Attribute Certificate Table
位置和大小 - 在签名数据后追加恶意代码,因为签名都是在PE文件生成之后添加的,所以为了避免影响已经确定的文件布局,签名数据一般是附在文件末尾,所以在签名数据后追加,也就是在文件末尾追加
- 更新签名数据的长度:
WIN_CERTIFICATE.dwLength
和OptionalHeader.DataDirectory[4].Size
- 重新计算文件的checksum并更新到
OptionalHeader.CheckSum
。不更新checksum也不会影响签名,但有的检测会校验checksum值,所以最好也更新下
2.3 局限性
通过上面的步骤,我们可以将恶意代码打包到签名的PE文件而不影响文件的签名,但这样仅仅能把恶意代码带入到目标系统中,它并不能自主运行,单点漏洞不能产生影响,要想执行这些恶意代码,还需要借助别的手段。
比如,3CXDesktopApp的供应链攻击事件中,3CXDesktopApp的构建环境事先被攻破,导致构建后的安装包携带了恶意文件d3dcompiler_47.dll
和ffmpeg.dll
[6]。攻击者利用上述漏洞在d3dcompiler_47.dll
签名后追加了shellcode,但不影响签名;并重新编译了ffmpeg.dll
,在其DllMain
中加入了额外的逻辑去读取、解密和执行d3dcompiler_47.dll
中添加的shellcode
。当3CXDesktopApp加载ffmpeg.dll
时,就会触发shellcode的执行[7]。
三、如何修复或规避
3.1 修复方式
漏洞的原因在于使用WinVerifyTrust
验证签名时没有考虑签名的附加数据(即真实签名数据之后的额外数据),因此修复的话需要调整WinVerifyTrust
的实现。在win10之前的系统上,需要安装MS13-098 KB2893294 更新[8],win10和win11已经修复,不需要另外安装更新。
修复后的版本中,WinVerifyTrust
可以支持更严格的签名校验,会检测签名数据中的附加数据,如果不符合Authenticode签名规范,则会被认定为未签名的。但微软没有默认开启 这个严格模式,需要用户添加注册表项EnableCertPaddingCheck
主动开启:
Plain
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\Software\Microsoft\Cryptography\Wintrust\Config]
"EnableCertPaddingCheck"=dword:1
[HKEY_LOCAL_MACHINE\Software\Wow6432Node\Microsoft\Cryptography\Wintrust\Config]
"EnableCertPaddingCheck"=dword:1
3.2 修复效果对比
基于svchost.exe ,修改两个副本,来对比开启EnableCertPaddingCheck
前后的签名验证结果:
-
svchost1.exe:不修改长度,只修改对齐的填充值,正常对齐的填充值为0,把它们改成非0
-
svchost2.exe:末尾追加全0数据,追加了0x10的数据0,并修改签名数据的长度字段
当注册表中不存在Wintrust\Config
,或者不存在EnableCertPaddingCheck
,或者EnableCertPaddingCheck
值为0时,三个文件的签名校验都成功:

在注册表中添加并启用EnableCertPaddingCheck
,svchost.exe 仍然校验成功;svchost1.exe 的校验失败,说明附加数据中如果有非0的,则不符合Authenticode的签名规范,将视其为未签名的;svchost2.exe校验成功,说明可以追加数据,但追加数据只能是全0数据,全0数据没意义,不会产生安全问题。

3.3 主动规避
上述修复方法需要用户手动开启严格签名校验才能规避掉这个漏洞,站在安全软件(杀软、EDR、反作弊系统等)的角度,为了保证严格签名校验的能力,不能只是被动依赖用户去规避,需要有主动规避的方法。
举个例子,在游戏反作弊中,有很多检测依赖对文件的签名校验,比如游戏进程内加载dll时,判断dll是否有签名,没有签名就禁止加载。但外挂用户和反作弊系统是对立的,如果外挂利用了这个漏洞,那基本上可以认定用户机器上严格签名校验是被外挂或外挂用户禁用了的,在这种场景下,需要主动规避方法来启用严格签名校验。
最先想到的方法是不使用系统的WinVerifyTrust
验证签名,自己实现一套验证代码,理论上可行,但工作量有点大,还有没有更简单点的?
既然严格签名校验需要注册表项才能开启,那可以推测WinVerifyTrust
中可能会去访问注册表中的EnableCertPaddingCheck
来判断是否开启严格校验,如果严格校验的开关是在进程层面生效的话,那我们就可以在安全软件进程或游戏进程中伪造注册表的信息 ,让WinVerifyTrust
认为EnableCertPaddingCheck
存在并已经开启,从而主动规避签名验证的漏洞,而伪造的方法就是通过hook注册表相关的函数。
先写个测试程序,调用WinVerifyTrust
验证文件签名,用ProcMon
观察下注册表的访问操作:
C++
// 调用WinVerifyTrust验证文件签名
bool verify(const wchar_t *path) {
WINTRUST_FILE_INFO wfi = {0};
wfi.cbStruct = sizeof(wfi);
wfi.pcwszFilePath = path;
wfi.hFile = nullptr;
wfi.pgKnownSubject = nullptr;
GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA wd = {0};
wd.cbStruct = sizeof(wd);
wd.pPolicyCallbackData = nullptr;
wd.pSIPClientData = nullptr;
wd.dwUIChoice = WTD_UI_NONE;
wd.fdwRevocationChecks = WTD_REVOKE_NONE;
wd.dwUnionChoice = WTD_CHOICE_FILE;
wd.dwStateAction = WTD_STATEACTION_VERIFY;
wd.hWVTStateData = nullptr;
wd.pwszURLReference = nullptr;
wd.dwUIContext = 0;
wd.pFile = &wfi;
bool isSigned = false;
if (WinVerifyTrust(nullptr, &action, &wd) == 0) {
isSigned = true;
}
wd.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(nullptr, &action, &wd);
return isSigned;
}

可以看到,测试程序中,WinVerifyTrust
会先去尝试打开注册表项 HKLM\Software\WOW6432Node\Microsoft\Cryptography\Wintrust\Config
,如果找不到,认为禁用强签名校验,svchost1.exe 校验成功;如果存在,会再去读取 EnableCertPaddingCheck
的值,1为开启,svchost1.exe 校验失败;最后关闭注册表项的句柄。所以严格校验的开关应该就是在进程层面生效,为了伪造EnableCertPaddingCheck
,我们需要hook open、query、close这三个注册表函数。
- open函数的hook中,如果open的是
HKLM\Software\WOW6432Node\Microsoft\Cryptography\Wintrust\Config
,且没找到,返回一个假句柄,让调用方认为open成功,这个句柄稍后在close时用到。 - query函数的hook中,如果查询的键是
EnableCertPaddingCheck
,直接修改返回结果,让调用方认为系统中已启用EnableCertPaddingCheck
。 - close函数的hook中,处理伪造的句柄,如果关闭的句柄是之前open时伪造的,直接返回
C++
typedef decltype(RegOpenKeyExW) *T_RegOpenKeyExW;
typedef decltype(RegQueryValueExW) *T_RegQueryValueExW;
typedef decltype(RegCloseKey) *T_RegCloseKey;
T_RegOpenKeyExW g_RegOpenKeyExW = nullptr;
T_RegQueryValueExW g_RegQueryValueExW = nullptr;
T_RegCloseKey g_RegCloseKey = nullptr;
#define FAKE_KEY_HANDLE (HKEY)0xfffffffe
LSTATUS WINAPI hookRegOpenKeyExW(_In_ HKEY hKey, _In_opt_ LPCWSTR lpSubKey,
_In_opt_ DWORD ulOptions, _In_ REGSAM samDesired,
_Out_ PHKEY phkResult) {
LSTATUS ls = g_RegOpenKeyExW(hKey, lpSubKey, ulOptions, samDesired, phkResult);
if (ls == ERROR_SUCCESS) {
return ls;
}
// 只在找不到Wintrust\\Config注册表项时进行伪造
if (ls == ERROR_FILE_NOT_FOUND && hKey == HKEY_LOCAL_MACHINE && lpSubKey != nullptr &&
_wcsicmp(lpSubKey, L"SOFTWARE\\Microsoft\\Cryptography\\Wintrust\\Config") == 0) {
*phkResult = FAKE_KEY_HANDLE; // 返回假的句柄,让调用方认为注册表项存在
return ERROR_SUCCESS;
}
return ls;
}
LSTATUS WINAPI hookRegQueryValueExW(_In_ HKEY hKey, _In_opt_ LPCWSTR lpValueName,
_Reserved_ LPDWORD lpReserved, _Out_opt_ LPDWORD lpType,
_Out_opt_ LPBYTE lpData, _Inout_opt_ LPDWORD lpcbData) {
// 当查询EnableCertPaddingCheck的值时,直接修改返回结果,让调用方认为已启用EnableCertPaddingCheck
if (lpValueName != nullptr && _wcsicmp(lpValueName, L"EnableCertPaddingCheck") == 0) {
if (lpType != nullptr) {
*lpType = REG_DWORD;
}
if (lpData != nullptr) {
*(DWORD *)lpData = 1;
}
if (lpcbData != nullptr) {
*lpcbData = sizeof(DWORD);
}
return ERROR_SUCCESS;
}
return g_RegQueryValueExW(hKey, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
LSTATUS WINAPI hookRegCloseKey(_In_ HKEY hKey) {
// 如果关闭的伪造的注册表项句柄,直接返回
if (hKey == FAKE_KEY_HANDLE) {
return ERROR_SUCCESS;
}
return g_RegCloseKey(hKey);
}
// hook注册表函数
bool installHook() {
HMODULE hKernelBase = GetModuleHandleW(L"kernelbase.dll");
if (hKernelBase == nullptr) {
return false;
}
g_RegOpenKeyExW = (T_RegOpenKeyExW)(GetProcAddress(hKernelBase, "RegOpenKeyExW"));
g_RegQueryValueExW = (T_RegQueryValueExW)(GetProcAddress(hKernelBase, "RegQueryValueExW"));
g_RegCloseKey = (T_RegCloseKey)(GetProcAddress(hKernelBase, "RegCloseKey"));
distormx_begin_defer();
distormx_hook((void **)&g_RegOpenKeyExW, hookRegOpenKeyExW);
distormx_hook((void **)&g_RegQueryValueExW, hookRegQueryValueExW);
distormx_hook((void **)&g_RegCloseKey, hookRegCloseKey);
int hookOK = distormx_commit();
distormx_abort_defer();
return hookOK > 0;
}
int wmain(int argc, wchar_t *argv[]) {
if (argc < 2) {
wprintf(L"Usage: %s <file> [hook]\n", argv[0]);
return 1;
}
const wchar_t *path = argv[1];
// 根据命令行参数个数判断是否开启hook
if (argc > 2) {
if (installHook()) {
printf("Install hook OK\n");
} else {
printf("Install hook failed\n");
}
}
if (verify(path)) {
wprintf(L"%s is signed.\n", path);
} else {
wprintf(L"%s is unsigned.\n", path);
}
return 0;
}
效果验证:删除Wintrust\config
注册表项,禁用严格签名校验,对上面修改附加数据的svchost1.exe进行验证
- signtool工具,校验签名成功
- 测试程序,不开启hook的情况下,校验签名成功
- 测试程序,开启hook的情况下,校验签名失败

所以通过hook注册表函数伪造EnableCertPaddingCheck
的方法可行,这种方式能使安全软件在系统未启用严格签名校验时,也能够使用严格签名校验,及时发现安全威胁。但这种方法只能在win10 和win11 上起效,因为系统中WinVerifyTrust
已经支持进行严格签名校验,如果是win10 之前的系统,需要安装MS13-098 KB2893294更新后,才能有效,否则即使hook了也没效果。
参考
- SigFlip, https://github.com/med0x2e/SigFlip
- WinVerifyTrust 签名验证漏洞, https://msrc.microsoft.com/update-guide/vulnerability/CVE-2013-3900
- Windows Authenticode Portable Executable Signature Format, https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/Authenticode_PE.docx
- PE Checksum Algorithm的较简实现, https://www.cnblogs.com/concurrency/p/3926698.html
- PE Format, #The Attribute Certificate Table (Image Only), https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only
- 3CXDesktopApp供应链攻击分析, https://www.freebuf.com/articles/network/366367.html
- 3CXDesktopApp遭遇APT组织供应链攻击分析报告, https://www.freebuf.com/articles/system/362686.html
- Microsoft Security Bulletin MS13-098 - Critical, https://learn.microsoft.com/en-us/security-updates/Securitybulletins/2013/ms13-098?redirectedfrom=MSDNhtml