签名不等于可信:详解PE数字签名校验的漏洞与主动规避方案

简介: 本文探讨了CVE-2013-3900漏洞的原理及其影响,该漏洞允许攻击者在不破坏数字签名有效性的情况下,向PE文件中添加恶意代码。漏洞源于Windows对签名数据后附加数据的校验缺失,导致恶意软件可伪装成合法软件。文章分析了WinVerifyTrust函数的工作机制及修复方法,包括通过注册表启用严格签名校验(EnableCertPaddingCheck)。同时,提出了通过hook注册表函数主动规避漏洞的方法,确保安全软件在未启用严格校验时仍能检测潜在威胁。此研究对提升PE文件签名安全性具有重要意义。

最近看到一款开源软件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注册表项。

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 理论基础

1.png

如上图[3]所示,当使用 Authenticode 对 Windows PE 文件进行签名时,计算文件 Authenticode hash的算法会排除PE中的三部分内容,这样在将签名嵌入文件时,就可以修改这些内容,而不会影响文件的hash。

1.OptionalHeader中的CheckSum字段

即OptionalHeader.CheckSum,会计算整个文件数据的校验和[4],所以加签名后,这个值会发生变动,不能包含在签名hash计算中。

2. 数据目录表中的Certificate Table Entry

也叫Security Directory Entry,在目录表的第4项(从0开始),即OptionalHeader.DataDirectory[4]。目录表中每一个Entry标识一类数据的位置和大小,Entry的结构都一样:VirtualAddress字段标识对应数据在文件中的位置,Size字段标识对应数据的大小。注意:数据目录表中Entry的VirtualAddress字段通常是指RVA,但对于Certificate Table Entry,VirtualAddress对应的是文件偏移,不是RVA。
2.png

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

3. 签名数据区

即Certificate Table Entry指向的内容,也叫Attribute Certificate Table[5],包括文件hash、证书信息、签名时间等。Attribute Certificate Table对应的是一个WIN_CERTIFICATE的结构:

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末尾填充0

wRevision:WIN_CERTIFICATE结构的版本,一般是WIN_CERT_REVISION_2_0,数值为0x0200

wCertificateType:证书类型,用于标识后面的bCertificate中存储的签名数据结构,Authenticode只支持WIN_CERT_TYPE_PKCS_SIGNED_DATA,即PKCS#7 SignedData

bCertificate:变长数组,存放签名数据,结构为PKCS#7 SignedData
3.png
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主动开启:

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
4.png
svchost2.exe:末尾追加全0数据,追加了0x10的数据0,并修改签名数据的长度字段
5.png
当注册表中不存在Wintrust\Config,或者不存在EnableCertPaddingCheck,或者EnableCertPaddingCheck值为0时,三个文件的签名校验都成功:
6.png
在注册表中添加并启用EnableCertPaddingCheck,svchost.exe仍然校验成功;svchost1.exe的校验失败,说明附加数据中如果有非0的,则不符合Authenticode的签名规范,将视其为未签名的;svchost2.exe校验成功,说明可以追加数据,但追加数据只能是全0数据,全0数据没意义,不会产生安全问题。

7.png
3.3 主动规避
上述修复方法需要用户手动开启严格签名校验才能规避掉这个漏洞,站在安全软件(杀软、EDR、反作弊系统等)的角度,为了保证严格签名校验的能力,不能只是被动依赖用户去规避,需要有主动规避的方法。

举个例子,在游戏反作弊中,有很多检测依赖对文件的签名校验,比如游戏进程内加载dll时,判断dll是否有签名,没有签名就禁止加载。但外挂用户和反作弊系统是对立的,如果外挂利用了这个漏洞,那基本上可以认定用户机器上严格签名校验是被外挂或外挂用户禁用了的,在这种场景下,需要主动规避方法来启用严格签名校验。

最先想到的方法是不使用系统的WinVerifyTrust验证签名,自己实现一套验证代码,理论上可行,但工作量有点大,还有没有更简单点的?

既然严格签名校验需要注册表项才能开启,那可以推测WinVerifyTrust中可能会去访问注册表中的EnableCertPaddingCheck来判断是否开启严格校验,如果严格校验的开关是在进程层面生效的话,那我们就可以在安全软件进程或游戏进程中伪造注册表的信息,让WinVerifyTrust认为EnableCertPaddingCheck存在并已经开启,从而主动规避签名验证的漏洞,而伪造的方法就是通过hook注册表相关的函数。

先写个测试程序,调用WinVerifyTrust验证文件签名,用ProcMon观察下注册表的访问操作:

// 调用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;
}
8.png
可以看到,测试程序中,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时伪造的,直接返回

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, _Inopt LPCWSTR lpSubKey,
_Inopt 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, _Inopt LPCWSTR lpValueName,
Reserved LPDWORD lpReserved, _Outopt LPDWORD lpType,
_Outopt LPBYTE lpData, _Inoutopt 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 [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的情况下,校验签名失败
9.png
所以通过hook注册表函数伪造EnableCertPaddingCheck的方法可行,这种方式能使安全软件在系统未启用严格签名校验时,也能够使用严格签名校验,及时发现安全威胁。但这种方法只能在win10和win11上起效,因为系统中WinVerifyTrust已经支持进行严格签名校验,如果是win10之前的系统,需要安装MS13-098 KB2893294更新后,才能有效,否则即使hook了也没效果。

相关文章
|
Ubuntu 编译器 开发工具
|
10月前
|
安全 搜索推荐 BI
AD域组策略管理
本文介绍了Windows AD域控组策略的设置方法,包括账号登录记录、限制电脑登录账号、禁用cmd和powershell、屏保与锁屏设置、禁用组策略、限制打开“用户和组”等。此外,还推荐了ADManagerPlus这一基于Web的AD管理工具,它能够简化GPO的创建、编辑、链接、执行及删除等操作,提高IT管理效率。
761 0
|
存储 机器学习/深度学习 人工智能
【AI大模型】Transformers大模型库(十六):safetensors存储类型
【AI大模型】Transformers大模型库(十六):safetensors存储类型
1157 0
|
关系型数据库 MySQL 数据库连接
解决在eclipse2021中,用mysql-connector-java-8.0.18.jar不兼容,导致无法访问数据库问题
解决在eclipse2021中,用mysql-connector-java-8.0.18.jar不兼容,导致无法访问数据库问题
485 0
|
缓存 关系型数据库 MySQL
MySQL登录时出现Access denied for user ‘root‘@‘localhost‘ (using password: YES)无法打开的解决方法
MySQL登录时出现Access denied for user ‘root‘@‘localhost‘ (using password: YES)无法打开的解决方法
22199 0
|
机器学习/深度学习 自然语言处理 API
使用 Python 集成 ChatGPT API
使用 Python 集成 ChatGPT API
760 1
|
监控 负载均衡 网络协议
一文带你浅入浅出Keepalived
一文带你浅入浅出Keepalived
|
SQL 存储 分布式计算
Data Lake 三剑客——Delta、Hudi、Iceberg 对比分析
定性上讲,三者均为 Data Lake 的数据存储中间层,其数据管理的功能均是基于一系列的 meta 文件。meta 文件的角色类似于数据库的 catalog/wal,起到 schema 管理、事务管理和数据管理的功能。
17957 3
Data Lake 三剑客——Delta、Hudi、Iceberg 对比分析
|
存储 人工智能 物联网
AI数字人无人直播/真人直播系统开发详细功能/方案设计/案例部署/源码设计
  区块链、人工智能、数字孪生、人机交互、物联网等面向数据的新一代信息技术的演进并非偶然,而是从Web2.0向Web3.0演进的技术准备。从技术上来看,元宇宙是基于Web3.0技术体系和运作机制支撑下的可信数字化价值交互网络,是以区块链为核心的Web3.0数字新生态。元宇宙是以区块链为核心的Web3.0技术体系支撑下的新场景、新产业和新生态,将会在数字环境下催生大量创新商业模式,形成数字空间新范式。