CVE-2020-0729:Windows LNK远程代码执行漏洞分析(一) (接前文,活动结束了,终于有时间把这个漏洞分析写完了。友情提示,在调试过程中,尽量只开一个poc的文件夹,不然可能要递归调用漏洞函数很多次。次数一多,就容易乱了。)
造成该漏洞的主要原因是在处理Leaf Condition
中的type为VT_VARIANT
的PropertyVariant
结构时发生错误导致。
如果一个LNK文件有一个ItemID List
,且其第一个ItemID
为Delegate Folder ItemID
,且该Delegate Folder ItemID
d的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对象的过程中,所有嵌套的对象会按照出现的顺序进行加载,包括Attributes
和Conditions
。当遇到一个Leaf Condition
时,会调用StructuredQuery1::LeafCondition::Load()
函数来读取所有的Attributes
,然后读取Condition Type PKEY
和Condition 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字节值所指向的位置。由于缓冲区并没有进行初始化,数据会被写入到一个未定义的位置,最终导致任意代码执行。
用一个简单的流程图来描述上面的流程:
首先是StructuredQuery1::LeafCondition::Load()
函数。在StructuredQuery1::LeafCondition::Load()
函数中依次读取了Condition Type PKEY
,Condition Operation
,然后调用StructuredQuery1::ReadPROPVARIANT()
函数读取PorpertyVariant
结构:
进入到StructuredQuery1::ReadPROPVARIANT()
函数内部:
这里首先读取variant的类型,然后进行VT_ARRAY
检查,再然后检测type是否为0x13,而0x13为VT_UI4
的code。如果不是,进入到后续的switch结构的处理流程:
VT_CF
的code为71,该类型表示指向CLIPDATA
结构的一个指针,Propvaritant type为pclipdata
。CLIPDATA
的详细结构如下(与一般的PROPVARIANT略有不同):
VT_CF
时的处理代码如下:
如果没有设置VT_VECTOR
,则跳转到如下代码。因为在处理VT_CF
时,对分配的缓冲区并没有做任何的初始化处理,然后直接将buffer的地址传入了rdx,并且向rdx的地址中写入了4个字节:
VT_VARIANT
为一个DWORD类型的标识后续值的类型的标识符,仅可以与VT_VECTOR
和VT_BYREF
结合使用。VT_VARIANT
时的处理代码如下:
跳转到loc_18003bcb0:
检查是否设置了VT_VECTOR
,如果没有设置,则调用CoTaskMemAlloc()
来分配包含另一个PropertyVariant
结构的24字节的buffer,且后续在未进行buffer初始化的情况下,函数进行递归调用。
至此,漏洞的静态分析过程大致完成。对于中间过程中各参数和返回值的具体表现情况,需要通过动态调试来观察。
首先直接观察下最终的crash现场,看下栈回溯:
并没有全部截图,但关键部分的相关调用已足够。
ISream_Read()
函数此时的3个参数分别为:0, 00000000`0e5ca200,2,该函数的原型为:
而调用该函数的位置恰好在写入4字节数据的那里(loc_18003bb98
),那么此时读取出来的数据是已经被篡改过的数据了。继续向上追溯,StructuredQuery!StructuredQuery1::ReadPROPVARIANT+0x31a39
处为函数递归调用的位置:
目前根据栈回溯结果,大致与前面静态分析的过程一致,下面进入漏洞触发过程的详细分析:
首先在StructuredQuery!StructuredQuery1::LeafCondition::Load+0xad
处下断,观察`StructuredQuery!StructuredQuery1::ReadPROPVARIANT()
函数调用时的状态:
继续执行,来到variant类型校验:
继续执行,判断是否为VT_UI4
:
此时eax的值为0x1f,不等于0x13,进入到后续的switch处理流程:
再往后执行,也没有触发漏洞代码,来到第二次调用:
而第二次及第三次调用与第一次调用流程一致(仅仅流程一致,部分关键地址和数据有变化),来到第四次调用:
第四次调用在比较是否为0x13时,判断成立,进入与前几次调用不同的流程:
在检查完VT_VARIANT
和VT_VECTOR
的相关设置后,分配24字节的buffer:
这里看下CoTaskMeMAlloc()
函数的函数原型:
函数很简单,分配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已分析完成。
基本条件
触发过程
attacker发送一个LNK文件给target:
应用协议:
已知文件类型:Shell Link
已知扩展名:LNK
已知MIME类型:application/x-ms-shortcut
从常规漏洞防御角度来说,文件格式类漏洞更适合使用终端防护软件进行防护。但该漏洞的主要应用场景应该是attacker诱骗target打开LNK文件,这样就会涉及到流量传输,在一定程度上可以对恶意流量进行检测:
attacker诱骗target请求恶意LNK文件:
attacker直接响应一个恶意LNK(注意分包):
StructuredQuery1::ReadPROPVARIANT()
函数的补丁对比结果:
总体上看,函数并没有做过多修改,进入查看具体更新内容。当case为12(VT_VARIANT类型)时,有一处比较明显的改动。在进行24字节的缓冲区分配后,对分配的缓冲区做了赋值0操作:
在更新前,对分配的buffer未做任何的初始化操作,直接进入了递归调用:
在更新后,对分配完的buffer做了初始化操作(3次赋值为0,总计24字节):
所以补丁的思路很简单,将分配的24字节的buffer初始化为0即可。
通过动态调试,证明了这种补丁思路:
进行了3次mov操作后,将24个字节的buffer成功初始化为0:
使用初始化为0后的buffer进入函数递归:
继续调用IStream_Read()
函数进行读取,此时的参数情况如下:
继续向下跟,会发现并没有在IStream_Read()
函数中产生补丁前的那种情况,而是执行完后,跳到异常处理,函数返回,而不进行内存分配:
针对该漏洞,目前很难做到无损检测。
流量防御:流量侧的防御可以检测恶意数据的具体内容,但需要联系数据上下文,因为该漏洞的本质是多种数据结构的组合使用导致异常。流量特征较弱,不是很建议使用流量防御。
终端防御:使用热补丁,对24个字节的buffer使用0进行初始化即可。
终端防御方法基本没有风险,但是流量侧误报风险较大。
看雪论坛内部共享,未经允许请勿擅自转载。谢谢。
typedef struct tagCLIPDATA {
/
/
cbSize
is
the size of the
buffer
pointed to
/
/
by pClipData, plus sizeof(ulClipFmt)
ULONG cbSize;
long
ulClipFmt;
BYTE
*
pClipData;
} CLIPDATA;
typedef struct tagCLIPDATA {
/
/
cbSize
is
the size of the
buffer
pointed to
/
/
by pClipData, plus sizeof(ulClipFmt)
ULONG cbSize;
long
ulClipFmt;
BYTE
*
pClipData;
} CLIPDATA;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-9-28 17:32
被有毒编辑
,原因: 补充内容