首页
社区
课程
招聘
[原创]#30天写作挑战#CVE-2020-0729:Windows LNK远程代码执行漏洞分析(二)
2020-9-27 20:40 20296

[原创]#30天写作挑战#CVE-2020-0729:Windows LNK远程代码执行漏洞分析(二)

2020-9-27 20:40
20296

CVE-2020-0729:Windows LNK远程代码执行漏洞分析(一)
(接前文,活动结束了,终于有时间把这个漏洞分析写完了。友情提示,在调试过程中,尽量只开一个poc的文件夹,不然可能要递归调用漏洞函数很多次。次数一多,就容易乱了。)

3. 详细分析

1. 漏洞触发流程

造成该漏洞的主要原因是在处理Leaf Condition中的type为VT_VARIANTPropertyVariant结构时发生错误导致。

 

如果一个LNK文件有一个ItemID List,且其第一个ItemIDDelegate Folder ItemID,且该Delegate Folder ItemIDd的GUID表示CLSID_SearchFolder,那么首先调用Windows.Storage.Search.dll中的CDBFolder::BindToObject()函数进行处理,该函数会调用CDBFolder::GetFilterConditionForChild()函数从child ItemID中获取search filter condition。

 

第一阶段,CDBFolder::GetFilterConditionForChild()函数在进行查找时,会在child ItemID中搜索PKEY_FilterInfo property,如果找到了,在序列化property store中的包含一个property bag的VT_STREAM就会通过调用SHLoadFilterFromStream()函数进行加载,该函数会创建一个CFilterCondition对象然后提供给IUnknown_LoadFromStream()函数。该操作会调用CFIlterCondition::Load()函数,而该函数会首先将property bag复制到一个内存中的store中,然后开始校验该store的结构,通过查找名称为Name, Type,key:FMTID,Key:PID以及Condition的字符串来确认是否property bag元素。

 

第二阶段,在确认完所有元素后,调用LoadConditionFromStream()函数来读取Condition property bag中的VT_STREAM,该元素调用IUnknown_LoadKnownImplFromStream()来读取Condition GUID并从StructuredQuery.dll中创建并加载相关的condition对象。在从stream中加载condition对象的过程中,所有嵌套的对象会按照出现的顺序进行加载,包括AttributesConditions。当遇到一个Leaf Condition时,会调用StructuredQuery1::LeafCondition::Load()函数来读取所有的Attributes,然后读取Condition Type PKEYCondition Operation,然后调用StructuredQuery1::ReadPROPVARIANT()函数来读取PropertyVariant结构。

 

第三节阶段,StructuredQuery1::ReadPROPVARIANT()先读取2字节的type,然后检查VT_ARRAY(0x2000)是否进行了设置,这里主要是因为StructuredQuery不支持。然后进入到一个switch语句来根据type进行不同的处理。如果type设置为VT_VARIANT(0x000c),就会检查完整的type字段来确认是否设置了VT_VECTOR,如果没有设置,则调用CoTaskMemAlloc()来分配包含另一个PropertyVariant结构的24字节的缓冲区。在递归调用StructuredQuery1::ReadPROPVARIANT()函数以读取紧随VT_VARIANT 的type字段之后的另一个PropertyVariant结构之前,并没有对这个缓冲区进行初始化。如果下一个type字段设置为VT_CF(0x0047),则应该是一个包含一个指向剪贴板数据的指针的PropertyVariant结构,ReadPROPVARIANT()函数尝试将stream的后4个字节写入先前分配的24字节缓冲区中的8字节值所指向的位置。由于缓冲区并没有进行初始化,数据会被写入到一个未定义的位置,最终导致任意代码执行。

 

用一个简单的流程图来描述上面的流程:

 


2. 静态分析

首先是StructuredQuery1::LeafCondition::Load()函数。在StructuredQuery1::LeafCondition::Load()函数中依次读取了Condition Type PKEYCondition Operation,然后调用StructuredQuery1::ReadPROPVARIANT()函数读取PorpertyVariant结构:

 

 

 

进入到StructuredQuery1::ReadPROPVARIANT()函数内部:

 

 

这里首先读取variant的类型,然后进行VT_ARRAY检查,再然后检测type是否为0x13,而0x13为VT_UI4的code。如果不是,进入到后续的switch结构的处理流程:

 


VT_CF的code为71,该类型表示指向CLIPDATA结构的一个指针,Propvaritant type为pclipdataCLIPDATA的详细结构如下(与一般的PROPVARIANT略有不同):

1
2
3
4
5
6
7
typedef struct  tagCLIPDATA {
    // cbSize is the size of the buffer pointed to
    // by pClipData, plus sizeof(ulClipFmt)
    ULONG              cbSize;
    long               ulClipFmt;
    BYTE*              pClipData;
    } CLIPDATA;

VT_CF时的处理代码如下:

 

 

如果没有设置VT_VECTOR,则跳转到如下代码。因为在处理VT_CF时,对分配的缓冲区并没有做任何的初始化处理,然后直接将buffer的地址传入了rdx,并且向rdx的地址中写入了4个字节:

 

 

