3

ExplorerFrame.dll 的 BUG可能导致不良设计的用户程序危害系统稳定

 1 year ago
source link: https://orbit.blog.csdn.net/article/details/125001297
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ExplorerFrame.dll 动态链接库中的 GetInfoTipEx 函数实现缺陷,可能导致不良设计的用户代码破坏系统稳定性。如果用户程序对 IQueryInfo 接口的GetInfoTip方法实现不当,可能会触发GetInfoTipEx的缺陷,导致 explorer.exe 异常退出。

1. 问题发现

最近发现在 Windows 10 操作系统的桌面创建快捷方式的时候,总是时不时地会导致资源管理器进程explorer.exe 异常退出,经过一番摸索,发现做以下操作时,问题出现概率比较高:

  1. 在桌面单击鼠标右键,在右键菜单中选择“新建” -> “快捷方式”

  2. 在“创建快捷方式”窗口中点击“浏览”按钮

  3. 在“浏览”窗口中选择目标程序,鼠标在程序上停留一会儿,等待弹出 “info tip” 信息

不关闭窗口,重复第 3 步的操作多次后,有大概率会导致 explorer.exe 异常退出。

这让我想起了早先的“震网”病毒,但那都是几年前的事情了,难道最新的 Windows 10 没有补上这个漏洞?于是我就用 Windbg 挂上 explorer.exe,然后做复现操作,发现异常原因是 CoTaskMemFree 释放了一个无效的指针触发的异常:

(13e4.1004): Break instruction exception - code 80000003 (first chance)
ntdll!RtlReportCriticalFailure+0x56:
00007ffb`6dde83fa cc              int     3

异常触发时的函数调用栈信息如下:

0:002> kb
00 00007ffb`6ddef97a : 00000000`ffffffff 00007ffb`6de4c6e0 00000000`02d60000 00000000`02d60000 : ntdll!RtlReportCriticalFailure+0x56
01 00007ffb`6dd8fc52 : 00000000`02d60000 00000000`077eae50 00000000`02d60000 00007ffb`6a3441a9 : ntdll!RtlpHeapHandleError+0x12
02 00007ffb`6ddaa260 : 00000000`00000001 00000000`0c34afd0 00007ffb`5523eeb8 00000000`00000000 : ntdll!RtlpLogHeapFailure+0x96
03 00007ffb`55208672 : 00000000`00000001 00000000`00000000 00000000`077eb8d0 00007ffb`6c4fc59e : ntdll!RtlFreeHeap+0x7b5a0
04 00007ffb`551aa7eb : 00000000`077eb8d0 00000000`00000000 00000000`0c34b05b 00000000`077eae50 : explorerframe!GetInfoTipEx+0x9a
05 00007ffb`55158062 : 00000000`00000000 00000000`00000000 00000000`0000ea60 00007ffb`6a370029 : explorerframe!CNscInfoTipTask::InternalResumeRT+0x5b
.... // 省略了一部分

2. 问题分析

看调用栈信息是GetInfoTipEx 函数中的内存释放操作触发的异常,于是把 GetInfoTipEx 反汇编出来,发现这个函数很短,简单分析了一下(加了点注释):

explorerframe!GetInfoTipEx:
00007ffe`ee2885d8 4c8bdc          mov     r11,rsp
00007ffe`ee2885db 49895b08        mov     qword ptr [r11+8],rbx
00007ffe`ee2885df 49897310        mov     qword ptr [r11+10h],rsi
00007ffe`ee2885e3 4d894318        mov     qword ptr [r11+18h],r8
00007ffe`ee2885e7 57              push    rdi
00007ffe`ee2885e8 4883ec50        sub     rsp,50h
00007ffe`ee2885ec 33db            xor     ebx,ebx
00007ffe`ee2885ee 498bf9          mov     rdi,r9
00007ffe`ee2885f1 66418919        mov     word ptr [r9],bx
00007ffe`ee2885f5 8bf2            mov     esi,edx
00007ffe`ee2885f7 4d85c0          test    r8,r8
00007ffe`ee2885fa 0f8484000000    je      explorerframe!GetInfoTipEx+0xac (00007ffe`ee288684)
00007ffe`ee288600 488b01          mov     rax,qword ptr [rcx]   //rcx是参数传入的IShellFolder 对象,第一个位置是 vtbl 的指针
00007ffe`ee288603 498d53e8        lea     rdx,[r11-18h]
00007ffe`ee288607 498953d8        mov     qword ptr [r11-28h],rdx
00007ffe`ee28860b 4d8d4b18        lea     r9,[r11+18h]
00007ffe`ee28860f 488d15225f0400  lea     rdx,[explorerframe!GUID_00021500_0000_0000_c000_000000000046 (00007ffe`ee2ce538)]  // IID_IQueryInfo 的 RIID 定义
00007ffe`ee288616 49895bd0        mov     qword ptr [r11-30h],rbx
00007ffe`ee28861a 498953c8        mov     qword ptr [r11-38h],rdx
00007ffe`ee28861e 448d4301        lea     r8d,[rbx+1]
00007ffe`ee288622 488b4050        mov     rax,qword ptr [rax+50h]  //rax 是IShellFolder 对象的vtbl,偏移 50H 是 CFSFolder::GetUIObjectOf 接口方法
00007ffe`ee288626 33d2            xor     edx,edx   // 第二个参数 cidl
00007ffe`ee288628 ff15fad80200    call    qword ptr [explorerframe!_guard_dispatch_icall_fptr (00007ffe`ee2b5f28)]  //call IShellFolder::GetUIObjectOf()
00007ffe`ee28862e 85c0            test    eax,eax
00007ffe`ee288630 7852            js      explorerframe!GetInfoTipEx+0xac (00007ffe`ee288684)
00007ffe`ee288632 488b4c2440      mov     rcx,qword ptr [rsp+40h]
00007ffe`ee288637 4c8d442478      lea     r8,[rsp+78h]  //局部变量取地址,(作为 GetInfoTip 的第二个参数)GetInfoTip 函数会申请一块内存,然后把地址存在这个位置
00007ffe`ee28863c 8bd6            mov     edx,esi
00007ffe`ee28863e 488b01          mov     rax,qword ptr [rcx]  //rcx 是IQueryInfo 对象指针
00007ffe`ee288641 488b4018        mov     rax,qword ptr [rax+18h]  //rax 是IQueryInfo::GetInfoTip 接口方法
00007ffe`ee288645 ff15ddd80200    call    qword ptr [explorerframe!_guard_dispatch_icall_fptr (00007ffe`ee2b5f28)]
00007ffe`ee28864b 4c8b442478      mov     r8,qword ptr [rsp+78h]  
00007ffe`ee288650 4d85c0          test    r8,r8
00007ffe`ee288653 741d            je      explorerframe!GetInfoTipEx+0x9a (00007ffe`ee288672)
00007ffe`ee288655 ba04010000      mov     edx,104h
00007ffe`ee28865a 488bcf          mov     rcx,rdi
00007ffe`ee28865d bb01000000      mov     ebx,1
00007ffe`ee288662 e86d35eaff      call    explorerframe!StringCchCopyW (00007ffe`ee12bbd4)
00007ffe`ee288667 488b4c2478      mov     rcx,qword ptr [rsp+78h]  //释放GetInfoTip 函数内部申请的内存  
00007ffe`ee28866c ff1596d10200    call    qword ptr [explorerframe!_imp_CoTaskMemFree (00007ffe`ee2b5808)]
00007ffe`ee288672 488b4c2440      mov     rcx,qword ptr [rsp+40h]
00007ffe`ee288677 488b11          mov     rdx,qword ptr [rcx]
00007ffe`ee28867a 488b4210        mov     rax,qword ptr [rdx+10h]
00007ffe`ee28867e ff15a4d80200    call    qword ptr [explorerframe!_guard_dispatch_icall_fptr (00007ffe`ee2b5f28)]
00007ffe`ee288684 488b742468      mov     rsi,qword ptr [rsp+68h]
00007ffe`ee288689 8bc3            mov     eax,ebx
00007ffe`ee28868b 488b5c2460      mov     rbx,qword ptr [rsp+60h]
00007ffe`ee288690 4883c450        add     rsp,50h
00007ffe`ee288694 5f              pop     rdi
00007ffe`ee288695 c3              ret

其中 _guard_dispatch_icall_fptr 并不是函数,它是“执行流保护(Control Flow Guard,CFG)” 技术的一项缓解措施。具体来说就是 CFG 为了防止恶意程序篡改间接调用的地址,进而控制了程序的执行流程,会对代码中通过地址间接调用函数的地方插入检查代码,检查地址的有效性。你可以理解为这就是一个桩,通过这个桩跳转到指定的地址。举个例子,假如代码中有这样的间接调用:

mov     rcx,qword ptr [rax+50h]
call    qword ptr [rcx]

如果开启了 /guard 编译选项,最终生成的代码就会是这样的:

mov     rcx,qword ptr [rax+50h]
mov     rax, rcx //是否使用 rax 寄存器,取决于编译器的代码生成
call    qword ptr _guard_dispatch_icall_fptr

我在微软的网站上找到了一个叫 Raymond 技术人员在 2018 年发表的文章([Raymond 1]),截取部分内容如下:

The call qword ptr [_guard_dispatch_icall_fptr] doesn’t mean “call the function _guard_dispatch_icall_fptr.” It means “read eight bytes from _guard_dispatch_icall_fptr,
treat those eight bytes as a 64-bit value, interpret that value as an
address, and call to that address.” The bytes you see at _guard_dispatch_icall_fptr are not code; they are data. Disassembling them as code is meaningless.

简单理解,就是代码中通过这个检查桩调用了 COM 接口,第一个_guard_dispatch_icall_fptr 代理的是IShellFolder::GetUIObjectOf 接口,第二个代理的是 IQueryInfo::GetInfoTip 接口。注意,问题就发生在调用 IQueryInfo::GetInfoTip 接口的时候,先来看看这个接口的定义:

HRESULT GetInfoTip(DWORD dwFlags, PWSTR *ppwszTip);

根据 MSDN 文档对 ppwszTip 参数的描述:

The address of a Unicode string pointer that, when this method returns successfully, receives the tip string pointer. Applications that implement this method must allocate memory for ppwszTip by calling CoTaskMemAlloc. Calling applications must call CoTaskMemFree to free the memory when it is no longer needed.

问题是如果用户的实现不支持这种文件类型,没有 Tip 信息该如何对 ppwszTip 赋值呢?答案是您一定不能随它不管,没有信息就给它赋值 NULL,原因请看后面的具体分析。

请看GetInfoTipEx 函数汇编代码的第 28 行:lea r8,[rsp+78h],这里是取一个局部变量的地址,这个地址就是要传给GetInfoTipppwszTip 参数,我所说的 BUG 就是这里没有给[rsp+78h]的数据初始化为 NULL,没有初始化的后果就是在交换频繁的栈上,这个位置的值是不确定的。在调用完GetInfoTip之后,在使用这个指针的时候也没有做安全性检查,只是用 test r8,r8结合je 跳转指令简单判断了一下是否是 NULL,接着就调用explorerframe!StringCchCopyW 访问这个地址,并最后调用 CoTaskMemFree 释放这个指针。

GetInfoTipEx 函数没有对[rsp+78h]的数据初始化,就意味着用户实现 GetInfoTip接口的时候必须给ppwszTip 赋值,要么是分配的 Tip 信息内存,要么是赋值为 NULL,总之不能空着不管。如果遇到这样不良设计的GetInfoTip接口实现呢?不幸的是,我的系统上就有一个这样的不良实现。是哪个软件我就不说了,总之跟了一下它的代码,发现它需要打开一个文件,根据文件头获取一些信息,然后组成这种文件的 Tip 信息。但是,如果打开文件后发现是不支持的文件,就没搭理ppwszTip ,直接返回 E_FAIL 了。可是偏偏GetInfoTipEx 有 BUG,没有检查返回值,只看*ppwszTip 是否是 NULL,那么,问题来了,假如[rsp+78h]的数据刚好是 NULL,于是躲过一劫;假如[rsp+78h]的数据是随机值,要么explorerframe!StringCchCopyW 函数访问无效地址异常,要么CoTaskMemFree报告错误地址异常,GetInfoTipEx 函数对这两个异常都没有防护,于是 explorer.exe 崩溃。

3. 故障原因验证

为了验证上述分析,我做了一个系统 Shell 扩展程序,实现了 IQueryInfo 接口,其中 GetInfoTip接口的实现如果是这样,则不会导致 explorer.exe 崩溃:

//IQueryInfo
HRESULT CTestInfoShellExt::GetInfoTip(DWORD dwFlags, LPWSTR *ppwszTip)
{
    *ppwszTip = NULL;
    
    return S_OK;
}

如果是这样,则无论返回值是 S_OK,还是其他系统错误值,都会触发 explorer.exe 崩溃:

//IQueryInfo
HRESULT CTestInfoShellExt::GetInfoTip(DWORD dwFlags, LPWSTR *ppwszTip)
{
    *ppwszTip = 0x1223344;
    
    return S_OK; 
}

到此为止,基本上验证我之前的分析。

GetInfoTipEx 函数在调用 GetInfoTip接口之前将*ppwszTip 赋值为 NULL,可以避免因为用户的不良设计导致的 explorer.exe 崩溃,但是无法躲过恶意代码,就像第三节介绍的技术验证代码,故意返回一个随机值让 explorer.exe 崩溃,微软最好是升级一下 IQueryInfo 接口,增加一个安全的方法。作为设计接口应用的开发人员来说,不管三七二十八,上来第一行代码就给*ppwszTip 赋值为 NULL,也是没毛病的。

我测试了 Windows 10 的 1809、1903、2004 三个版本,都存在这样的问题。

在这里插入图片描述

参考文献:

[Raymond 1] The case of the buffer overflow vulnerability that was neither a buffer overflow nor a vulnerability, Raymond, December 7th, 2018
https://devblogs.microsoft.com/oldnewthing/20181207-00/?p=100435


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK