
تحليل فني لثغرة CVE-2020-1206 (SMBleed) للإفصاح عن معلومات kernel في Windows SMBv3، بما في ذلك نبوءة تسرب الذاكرة غير المصادق عليها وتقنيات الاستغلال المقترنة مع SMBGhost لتحقيق تنفيذ عن بعد (RCE).
في ثغرة SMBGhost (CVE-2020-0796) تحدثت عن تقنية write-what-where primitive من خلال استخدام خطأ تجاوز عدد صحيح (integer overflow) لتغيير المؤشر Alloc.Userbuffer ليشير إلى عنوان نريده وكتابة بيانات عشوائية فيه. على غرار SMB Ghost، توجد هذه الثغرة أيضًا في دالة Srv2DecompressData في srv2.sys. دعنا نراجع دالة Srv2DecompressData المرتبطة بثغرة SMBGhost (CVE-2020-0796) والتي تم تبسيطها بواسطة Zecops``` c typedef struct _COMPRESSION_TRANSFORM_HEADER { ULONG ProtocolId; ULONG OriginalCompressedSegmentSize; USHORT CompressionAlgorithm; USHORT Flags; ULONG Offset; } COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;
typedef struct _ALLOCATION_HEADER { // ... PVOID UserBuffer; // ... } ALLOCATION_HEADER, *PALLOCATION_HEADER;
NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize) { PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset), NULL); If (!Alloc) { return STATUS_INSUFFICIENT_RESOURCES; }
ULONG FinalCompressedSize = 0;
NTSTATUS Status = SmbCompressionDecompress(
Header->CompressionAlgorithm,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
(ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
(PUCHAR)Alloc->UserBuffer + Header->Offset,
Header->OriginalCompressedSegmentSize,
&FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
if (Header->Offset > 0) {
memcpy(
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;
}
Hàm Srv2DecompressData nhận vào một message được nén do client gửi đến và tiến hành cấp phát một vùng nhớ cần thiết، giải nén message vào đó. Sau đó، nếu trường Offset khác không، nó sẽ copy data (RawData) ở trước data được nén vào phần đầu vùng nhớ được cấp phát.

Lỗi SMBGhost nằm ở việc hàm không kiểm tra integer overflow، dẫn đến cấp phát sai kích thước gây ra buffer overflow. Sau 3 tháng kể từ ngày Microsoft vá SMBGhost، lỗ hỏng CVE-2020-1206 (SMBleed - theo cách gọi của [Zecops Blog](https://blog.zecops.com/)) được tìm thấy. Lỗ hỏng này cho phép chúng ta leak được địa chỉ của máy khác، và nếu kết hợp với SMBGhost، ta có thể có được RCE. Để có cái nhìn đơn giản hơn về hàm Srv2DecompressData، ta sẽ dùng lại hàm này khi chưa được vá lỗi SMBGhost và giả sử rằng nó đã được vá.
# Giả mạo OriginalCompressedSegmentSize
Như SMBGhost، lần này ta vẫn sẽ giả mạo OriginalCompressedSegmentSize với một số lớn hơn một chút so với dữ liệu giải nén mà ta gửi. Ví dụ ta nén một data có kích thước x byte، thay vì đặt vào trường OriginalCompressedSegmentSize x، ta sẽ đặt thành x + 0x1000، xem hình sau sẽ rõ hơn:

Uninitialized kernel data sẽ được coi như một phần của message.
Như ở bài phân tích [CVE-2020-0796](https://github.com/datntsec/CVE-2020-0796) tôi có nói، Srv2DecompressData vẫn sẽ bỏ qua giai đoạn kiểm tra sau hàm SmbCompressionDecompress nếu việc decompress diễn ra thành công:``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
على الرغم من أن الحقل OriginalCompressedSegmentSize يتم تعيينه إلى x + 0x1000 بدلاً من x، إلا أنه بعد نجاح فك الضغط، لا يحتوي المتغير FinalCompressedSize على القيمة x، بل سيحتوي على القيمة x + 0x1000:```c NTSTATUS SmbCompressionDecompress( USHORT CompressionAlgorithm, PUCHAR UncompressedBuffer, ULONG UncompressedBufferSize, PUCHAR CompressedBuffer, ULONG CompressedBufferSize, PULONG FinalCompressedSize) { // ...
NTSTATUS Status = RtlDecompressBufferEx2(
...,
FinalUncompressedSize,
...);
if (status >= 0) {
*FinalCompressedSize = CompressedBufferSize;
}
// ...
return Status;
}
لأنه بعد فك الضغط بنجاح، يتم تحديث `FinalCompressedSize` لتحتفظ بقيمة `CompressedBufferSize` (المقابلة لـ `OriginalCompressedSegmentSize` التي تم تمريرها إلى الدالة `SmbCompressionDecompress`). هذا التحديث والفحص اللاحق يكاد لا يكون ضرورياً، مما قد يؤدي إلى بعض الأخطاء غير المتوقعة.
# الاستغلال على المستوى الأساسي
بنية الرسالة التي يستخدمها Zecops لإثبات الثغرة هي [رسالة SMB2 WRITE](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8). تحتوي هذه البنية على حقول مثل عدد البايتات القابلة للكتابة، العلامات،...، تليها مخزن مؤقت بطول اختياري. هذا مثالي تماماً لاستغلال الثغرة، حيث يمكننا إنشاء رسالة وتحديد الرأس، مع مخزن مؤقت يحتوي على بيانات غير مهيأة.
استناداً إلى [POC من Zecops](https://blog.zecops.com/) على مستودع WindowsProtocolTestSuites الخاص بـ Microsoft، للحصول على رؤية أوضح حول هذا الأمر، سنضيف هذه الإضافة الصغيرة لدالة الضغط:``` c
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}
لاحظ أن هذا الإثبات يتطلب بيانات اعتماد ووصول كتابة مشترك، وهو متاح عادةً في العديد من الحالات. ومع ذلك، فإن الخطأ الذي يتم إرجاعه سينطبق على أي رسالة (بما في ذلك الرسائل التي تحتوي على بيانات اعتماد أو بدونها)، لذلك من المحتمل أن نتمكن من الاستغلال دون الحاجة إلى المصادقة. أيضًا، الذاكرة التي سنقوم بتسريبها هي من التخصيصات السابقة داخل NonPagedPoolNx، ونظرًا لأننا نستطيع التحكم في حجم التخصيص، فسنتمكن من التحكم في البيانات التي سنقوم بتسريبها إلى حد ما.
فإذا لم تكن هناك بيانات اعتماد، فهل يمكن تسريب عنوان kernel؟ للإجابة على هذا السؤال، دعنا نحلل SMB بشكل أعمق.
عند مصادقة المعلومات، سيرسل العميل الرسائل التالية:
SMB2 NEGOTIATE → SMB2 SESSION_SETUP → SMB2 SESSION_SETUP
إذا كانت بيانات الاعتماد خاطئة، فسيتم إنهاء جلسة الاتصال بعد حزمة SMB2 SESSION_SETUP الثانية:

بافتراض أنه ليس لدينا بيانات اعتماد، سنتحقق مما إذا كان هناك أمر يمكن إرساله دون الحاجة إلى المصادقة. من خلال البحث، نلاحظ:
SMB2 NEGOTIATE وهو الأمر الوحيد SMB2 NEGOTIATE طوال الجلسة.SMB2 SESSION_SETUP.حيث أن رسالة SMB2 NEGOTIATE لن يتم ضغطها. الخلل موجود في دالة فك الضغط، لذلك لن ننظر فيها، بل سننظر فقط في رسائل SMB2 SESSION_SETUP.
كما ذكر أعلاه، الجلسة العادية ستحتوي على أمرين SMB2 SESSION_SETUP يتم إرسالهما. الحزم المرتجعة لن تحتوي على أي بيانات ضرورية للاستغلال، وليس لدينا طريقة للتأثير على الحزمة المرتجعة. ومع ذلك، فإن الحزمة المرتجعة الثانية ستحتوي على جسم فارغ مع الحالة 0xC000006D (STATUS_LOGON_FAILURE) في رأس الحزمة. نلاحظ أن حزمة SMB2 SESSION_SETUP الأولى ستحتوي على طلب رسالة NTLM Negotiate والحزمة الثانية ستحتوي على رسالة NTLM Authenticate. رسالة NTLM Negotiate بسيطة إلى حد ما، وقد لا تحتوي على أي شيء مثير للاهتمام، لذلك سنتعمق في رسالة NTLM Authenticate.