VT_VARIANT为一个DWORD类型的标识后续值的类型的标识符,仅可以与VT_VECTORVT_BYREF结合使用。VT_VARIANT时的处理代码如下:

 


跳转到loc_18003bcb0:

 

 

检查是否设置了VT_VECTOR,如果没有设置,则调用CoTaskMemAlloc()来分配包含另一个PropertyVariant结构的24字节的buffer,且后续在未进行buffer初始化的情况下,函数进行递归调用。

 

至此,漏洞的静态分析过程大致完成。对于中间过程中各参数和返回值的具体表现情况,需要通过动态调试来观察。

3. 动态分析

首先直接观察下最终的crash现场,看下栈回溯:

 

 

并没有全部截图,但关键部分的相关调用已足够。

 

ISream_Read()函数此时的3个参数分别为:0, 00000000`0e5ca200,2,该函数的原型为:

1
2
3
4
5
6
7
LWSTDAPI IStream_Read(
  IStream *pstm,    // 指向待读取stream的IStream接口的pointer
  void    *pv,        // 指向从pstm接收数据的buffer的pointer, buffer的大小最小为cb个字节
  ULONG   cb        // 函数需要从input的stream中读取的数据大小,字节为单位
);
 
// 函数读取成功过后返回S_OK

而调用该函数的位置恰好在写入4字节数据的那里(loc_18003bb98),那么此时读取出来的数据是已经被篡改过的数据了。继续向上追溯,StructuredQuery!StructuredQuery1::ReadPROPVARIANT+0x31a39处为函数递归调用的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:000000018003BCB0 loc_18003BCB0:                          ; CODE XREF: StructuredQuery1::ReadPROPVARIANT(IStream *,tagPROPVARIANT *)+31984↑j
.text:000000018003BCB0                 mov     ecx, 18h        ; cb
.text:000000018003BCB5                 call    cs:__imp_CoTaskMemAlloc ; allocate 24 bytes buffer
.text:000000018003BCBB                 mov     rdx, rax        ; struct IStream *
.text:000000018003BCBE                 mov     [r14], rax
.text:000000018003BCC1                 mov     rcx, rsi        ; this
.text:000000018003BCC4                 call    ?ReadPROPVARIANT@StructuredQuery1@@YAJPEAUIStream@@PEAUtagPROPVARIANT@@@Z ; StructuredQuery1::ReadPROPVARIANT(IStream *,tagPROPVARIANT *)
.text:000000018003BCC9                 mov     edi, eax
.text:000000018003BCCB                 test    eax, eax
.text:000000018003BCCD                 js      loc_18000A302   ; jumptable 000000018000A343 cases 0,1,24
.text:000000018003BCD3                 mov     eax, 4000h
.text:000000018003BCD8                 or      [rbx], ax
.text:000000018003BCDB                 jmp     loc_18000A302   ; jumptable 000000018000A343 cases 0,1,24

目前根据栈回溯结果,大致与前面静态分析的过程一致,下面进入漏洞触发过程的详细分析:

 

首先在StructuredQuery!StructuredQuery1::LeafCondition::Load+0xad处下断,观察`StructuredQuery!StructuredQuery1::ReadPROPVARIANT()函数调用时的状态:

 

 

继续执行,来到variant类型校验:

 


继续执行,判断是否为VT_UI4

 

 

此时eax的值为0x1f,不等于0x13,进入到后续的switch处理流程:

 

 

再往后执行,也没有触发漏洞代码,来到第二次调用:

 

 

而第二次及第三次调用与第一次调用流程一致(仅仅流程一致,部分关键地址和数据有变化),来到第四次调用:

 

 

第四次调用在比较是否为0x13时,判断成立,进入与前几次调用不同的流程:

 

 

在检查完VT_VARIANTVT_VECTOR的相关设置后,分配24字节的buffer:

 

 

这里看下CoTaskMeMAlloc()函数的函数原型:

1
2
3
LPVOID CoTaskMemAlloc(
  SIZE_T cb
);

函数很简单,分配cb字节大小的内存,此处的cb = 0x18 = 24,函数成功分配后返回内存块地址:

 

 

返回后,对该buffer没有做任何处理,直接将buffer地址赋值给rdx做为递归调用的参数进行使用:

 

 

带着未初始化的buffer,进入到递归调用过程:

 

 

继续向下,读取variant的类型:

 

 

此次读取出的类型为VT_CF(0x47)类型。在进行是否为0x13的比较时,[r14]中保存着buffer的地址,eax中保存着类型:

 

 

 

继续向下执行,来到这里后首先看下buffer中现在的情况:

 

 

然后继续向下:

 

 

继续:

 

 

继续:

 

 

然后,从stream中写4字节数据到加载的地址(rcx)。此时IStream_Read()函数的参数情况如下:

 

 

很明显,此时的pv是一个错误值,从而导致stream中的4个字节的数据写入到0000002800000037这个错误地址。进入IStream_Read()函数:

 

 

 

后续使用的非法地址略有变动,变为rdx=006f004600740072(别问为什么,问就是跑飞了重来的!!!)

 

进入ntdll!LdrpDispatchUserCallTarget()函数之前:

 

 

然后来到crash的函数:combase!CMemStm::Read()函数处:

 

 

继续向下:

 

 

这里r8为字节数,rdx中存放的是buffer+8地址处的内容(未定义地址),rcx中存放的stream的地址。继续向下,看后续如何对rdx中的未定义地址进行处理:

 

 

调用memcpy()函数进行data的复制,dst = rcx = 006f004600740072,src = rdx = 006f004600740072,size = r8 = 4。正如前面分析所得,目的地址是一个未定义地址:

 

 

跟进memecpy()函数可以清楚地看到最终的问题所在:

 

 

至此为止,漏洞的完整Root Case已分析完成。

4. 利用思路

1. 利用条件
  1. 基本条件

    • attacker需要将恶意的LNK文件发送给target或者target可以正常访问LNK
  2. 触发过程

    • attacker需要诱使target访问包含LNK文件的路径
2. 利用过程

attacker发送一个LNK文件给target:

1
[ Attacker ] <--------------------> [ Target ]
3. 利用界面

应用协议:

  1. HTTP,端口:80 TCP
  2. HTTPS,端口:443 TCP
  3. SMB/CIFS,端口:139/445 TCP
  4. IMAP,端口:143 TCP
  5. POP3,端口:110 TCP
  6. SMTP,端口:25 TCP
4. 攻击向量

已知文件类型:Shell Link

 

已知扩展名:LNK

 

已知MIME类型:application/x-ms-shortcut

5. 流量分析

从常规漏洞防御角度来说,文件格式类漏洞更适合使用终端防护软件进行防护。但该漏洞的主要应用场景应该是attacker诱骗target打开LNK文件,这样就会涉及到流量传输,在一定程度上可以对恶意流量进行检测:

  1. attacker诱骗target请求恶意LNK文件:

  2. attacker直接响应一个恶意LNK(注意分包):


    6. 补丁分析

1. 补丁比较结果

StructuredQuery1::ReadPROPVARIANT()函数的补丁对比结果:

 

 

总体上看,函数并没有做过多修改,进入查看具体更新内容。当case为12(VT_VARIANT类型)时,有一处比较明显的改动。在进行24字节的缓冲区分配后,对分配的缓冲区做了赋值0操作:

 

2. 补丁思路

在更新前,对分配的buffer未做任何的初始化操作,直接进入了递归调用:

 

 

在更新后,对分配完的buffer做了初始化操作(3次赋值为0,总计24字节):

 

 

所以补丁的思路很简单,将分配的24字节的buffer初始化为0即可。

3. 补丁验证

通过动态调试,证明了这种补丁思路:

 

 

进行了3次mov操作后,将24个字节的buffer成功初始化为0:

 


使用初始化为0后的buffer进入函数递归:

 

 

继续调用IStream_Read()函数进行读取,此时的参数情况如下:

 

 

继续向下跟,会发现并没有在IStream_Read()函数中产生补丁前的那种情况,而是执行完后,跳到异常处理,函数返回,而不进行内存分配:

 

四、漏洞检测和防御

1. 漏洞检测

针对该漏洞,目前很难做到无损检测。

2. 漏洞防御

1. 防御思路

流量防御:流量侧的防御可以检测恶意数据的具体内容,但需要联系数据上下文,因为该漏洞的本质是多种数据结构的组合使用导致异常。流量特征较弱,不是很建议使用流量防御。

 

终端防御:使用热补丁,对24个字节的buffer使用0进行初始化即可。

2. 可能存在的风险

终端防御方法基本没有风险,但是流量侧误报风险较大。

五、参考文献

  1. Shell Link(.LNK) Binary File Format
  2. Common Explorer Concepts, Identifying Namespace Objects)
  3. MS-OLEPS: TypedPropertyValue
  4. MS-OLEPS: IndirectPropertyName
  5. Structured Storage, Propidl.h, PROPVARIANT structure

六、备注

看雪论坛内部共享,未经允许请勿擅自转载。谢谢。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2020-9-28 17:32 被有毒编辑 ,原因: 补充内容
收藏
点赞5
打赏
分享
最新回复 (6)
雪    币: 4904
活跃值: (1440)
能力值: ( LV9,RANK:246 )
在线值:
发帖
回帖
粉丝
Saturn35 4 2020-9-28 10:23
2
0
mark
雪    币: 220
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
xiaozhu023 2020-10-3 18:05
3
0
厉害,好多高手
雪    币: 5475
活跃值: (3377)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
qwqdanchun 2020-12-5 17:22
4
0
可以发下poc的链接吗
雪    币: 114
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
路远_人尚在 2021-7-16 11:37
5
0
您好,POC链接方便发下吗?
雪    币: 12715
活跃值: (16347)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
有毒 10 2021-7-19 08:46
6
0
路远_人尚在 您好,POC链接方便发下吗?
poc目前互联网没有公开,属于公司内部资料,我没法公开,抱歉。。
雪    币: 114
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
路远_人尚在 2021-7-22 16:37
7
0
理解
游客
登录 | 注册 方可回帖
返回