Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2020-1206 — تحليل فني لثغرة CVE-2020-1206 (SMBleed) للإفصاح عن معلومات kernel في Windows SMBv3، بما في ذلك نبوءة تسرب الذاكرة غير المصادق عليها وتقنيات الاستغلال المقترنة مع SMBGhost لتحقيق تنفيذ عن بعد (RCE). | Kitploit
أدوات/GitHubGitHub/datntsec/cve-2020-1206
تحليل الذاكرة الجنائيتحليل الثغرات الأمنيةالاستغلالجمع المعلوماتاختبار الاختراقاستغلال الملفات الثنائية
GitHubdatntsec/cve-2020-1206

CVE-2020-1206

تحليل فني لثغرة CVE-2020-1206 (SMBleed) للإفصاح عن معلومات kernel في Windows SMBv3، بما في ذلك نبوءة تسرب الذاكرة غير المصادق عليها وتقنيات الاستغلال المقترنة مع SMBGhost لتحقيق تنفيذ عن بعد (RCE).

عرض المستودع
منذ 5 سنواتلم تتم المراجعة بعد

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

في ثغرة 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; }

root@kitploit:~
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;

}

root@kitploit:~
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.

![](https://assets.kitploit.com/production/public/readmes/24502/a5bf8b5059336fb3677162343bace0d0b45085f2eb89e42d85f81e032fe233dc.png)

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:

![](https://assets.kitploit.com/production/public/readmes/24502/687d3bdc67e4b2f5bb3cecbe41ea52be98a5c904a8688b325a69acb0a854ca75.png)

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) { // ...

root@kitploit:~
NTSTATUS Status = RtlDecompressBufferEx2(
    ...,
    FinalUncompressedSize,
    ...);
if (status >= 0) {
    *FinalCompressedSize = CompressedBufferSize;
}

// ...

return Status;

}

root@kitploit:~
لأنه بعد فك الضغط بنجاح، يتم تحديث `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، ونظرًا لأننا نستطيع التحكم في حجم التخصيص، فسنتمكن من التحكم في البيانات التي سنقوم بتسريبها إلى حد ما.

كود مصدر SMBleed POC

فإذا لم تكن هناك بيانات اعتماد، فهل يمكن تسريب عنوان kernel؟ للإجابة على هذا السؤال، دعنا نحلل SMB بشكل أعمق.

التعمق في 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

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

رسالة NTLM Authenticate

بعد دراسة رسالة NTLM Authenticate، نلاحظ أن الجزء الأكثر تعقيدًا في هذه الرسالة، والأكثر ملاءمة للاستغلال، هو هيكل NTLM2 V2 Response. هذا الهيكل عبارة عن مصفوفة بايت ذات حجم غير ثابت، تحتوي بشكل أساسي على هيكل NTLMv2_CLIENT_CHALLENGE. نلاحظ أنه إذا لم يجتاز هذا الهيكل الفحوصات الأولية، فسيتم إرجاع القيمة 0xC000000D (STATUS_INVALID_PARAMETER) بدلاً من 0xC000006D (STATUS_LOGON_FAILURE). أحد هذه الفحوصات الأولية هو التحقق من حقل AvPairs.

حقل AvPairs عبارة عن مصفوفة بايت ذات حجم غير ثابت تحتوي على هياكل AV_PAIR. كل AV_PAIR يعرّف زوج سمة/قيمة، ويتم تعريف السمة بواسطة حقل AvId، وحقل AvLen يحدد طول القيمة بالبايت، وحقل Value هو مصفوفة بايت ذات حجم غير ثابت تحتوي على قيمته الخاصة. عنصر بالسمة MsvAvEOL وطول صفري يشير إلى نهاية المصفوفة.

يتم معالجة رسالة Authenticate بواسطة دالة SsprHandleAuthenticateMessage في الوحدة msv1_0.dll. في الفحوصات الأولية، تضمن هذه الدالة أن المصفوفة AvPairs ستحتوي على السمات التالية: 0x0001 (MsvAvNbComputerName)، 0x0002 (MsvAvNbDomainName). ومع ذلك، لا يتم التحقق من قيمتها، فهي تتحقق فقط من خلال المرور عبر المصفوفة والتحقق من وجود السمة المطلوبة وما إذا كان طولها ضمن الهيكل. إذا كان الطول كبيرًا جدًا، فسيتم إيقاف النقل. لذلك، في الواقع، لا يتم التحقق من صحة MsvAvEOL.

في هذه المرحلة، اكتشفنا أنه يمكننا إنشاء طلب يساعدنا في الإجابة على السؤال التالي: بالنظر إلى وحدتي بايت عند الإزاحة x، من النوع uint16، هل القيمة أكبر من y؟ يتم التحكم في x و y بواسطتنا. دعنا ننظر إلى الحزمة التالية:

محتوى القيمة 0x0001 (MsvAvNbComputerName) ليس مهمًا، لذلك يمكننا استخدامه لضبط إزاحة القيمة الثانية. بالنسبة للقيمة الثانية، نضع السمة فقط على 0x0002 (MsvAvNbDomainName)، دون تهيئة len و value. وفي الوقت نفسه، قمنا بتعيين حجم الحزمة بالكامل بحيث يكون y بايت وفقًا لحقل الطول. هناك نتيجتان محتملتان اعتمادًا على القيمة غير المهيأة لحقل length للقيمة الثانية:

  • length <= y: في هذه الحالة، يمر الفحص لأن القيمة 0x0002 صالحة (MsvAvNbDomainName) تم العثور عليها. يقوم الخادم بإرجاع 0xC000006D (STATUS_LOGON_FAILURE) لأن بيانات الاعتماد غير صحيحة.
  • length > y: في هذه الحالة، يفشل الفحص، لأن القيمة الثانية لها طول غير صالح ويتم تجاهلها. يقوم الخادم بإرجاع 0xC000000D (STATUS_INVALID_PARAMETER) لهذه الحالة.

وفقًا لاستجابة الخادم، يمكننا دائمًا استنتاج إجابة السؤال أعلاه.

ومع ذلك، فإن رسالة NTLM Authenticate محدودة بـ 0xB48 بايت وسيتم تجاهلها إذا كانت أكبر من ذلك. يتم الفحص بواسطة دالة SspContextGetMessage في الوحدة msv1_0.dll. لذا بافتراض أننا نكتب فقط 1 بايت من len، والبايت المتبقي سيحتوي على قيمة غير مهيأة، فهل يمكن تجاوز ذلك؟ للأسف لا، لأن قيمة uint16 مشفرة بصيغة little endian. وبالتالي، لا يمكننا تحقيق ما نريده في جلسة SMB واحدة، وسننظر في عوامل أخرى.

الملاحظة رقم 1: قوائم Lookaside

كما ذكرنا في البحث السابق (CVE-2020-0796)، تستخدم الوحدات النمطية لمعالجة SMB في kernel (srv2.sys و srvnet.sys) وظيفة تخصيص مخصصة - SrvNetAllocateBuffer - التي يتم تصديرها بواسطة srvnet.sys. تستخدم هذه الوظيفة قوائم lookaside للتخصيصات الصغيرة لتحسين الأداء. تُستخدم قوائم lookaside لتخزين مجموعة من المخازن المؤقتة ذات الحجم الثابت بكفاءة، والتي يمكن إعادة استخدامها للسائق.

يتم إنشاء قوائم lookaside عند التهيئة، والقوائم لكل حجم ومعالج منطقي موصوفة في الجدول التالي:

كل خلية بها رمز "📝" هي قائمة lookaside منفصلة. لتبسيط التحليل، سنفترض أن هدفنا يحتوي على معالج منطقي واحد فقط. في هذه الحالة، طالما يتم تخصيص نفس عدد البايتات، واستخدام نفس قائمة lookaside، فسيتم إعادة استخدام نفس المخزن المؤقت عدة مرات. يمكننا استخدام هذا للحصول على بعض التحكم في البيانات غير المهيأة.

الملاحظة رقم 2: فشل فك الضغط

دعنا نراجع ما يحدث عند فك ضغط حزمة مضغوطة (ارجع إلى التوثيق السابق CVE-2020-0796 لمزيد من التفاصيل والكود الزائف):

في حالة عدم صلاحية CompressedData، ستفشل مرحلة فك الضغط، ولن يتم تنفيذ مرحلة النسخ، وسيتم قطع الاتصال. لكن فك الضغط قد يفشل فقط بعد فك ضغط جزء من CompressedData الصالح. هذا يسمح لنا بإنشاء طلب بحيث يتم كتابة البيانات التي نختارها عند الإزاحة التي نختارها، كما هو موضح في الشكل التالي:

العودة إلى رسالة NTLM Authenticate

يمكننا استخدام الملاحظات أعلاه لجعل تقنيتنا تعمل باستخدام خطوتين:

  1. إرسال رسالة ببيانات مضغوطة غير صالحة بحيث يتم فك ضغط بايت واحد فقط من الصفر. سيكون هذا البايت هو البايت الأول من حقل length للقيمة الثانية في مصفوفة AvPairs.
  2. إرسال رسالة مماثلة كما في السابق، ولكن مع ضمان استخدام نفس قائمة lookaside للتخصيص، بحيث يكون البايت الصفري موجودًا هناك.

هذه المرة، يمكن لهذه التقنية الإجابة على السؤال التالي: بالنظر إلى بايت عند الإزاحة x، هل القيمة أكبر من y؟ كما في السابق، يتم التحكم في x و y بواسطتنا.

نظرًا لأنه يمكننا إعادة استخدام المخزن المؤقت عدة مرات عن طريق ضمان استخدام نفس قائمة lookaside، يمكننا تكرار الخطوات عدة مرات مع تغيير y، واستنتاج قيمة البايت في النهاية عند إزاحة معينة.

ومع ذلك، فإن لهذه التقنية قيدًا - إزاحة البايت الذي يمكننا قراءته محدودة بالبايت 0xADB من بداية المخزن المؤقت للحزمة. وذلك لأن إزاحة رسالة NTLM Authenticate (AUTHENTICATE_MESSAGE) محدودة بـ 0x40 بايت بعد نهاية رؤوس SMB2 SESSION_SETUP (يتم فرضها بواسطة دالة Smb2ValidateSessionSetup في srv2.sys) وحجم رسالة NTLM Authenticate (AUTHENTICATE_MESSAGE) محدود بـ 0xB48 بايت. سنبحث عن طريقة لحل هذا.

بافتراض أننا نريد قراءة بايت عند الإزاحة 0x1100. لا يمكننا فعل ذلك مباشرة بالتقنية أعلاه، ومع ذلك لا يزال بإمكاننا استخدام التقنية التالية: نظرًا لأن المخازن المؤقتة يتم إعادة استخدامها من قوائم lookaside، يمكننا "رفع" البايت الهدف من خلال دالة فك الضغط عن طريق تعيين حقل Offset لتجاوز ذلك البايت. نحتاج فقط إلى ضمان أن البيانات الموجودة هناك يمكن تفسيرها كبيانات مضغوطة صالحة، وإلا فلن يحدث النسخ.

سيحتوي مخزن الحزمة على بيانات أرسلها العميل بالإضافة إلى 16 بايت إضافية من الرؤوس التي لا يتم نسخها عند حدوث عملية فك الضغط. نتيجة لذلك، يتم نسخ البيانات التي تم فك ضغطها، بما في ذلك البايت الهدف، إلى موقع أقرب بـ 16 بايت إلى بداية المخزن المؤقت المخصص. يمكننا تكرار ذلك عدة مرات، حتى تصبح إزاحة البايت الهدف منخفضة بما يكفي.

إثبات تسريب العنوان

يمكنك العثور على سكريبت يوضح التقنية أعلاه هنا. تذكر أننا افترضنا أن خادم الكمبيوتر يحتوي على معالج منطقي واحد فقط، لذلك ستحتاج إلى تكوين جهازك الافتراضي بشكل صحيح لكي يعمل السكريبت. إذا سارت الأمور بسلاسة، فسيقرأ السكريبت ويسرب عنوان تجمع NonPagedPoolNx. في الواقع، سيكون عنوان أحد المخازن المؤقتة الموجودة في نفس قوائم lookaside.

نظرًا لأن هذه التقنية بها العديد من القيود، فلن أتحليلها بشكل أعمق. ومع ذلك، لا يزال بإمكانك قراءة السكريبت أعلاه وتحليله بنفسك.

نهج مختلف – فك الضغط

أثناء البحث، أدرك Zecops أن الحزمة SMB التي تم فك ضغطها ليست الهيكل المعقد الوحيد الذي قد يكون غير صالح بطرق مختلفة. حتى قبل معالجة جميع الهياكل المتعلقة بـ SMB، قد يكون المخزن المؤقت المضغوط أيضًا غير صالح. إذا فشل فك الضغط، فسيتم قطع الاتصال بالخادم.

توفر Microsoft ثلاث خوارزميات ضغط للاختيار من بينها عند تنفيذ SMB: LZNT1 و Plain LZ77 و LZ77 + Huffman. سننظر فقط في LZNT1 لأنه بسيط إلى حد ما - حوالي 80 سطرًا من Python لدالة فك ضغط. سأتحدث بإيجاز عن عملية فك الضغط: تتكون البيانات المضغوطة من سلسلة من الكتل المضغوطة، كل كتلة تبدأ بمتغير uint16 يشير إلى طول تلك الكتلة. عندما يتم مواجهة طول يساوي 0، تكتمل عملية فك الضغط. سنستخدم هذا لكتابة سلسلة من البايتات الصفرية التي تمثل بيانات مضغوطة صالحة. الهدف هو الإجابة على السؤال أعلاه: بالنظر إلى بايت عند الإزاحة x، هل القيمة أكبر من y؟ بالطبع، سيظل x و y تحت سيطرتنا.

فيما يلي مثال على البيانات المضغوطة التي سنرسلها:

هناك نتيجتان محتملتان اعتمادًا على القيمة غير المهيأة للبايت الأول من حقل length:

  • length <= y: في هذه الحالة، ستكون الكتلة الأولى بأكملها بايتات صفرية، وهذا صالح تمامًا وسيكون طول الكتلة التالية صفرًا، وسيكتمل فك الضغط. سيعيد الخادم استجابة.
  • length > y: في هذه الحالة، ستحتوي الكتلة المضغوطة الأولى أو الثانية على بايتات 0xFF، ولن يتم فك ضغط هذه الكتلة. سيفصل الخادم الاتصال بسبب بيانات مضغوطة غير صالحة.

كما هو الحال مع التقنية السابقة، يمكننا استخدام الملاحظتين رقم 1 و 2 لإنشاء رسالة تحتوي على بايت واحد غير مهيأ في منتصف الرسالة باستخدام خطوتين:

  1. إرسال رسالة ببيانات مضغوطة غير صالحة بحيث يتم فك ضغط جزء فقط من البيانات، على غرار الشكل أعلاه
  2. إرسال الرسالة الثانية وضمان استخدام نفس قائمة lookaside في الرسالة الأولى، بحيث تكون البايتات من الخطوة 1 موجودة هناك.

لاحظ أن قيمة Offset في رأس حزمة SMB ستشير إلى البيانات المضغوطة، والتي قد تكون صالحة أم لا اعتمادًا على قيمة البايت غير المهيأ.

أبرز ميزة لهذه التقنية مقارنة بالتقنية السابقة هي عدم وجود حد للإزاحة.

وبالتالي، بإيجاز، لدينا تقنيتان لقراءة منطقة ذاكرة غير مهيأة من المخزن المؤقت للتجمع المخصص بواسطة دالة SrvNetAllocateBuffer من الوحدة srvnet.sys. التقنية الأولى تنشئ حزمة SMB خاصة، ثم تستنتج المعلومات من خلال استجابة الخادم. التقنية الثانية، مع قيود أقل، سننشئ بيانات مضغوطة خاصة ونرسلها، ثم نستنتج المعلومات بناءً على ما إذا كان الخادم يفصل الاتصال أم لا.

وبالتالي، يمكننا استخدام إحدى التقنيتين أعلاه للاستغلال. وكما قلت، التقنية الأولى لها العديد من القيود، لذا سنتعمق فقط في التقنية الثانية.

هذه التقنية ستساعدنا على الاستغلال في اتجاه write-what-where primitive الذي أثبته Zecops سابقًا في بحث سابق حول إمكانية تحقيق تصعيد الامتيازات المحلية. سنستخدم هذه التقنية لتسريب عنوان في تخطيط الذاكرة لتمكين استخدام write-what-where primitive. لسوء الحظ، الذاكرة المخصصة بواسطة دالة SrvNetAllocateBuffer تستخدم بشكل أساسي لبيانات الشبكة مثل حزم SMB ولا تحتوي على أي مؤشرات للنظام. وبما أننا نحتاج إلى تحقيق RCE، فإن تسريب مناطق الذاكرة غير المهيأة من التخصيصات السابقة لدالة SrvNetAllocateBuffer غير مجدية بسبب عدم اليقين في موقع المؤشر المطلوب. نحتاج إلى إيجاد شيء أكثر فائدة.

SrvNetAllocateBuffer وتخطيط المخزن المؤقت المخصص

كما ذكرت في بحثي حول تصعيد الامتيازات المحلية (CVE-2020-0796)، لا تقوم دالة SrvNetAllocateBuffer بإرجاع مجرد مخزن مؤقت بالحجم المطلوب. بدلاً من ذلك، ستعيد مؤشرًا يشير إلى المنطقة الواقعة أسفل المخزن المؤقت للمستخدم مباشرةً من كتلة الذاكرة المخصصة من التجمع، والتي تحتوي على معلومات حول المخزن المؤقت المخصص. تخطيط كتلة الذاكرة المخصصة من التجمع كما يلي:

على الرغم من أن تقنية القراءة لدينا يمكنها فقط قراءة البايتات من منطقة "المخزن المؤقت للمستخدم"، لا يزال بإمكاننا استخدام تقنية أخرى لنسخ أجزاء من هيكل SRVNET_BUFFER_HDR إلى "المخزن المؤقت للمستخدم" لمخزن مؤقت آخر لقراءتها. عن طريق تعيين حقل Offset للإشارة إلى هيكل SRVNET_BUFFER_HDR الواقع خارج البيانات التي نريد قراءتها. نحتاج فقط إلى ضمان أن البيانات الموجودة هناك يمكن تفسيرها كبيانات مضغوطة صالحة، وإلا فلن يحدث النسخ.

البحث عن المؤشرات

دعنا نفحص حقول هيكل SRVNET_BUFFER_HDR ونرى ما إذا كان هناك أي محتوى يستحق القراءة:``` c #pragma pack(push, 1) struct SRVNET_BUFFER_HDR { /00/ LIST_ENTRY ConnectionBufferList; /10/ WORD BufferFlags; // 0x01 - no transport header, 0x02 - part of a lookaside list /12/ WORD LookasideListIndex; // 0 to 8 /14/ WORD LookasideListLogicalProcessor; /16/ WORD TracingDataCount; // 0, 1 or 2, for TracingPtr1/2, TracingUnknown1/2 /18/ PBYTE UserBufferPtr; /20/ DWORD UserBufferSizeAllocated; /24/ DWORD UserBufferSizeUsed; /28/ DWORD PoolAllocationSize; /2C/ BYTE unknown1[4]; /30/ PBYTE PoolAllocationPtr; /38/ PMDL pMdl1; /40/ DWORD BytesProcessed; /44/ BYTE unknown2[4]; /48/ SIZE_T BytesReceived; /50/ PMDL pMdl2; /58/ PVOID pSrvNetWskStruct; /60/ DWORD SmbFlags; /64/ PVOID TracingPtr1; /6C/ SIZE_T TracingUnknown1; /74/ PVOID TracingPtr2; /7C/ SIZE_T TracingUnknown2; /84/ BYTE unknown3[12]; }; #pragma pack(pop)

root@kitploit:~
Các con trỏ `UserBufferPtr`, `PoolAllocationPtr`, `pMdl1`, `pMdl2` là những con trỏ trỏ vào bên trong pool-allocated memory block, với các offset có thể được tính toán trước, vì vậy ta chỉ cần đọc một trong số chúng là được. Việc có một con trỏ trỏ đến pool-allocated memory block sẽ chắc chắn giúp ích cho ta trong việc khai thác. Ngoài ra các con trỏ sau cũng rất quan trọng:
- **ConnectionBufferList**: Một danh sách liên kết của tất các buffer được nhận nhưng chưa được xử lý của một kết nối. Phần đầu của danh sách này là một connection object được tạo bởi hàm SrvNetAllocateConnection trong srvnet.sys. Một buffer được thêm vào danh sách bởi hàm SrvNetWskReceiveComplete. Trong trường hợp của ta, sẽ chỉ có duy nhất một buffer trong danh sách, vì vậy cả 2 con trỏ (Flink và Blink của cấu trúc LIST_ENTRY) sẽ cùng trỏ đến đầu danh sách bên trong connection object.
- **pSrvNetWskStruct**: Ban đầu, một con trỏ trỏ đến connection object được đề cập ở trên. Con trỏ được set bởi hàm SrvNetWskReceiveEvent, nhưng bị ghi đè bởi hàm SrvNetWskReceiveComplete bằng con trỏ trỏ đến cấu trúc SRVNET_BUFFER_HDR. Vì vậy, đọc nó không hữu ích hơn đọc một trong bốn con trỏ đã nói ở trên. Nhân tiện, nếu bạn tìm kiếm “pSrvNetWskStruct”, bạn sẽ thấy rằng nó có một vai trò trong việc khai thác EternalBlue.
- **TracingPtr1/2**: Các con trỏ này chỉ được sử dụng khi tính năng tracing được kích hoạt.

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Như bạn có thể thấy, con trỏ hữu ích duy nhất khác để chúng ta đọc là một con trỏ trong cấu trúc ConnectionBufferList. Cả hai con trỏ (Blink và Flink trong cấu trúc LIST_ENTRY) đều trỏ đến connection object. Object này được đặt tên là SRVNET_RECV bởi nhà nghiên cứu EternalBlue, vì vậy ta cũng sẽ sử dụng tên này.

## Getting a module base address
Bây giờ, chúng ta đã biết cách lấy hai con trỏ - một con trỏ trỏ tới pool-allocated memory block và một con trỏ trỏ tới cấu trúc SRVNET_RECV - chúng ta có thể tự do sửa đổi hai buffers bằng cách sử dụng write-what-where primitive. Có thể sẽ có nhiều cách để đạt được RCE, nhưng việc lấy một base address của một module sẽ là lựa chọn đơn giản nhất vì có rất nhiều thứ từ chúng mà ta có thể sửa đổi trong data section của một module. Như chúng ta đã thấy, không có một con trỏ nào trong memory block được cấp phát bởi SrvNetAllocateBuffer trỏ đến một module. Tuy nhiên, vẫn có một số con trỏ trỏ đến các module:

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

Kĩ thuật đọc mà ta có chỉ cho phép ta đọc được dữ liệu ở vùng "User Buffer", trong khi đó các con trỏ này lại ở khá xa và được trỏ bởi khá nhiều con trỏ khác. Ta cần một đoạn code có thể làm được điều sau để copy giá trị con trỏ vào vùng "User Buffer":``` c
ptr1 = *(pSrvNetRecv + offset1)
value = *ptr1
ptr2 = *(pSrvNetRecv + offset2)
*ptr2 = value

إذا تمكنا من العثور على مقطع كود كهذا، فسنقوم بتفعيله لنسخ المؤشر الأول (مثال: HandlerFunctions) إلى منطقة "User Buffer"، وقراءته، ثم نسخ المؤشر الثاني (مثال: مؤشر الدالة Srv2ConnectHandler) إلى "User Buffer" وقراءته، واستنتاج عنوان القاعدة (base address) للوحدة النمطية منه. لقد بحث فريق Zecops عن مقطع كود كهذا لفترة طويلة، لكنهم لم يجدوا مقطعًا مناسبًا. في النهاية، استخدموا خيارًا آخر يتعلق بالدالة SrvNetFreeBuffer (التي تم تبسيطها كما يلي) والتي تؤدي وظيفة قريبة من المطلوب:``` c void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer) { PMDL pMdl1 = Buffer->pMdl1; PMDL pMdl2 = Buffer->pMdl2;

root@kitploit:~
if (pMdl2->MdlFlags & 0x0020) {
    // MDL_PARTIAL_HAS_BEEN_MAPPED flag is set.
    MmUnmapLockedPages(pMdl2->MappedSystemVa, pMdl2);
}

if (Buffer->BufferFlags & 0x02) {
    if (Buffer->BufferFlags & 0x01) {
        pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
        pMdl1->ByteCount -= 0x50;
        pMdl1->ByteOffset += 0x50;
        pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

        pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
        pMdl2->ByteCount = pMdl1->ByteCount;
        pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
        pMdl2->Size = /* some calculation */;
        pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL
    }

    Buffer->BufferFlags = 0;

    // ...

    pMdl1->Next = NULL;
    pMdl2->Next = NULL;

    // Return the buffer to the lookaside list.
} else {
    SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
    ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
}

}

root@kitploit:~
عند تحرير المخزن المؤقت (buffer)، إذا كانت أعلام المخزن (buffer flags) هي 0x02 (أي أن المخزن جزء من قائمة جانبية lookaside list) و0x01 (أي أن المخزن لا يحتوي على رأس نقل transport header) قد تم تعيينها، يتم تنفيذ بعض العمليات على كائني MDL لإضافة رأس النقل قبل إعادة تعيين الأعلام إلى 0 وإعادة المخزن إلى القائمة الجانبية. إذا نظرنا عن كثب خلف العمليات على كائني MDL، يمكننا ملاحظة أن مقطع الشيفرة يقوم بعملية قراءة مزدوجة بالإشارة (double-dereference-read) تتبعها كتابة مزدوجة بالإشارة (double-dereference-write) على متغيرين نتحكم بهما (مؤشرا MDL)، وهذا هو ما نبحث عنه. العيب هو أن المحتوى الذي نريد قراءته يتم تعديله أيضًا، وهو تأثير جانبي نأمل في تجنبه.

بناءً على ما سبق، هذه هي الطريقة التي ندير بها قراءة مؤشر AcceptSocket:
1. قم بتحضير المخزن A من قائمة جانبية بحيث تكون منطقة "المخزن المؤقت للمستخدم" (User buffer) مليئة بالأصفار. ستتضمن منطقة المخزن المؤقت للمستخدم لهذا المخزن المؤشر الذي سنقوم بقراءته.
2. قم بتحضير المخزن B من قائمة جانبية أخرى بحيث:
   - يشير المؤشر pMdl1 إلى عنوان مؤشر AcceptSocket ناقص 0x18 (بسبب إزاحة MappedSystemVa وهو 0x18 في بنية MDL).
   - يشير المؤشر pMdl2 إلى منطقة "المخزن المؤقت للمستخدم" للمخزن A.
   - يتم تعيين حقل الأعلام (Flags) إلى 0x03.

يمكننا تجاوز حقول بنية SRVNET_BUFFER_HDR عن طريق فك ضغطها من المخزن الأكبر باستخدام التقنية الموضحة في قسم [الملاحظة #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression) أعلاه.

3. عند تحرير المخزن B، سوف تحدث العمليات التالية:
   - ستتم قراءة أعلام MDL من كائن MDL الثاني في المخزن A. إذا تم تعيين علم MDL_PARTIAL_HAS_BEEN_MAPPED، سيتم استدعاء MmUnmapLockedPages ومن المحتمل أن يتعطل النظام. هذا هو السبب في أننا يجب أن نملأ المخزن بالأصفار في الخطوة 1.
   - سيتم تعديل مؤشر AcceptSocket والذاكرة المحيطة به كما هو موضح هنا:```
+00 |  00 00 00 00 00 00 00 00
+08 |  __ __ __|10 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  [+50..................]  <--  AcceptSocket
+20 |  __ __ __ __ __ __ __ __
+28 |  [-50......] [+50......]
  • سيتم قراءة مؤشر AcceptSocket والذاكرة المحيطة به كما هو موصوف هنا:``` +00 | __ __ __ __ __ __ __ __ +08 | __ __ __ __ __ __ __ __ +10 | __ __ __ __ __ __ __ __ +18 | ab cd ef gh ij kl mn op <-- AcceptSocket +20 | __ __ __ __ __ __ __ __ +28 | qr st uv wx __ __ __ __
root@kitploit:~
- سيتم تعديل منطقة "User buffer" للمخزن المؤقت A كما هو موضح هنا: (تحتوي البايتات البرتقالية على المؤشرات التي نريد قراءتها، نحتاج فقط إلى ترتيبها بشكل صحيح)```
+00 |  00 00 00 00 00 00 00 00
+08 |  ?? ?? 04 00 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  __ __ __ __ __ __ __ __
+20 |  00 c0 ef gh ij kl mn op
+28 |  qr st uv wx ab 0d 00 00

  1. اقرأ المؤشر AcceptSocket من منطقة "User buffer" الخاصة بالمخزن المؤقت A.

الخبر الجيد هو أننا قرأنا المؤشر. الخبر السيئ هو أننا أفسدنا بعض البيانات في بنية SRVNET_RECV. لحسن الحظ بالنسبة لنا، لا يؤثر الخلل على النظام طالما لم يحدث شيء للاتصال المعني. عندما يحدث شيء ما، على سبيل المثال إغلاق الاتصال، سيتعطل النظام. هذه ليست مشكلة لأننا سنحصل قريبًا على RCE ويمكننا إصلاح الخلل إذا أردنا.

بعد قراءة المؤشر AcceptSocket، نستمر في استخدام الأسلوب نفسه لقراءة المؤشر srvnet!SrvNetWskConnDispatch. سبب قراءة المؤشر AcceptSocket وليس المؤشر HandlerFunctions هو أن مصفوفة HandlerFunctions مشتركة بين جميع الاتصالات، بينما المخزن المؤقت المشار إليه بواسطة AcceptSocket غير مشترك مع الاتصالات الأخرى. لذلك، إذا أفسدنا أجزاءً في AcceptSocket، فسيؤثر ذلك فقط على استقرار اتصال واحد.

إذا كان لدينا نسخة من ملف srvnet.sys المستخدم على الجهاز المستهدف، يمكننا بسهولة استنتاج العنوان الأساسي لوحدة srvnet.sys عن طريق طرح الإزاحة للمؤشر SrvNetWskConnDispatch الذي تم تسريبه.

تنفيذ القراءة التعسفية

لنفترض أن لدينا العنوان الأساسي لوحدة srvnet.sys، يمكننا استدعاء أي دالة في الوحدة. ولكن ماذا عن وسائط الدالة؟ يتم استدعاء الدالة srv2!Srv2ReceiveHandler بواسطة SrvNetCommonReceiveHandler ويكون شكل الاستدعاء كالتالي:``` c HandlerFunctions = *(pSrvNetRecv + 0x118); Arg1 = *(ULONG_PTR)(pSrvNetRecv + 0x128); Arg2 = *(ULONG_PTR)(pSrvNetRecv + 0x130); (HandlerFunctions[1])(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8);

root@kitploit:~
تتم قراءة الوسيطين الأولين من بنية SRVNET_RECV، لذا يمكننا التحكم فيهما، لكن لا يمكننا التحكم في الوسائط المتبقية. تنص اصطلاحات استدعاء x86-64 على أن المتصل هو المسؤول عن تخصيص وتحرير مساحة المكدس للوسائط، لذلك على الرغم من أن الدالة كانت مخصصة للاستدعاء بـ 8 وسائط، يمكننا استبدال المؤشر بدالة تتوقع أي عدد آخر.

![](https://assets.kitploit.com/production/public/readmes/24502/223827f49f26993604c20b873d75ea5b11d306a85ff5210ceb2583f0a0b9452c.png)

فيما يلي الخطوات التي سنستخدمها لتشغيل استدعاء الدالة:
1. أرسل رسالة مصممة خصيصًا بحيث يتم نسخ مؤشر بنية SRVNET_RECV للاتصال إلى مخزن مؤقت يمكننا قراءته.
2. أرسل رسالة صالحة أخرى، هذه الرسالة ستعيد استخدام نفس بنية SRVNET_RECV، ولكن دون إغلاق الاتصال. لاحظ أنه عند إغلاق الاتصال، لا يتم تحرير بنية SRVNET_RECV. يتم استدعاء الدالة SrvNetPrepareConnectionForReuse لإعادة تعيين البنية بحيث يمكن إعادة استخدامها للاتصال التالي.
3. اقرأ مؤشر بنية SRVNET_RECV الذي نسخناه في الخطوة 1.
4. استبدل مؤشر HandlerFunctions والوسائط باستخدام تقنية write-what-where primitive.
5. أرسل رسالة إضافية عبر الاتصال من الخطوة 2 لاستدعاء الدالة البديلة لـ srv2!Srv2ReceiveHandler.

الآن كل ما علينا فعله هو العثور على دالة لنسخ الذاكرة من موقع إلى آخر، حتى نتمكن من نسخ الذاكرة بشكل عشوائي إلى مخزن مؤقت في pool يمكننا القراءة منه. memcpy هو خيار، و srvnet.sys يحتوي على دالة كهذه (بشكل أدق memmove)، لكن هذه الدالة تتطلب وسيطًا ثالثًا، وهو الوسيط الذي يحدد عدد البايتات التي يجب نسخها ولا نتحكم فيه. ومع ذلك، نحن لسنا مقيدين بالدوال المطبقة في srvnet.sys، يمكننا أيضًا استدعاء دوال من جدول الاستيراد الخاص بـ srvnet، ودالة RtlCopyUnicodeString هي اختيار ممتاز لتحقيق ما نريد.

تأخذ الدالة RtlCopyUnicodeString مؤشرين من نوع UNICODE_STRING كوسيطين وتنسخ محتوى source string إلى destination string. على عكس سلاسل C المنتهية بـ NULL، يتم تعريف السلاسل في kernel بواسطة بنية UNICODE_STRING التي تحتوي على مؤشر يشير إلى السلسلة وطول السلسلة بالبايت. يمكن لمخزن السلسلة أن يحتوي على أي بيانات ثنائية. إذا نظرت إلى كود الدالة RtlCopyUnicodeString، يمكنك رؤية أن النسخ يتم باستخدام الدالة memmove، أي نسخ بيانات ثنائية خالصة. كل ما علينا فعله هو تحضير بنيتين من نوع UNICODE_STRING واستدعاء RtlCopyUnicodeString، ثم قراءة البيانات المنسوخة:

![](https://assets.kitploit.com/production/public/readmes/24502/8a450b86ecf834ec4905a2bb72dab3781d1dd2ad56ee197a003e282180ce51bc.png)

## تنفيذ shellcode
بعد تحقيق primitive قراءة عشوائية مناسبة، ننتقل إلى التحدي التالي نحو هدف تنفيذ الكود عن بُعد (Remote Code Execute) من خلال تشغيل shellcode. سنستخدم التقنية التي عرضها Morten Schenk في محاضرته عن [Black Hat USA 2017](https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf) (الصفحات 47-51).

الفكرة هي كتابة شيلكود أسفل بنية KUSER_SHARED_DATA التي لها عنوان ثابت، وهو العنوان الوحيد غير العشوائي في تخطيط ذاكرة kernel للإصدارات الأخيرة من Windows. ثم تعديل إدخال جدول الصفحات (page table entry) المرتبط، لجعل الصفحة قابلة للتنفيذ. العنوان الأساسي لإدخالات جدول الصفحات في kernel عشوائي، ولكن يمكن الوصول إليه من الدالة MiGetPteAddress في ntoskrnl.exe. فيما يلي الخطوات التي سنستخدمها لتنفيذ shellcode الخاص بنا:
1. استخدم primitive القراءة العشوائية للحصول على العنوان الأساسي (base address) لـ ntoskrnl.exe من جدول الاستيراد الخاص بـ srvnet.
2. اقرأ العنوان الأساسي لإدخال جدول الصفحات من الدالة MiGetPteAddress، كما هو موصوف في شرائح Morten.
3. اكتب shellcode في العنوان KUSER_SHARED_DATA + 0x800 (0xFFFFF78000000800). لاحظ أنه يمكننا أيضًا استخدام أحد مخازن pool المؤقتة لتخزين shellcode، لكن استخدام KUSER_SHARED_DATA يجعل الأمور أبسط.
4. احسب عنوان إدخال جدول الصفحات المرتبط وقم بمسح البت NX للسماح بالتنفيذ، كما هو موصوف في شرائح Morten.
5. استدعي shellcode باستخدام التقنية المذكورة أعلاه لاستدعاء دالة عشوائية.

شيلكود الذي يستخدمه Zecops للـ reverse shell هو [sleepya’s shellcode](https://github.com/worawit/MS17-010/tree/master/shellcode) الذي كُتب لغرض استغلال EternalBlue. لقد قاموا بتعديل هذا الشيلكود ليعمل على الإصدارات الأخيرة من Windows.

# التصحيح

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

معلومات حزمة SMB ستكون ذات بنية مشابهة تقريبًا لما سبق. افترض أننا بحاجة إلى تسريب عنوان User Buffer، سنحتاج إلى قراءة المؤشر UserBufferPtr. لقراءة هذا المؤشر، سنستفيد من [التقنية](https://github.com/datntsec/CVE-2020-1206#srvnetallocatebuffer-and-the-allocated-buffer-layout) التي تضع حقل الإزاحة خارج المؤشر بحيث يتم نسخه إلى منطقة user buffer لمخزن مؤقت آخر.

مثال على حزمة SMB يرسلها العميل بمحتوى كما يلي:```c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x0
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x2116
Data = ‘A’ * 0x1101.

تصل هذه الحزمة إلى الخادم ويتم تخزينها في مخزن مؤقت تم إنشاؤه بواسطة الدالة SrvNetAllocateBuffer. نظرًا لأن حجم الحزمة بالكامل يتراوح بين 0x1100 و 0x2100، فإن هذه الدالة ترجع تخصيصًا بمنطقة مخزن مستخدم بحجم 0x2100 (سنسميها Alloc A)، ثم تخزن معلومات العميل المرسلة كما هو موضح في الصورة أدناه:

يمكننا أن نرى أن الجزء من العنوان 0xffffd38439044050 إلى 0xffffd38439045160 هو بيانات أرسلها العميل، والجزء من 0xffffd38439045160 إلى 0xffffd38439046150 هو بيانات لم تتم تهيئتها على جانب الخادم، والجزء من 0xffffd38439046150 إلى 0xffffd38439046240 هو بيانات SRVNET_BUFFER_HDR الخاصة بـ Alloc A. وبالتالي، فالمؤشر الذي نريد قراءته سيكون في 0xffffd38439046150 + 0x18 = 0xffffd38439046168.

لقراءة هذا المؤشر، استخدمت تقنية التي ذكرتها سابقًا، وهي تعيين حقل الإزاحة ليتجاوز المؤشر المطلوب قراءته. ولهذا السبب، على الرغم من أن الحزمة أعلاه أصغر من 0x2100، إلا أن الإزاحة تم ضبطها على 0x2116.

بعد ذلك، يقوم خادم SMB باستدعاء الدالة SrvNetAllocateBuffer لتخصيص منطقة ذاكرة بناءً على مجموع OriginalCompressedSegmentSize و Offset (0x2116). ومن ثم، يتم تخصيص مخزن بمنطقة مخزن مستخدم بحجم 0x4100 (سنسميه Alloc B). ستكون البيانات المخصصة على النحو التالي:

لتجنب الأخطاء غير المرغوب فيها، قمت مسبقًا بإنشاء وإعادة إنشاء عدة مرات مخازن مؤقتة بنفس قائمة lookasite مثل Alloc B وملؤها ببايتات 0x0.

بعد ذلك، يشرع خادم SMB في فك ضغط البيانات المضغوطة ونسخ البيانات غير المضغوطة التي أرسلها العميل إلى منطقة المخزن المستخدم لـ Alloc B:

كما يمكننا أن نرى، لا توجد بيانات مضغوطة بسبب OriginalCompressedSegmentSize = 0، سيقوم البرنامج بنسخ البيانات من 0xffffd38439044060 إلى 0xffffd38439044060 + 0x2116 = 0xffffd38439046176 من Alloc A إلى منطقة المخزن المستخدم لـ Alloc B. وبذلك، تم نسخ جزء من معلومات SRVNET_BUFFER_HDR الخاصة بـ Alloc A إلى منطقة المخزن المستخدم لـ Alloc B.

الآن سنستخدم تقنية التي ذكرناها سابقًا لتسريب عنوان تجمع التخصيص (عنوان المخزن المستخدم).

لنفترض أننا نريد معرفة ما إذا كانت بايتة واحدة في العنوان 0xffffd3843636f15e أكبر من 0x7f أم لا؟ سنقوم بإنشاء حزمة SMB بالمعلومات التالية:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0x7f+3) + ‘\xff’(0xff - 0x7f)) + ‘\xff’*0x1fe9
root@kitploit:~
لماذا نحتاج إلى إنشاء SMB مثل هذا؟ سنقوم بتحليل كل جزء بالتدريج. أولاً، مجموع `OriginalCompressedSegmentSize` و `Offset` هو `0x4100`، وبالتالي سيتم إعادة استخدام `alloc` له نفس `user buffer`، أي `Alloc B` الذي تم استخدامه سابقًا. نظرًا لأننا نريد تخمين ما إذا كانت البايتة الموجودة في العنوان `0xffffd3843636f15e` أكبر من `0x7f` أم لا، وهذا العنوان يبعد عن `user buffer` بمقدار `0x210e`، لذلك ستحتوي البيانات غير المضغوطة على `0x210e` بايت (`'B' * 0x210e`). بعد ذلك ستكون منطقة بيانات مضغوطة صالحة (مضغوطة بواسطة دالة `compress()`)، ويتبعها بيانات مضغوطة غير صالحة (`'\xff' * 0x1fe9`). بحيث عند إجراء فك الضغط، فقط الجزء المضغوط الصالح يتم فك ضغطه إلى منطقة `alloc` أخرى، ثم سيتم قطع الاتصال بسبب البيانات المضغوطة غير الصالحة اللاحقة، ولن يتم نسخ البيانات غير المضغوطة إلى ذلك `alloc`، وبالتالي ستبقى البيانات التي نسخناها سابقًا كما هي.

![](https://assets.kitploit.com/production/public/readmes/24502/3e1ffd768b120645917e7f1757987f80c79514b52f3bc18e46fdfb0a2d2ca05f.png)

أعلاه هو `Alloc` يحتوي على المعلومات التي تحدثنا عنها أعلاه والتي تم إنشاؤها بواسطة خادم SMB. بعد ذلك، سيقوم خادم SMB باستدعاء الدالة `SrvNetAllocateBuffer` لإنشاء `Alloc` مماثل. نظرًا لأن مجموع `OriginalCompressedSegmentSize` و `Offset` هو `0x4100`، فسيتم إعادة استخدام `Alloc B`:

![](https://assets.kitploit.com/production/public/readmes/24502/5b218cbd142d58da175ee6a58bfc505646a560467db6db28ab70e0db34ee9ce3.png)

بعد ذلك، يقوم خادم SMB بفك ضغط المعلومات المرسلة من العميل إلى `user buffer` الخاص بـ `Alloc B` عند `offset` المحدد.

![](https://assets.kitploit.com/production/public/readmes/24502/f3b545366e8f3bcff6a69d184cc383bfcf70643c23ff0ac01ece1a95cc0fd88d.png)

البيانات الحمراء هي البيانات التي تم فك ضغطها، وسيظل الباقي كما هو. كما نرى في الصورة أعلاه، ستبقى البايتة التي نحتاج إلى معرفتها كما هي، ويأتي بعدها مباشرة البيانات التي تم فك ضغطها للتو.

لمعرفة ما إذا كانت هذه البايتة أكبر من `0x7f` أم لا، نقوم بالخطوات التالية:

نستمر في إنشاء حزمة SMB بمحتوى كما يلي:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x2004
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x20fd
Data = ‘B’ * 0x20f1

على الرغم من أن مجموع OriginalCompressedSegmentSize و Offset أكبر من 0x4100، إلا أنه عند تخصيص منطقة alloc لتخزين الحزمة المرسلة من العميل (بحجم إجمالي أقل من 0x4100)، لا يزال خادم SMB يخصص Alloc بمنطقة مخزن مؤقت للمستخدم بحجم 0x4100 بايت كما هو موضح في الصورة أدناه:

البيانات الملونة باللون الأخضر في الأعلى هي بيانات Alloc B التي تم تخصيصها سابقًا، ونظرًا لكونها من نفس قائمة lookaside، يتم إعادة استخدامها.

بعد ذلك، سيقوم خادم SMB باستدعاء الدالة SrvNetAllocateBuffer لتخصيص Alloc يحتوي على البيانات بعد فك الضغط:

من خلال عملية فك الضغط، سيقوم خادم SMB باستخراج البيانات من User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. ستكون البيانات المستخرجة على النحو التالي:

مع خوارزمية فك الضغط المذكورة أعلاه، سيتم استخراج أول 2 بايت واستخدامها كطول للكتلة (length)، وبناءً على هذا الطول، سيتم استخراج الجزء التالي بعد الطول وفك ضغطه. كما هو موضح أعلاه، سيكون الطول 0xB0D3، ولكن وفقًا للخوارزمية، فإن الطول الحقيقي يتبع الصيغة: length = length & 0xFFF + 1 → سيكون الطول 0xD4. سيتم استخراج D4 بايت التالية وفك ضغطها بشكل طبيعي حتى يتم العثور على البايت FF (نظرًا لأن الـ D4 بايت ستشمل جميع البايتات 00 وجزءًا من البايت FF)، عندها تعتبر البيانات المضغوطة غير صالحة، ولن يستمر فك الضغط وسيتم قطع الاتصال.

بناءً على قطع الاتصال من الخادم، يمكننا تخمين أن البايت الذي نحتاج إلى معرفته سيكون أكبر من 0x7f.

ماذا لو كان البايت الذي نحتاج إلى تخمينه أصغر من ذلك؟ سنستمر في التحليل أعلاه، لكن هذه المرة سنستخدم بايت المقارنة D7، وبالتالي D3 سيكون أصغر من D7. دعنا نرى ماذا سيحدث:

أولاً، سيتم إرسال الحزمة التالية إلى خادم SMB:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0xd7+3) + ‘\xff’(0xff - 0xd7)) + ‘\xff’*0x1fe9
root@kitploit:~
سيقوم خادم SMB بإنشاء تخصيص تخزين كما يلي:

![](https://assets.kitploit.com/production/public/readmes/24502/3df2c6c8e5de90714455663bbb922efec7ae6888a970a3d65e389a28aea16837.png)

ثم يقوم باستدعاء الدالة SrvNetAllocateBuffer لتخصيص alloc كما يلي:

![](https://assets.kitploit.com/production/public/readmes/24502/51affc73361727907ada6fb3f439831d0e95cee531a65106fa79ce4519ceaab8.png)

بالطبع، يتم إعادة استخدام هذا alloc من alloc الذي له نفس lookaside list (Alloc B). بعد ذلك، يواصل البرنامج فك الضغط بشكل طبيعي مع البيانات المضغوطة الصالحة، ويقطع الاتصال بالبيانات المضغوطة غير الصالحة:

![](https://assets.kitploit.com/production/public/readmes/24502/01a9cc43acbc6b7cc283f40a0dbe348608a3566bfc34ce24aa2cc694315e67a3.png)

في الخطوة التالية، سنرسل بيانات مشابهة للبيانات السابقة وسيقوم خادم SMB بتخصيص alloc مناظر:

![](https://assets.kitploit.com/production/public/readmes/24502/ed438d4d0049b2188573e0fb3171cadf23701c7b0cef2de43a52434c07381341.png)

البيانات الملونة باللون الأخضر كما هو موضح أعلاه هي بيانات Alloc B التي تم تخصيصها في المرة السابقة، وبسبب نفس lookaside list، يتم إعادة استخدامها.

بعد ذلك، سيقوم خادم SMB باستدعاء الدالة SrvNetAllocateBuffer لتخصيص Alloc يحتوي على البيانات بعد فك الضغط:

![](https://assets.kitploit.com/production/public/readmes/24502/7ed3dca4594bd1772c44689190d4df3b597d43daf7c87edb281b3e82a834884e.png)

من خلال عملية فك الضغط، سيأخذ خادم SMB البيانات من User buffer address + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. ستكون البيانات المأخوذة على النحو التالي:

![](https://assets.kitploit.com/production/public/readmes/24502/68f677093ef93e02c5df9d70dce560e4d06e74446970f1f14f1ad37f08ce898e.png)

كما في المرة السابقة، سيكون الطول الأولي 0xB0D3، وبعد الحساب سيصبح 0xD4. وسيأخذ الـ D4 بايت التالية ويقوم بفك الضغط بشكل طبيعي. ومع ذلك، نظرًا لأن D4 < D7، فإن البيانات المضغوطة المأخوذة لفك الضغط في هذه اللحظة تحتوي فقط على بايتات 0. سيقوم بفك الضغط بشكل طبيعي حتى نهاية تلك الكتلة. ثم سيقوم بأخذ طول الكتلة التالية من خلال طول الكتلة السابقة، والبايتان التاليتان هما D5 و D6 وهما 0x0، وبالتالي سيكون الطول في هذه اللحظة 0x0، وبالتالي سيكون الطول المأخوذ 0x0000 → انتهاء عملية فك الضغط → نجاح فك الضغط → يقوم خادم SMB بإرجاع استجابة → نعرف أن البايت المطلوب أصغر من أو يساوي 0xD7.

وبالمثل، سنستمر حتى يتم تسريب الـ 6 بايتات الكاملة لعنوان واحد. سنحصل على عنوان allocation pool.

عند الحصول على عنوان allocation pool، سنقوم بالبحث عن عنوان srvnet base address من خلال الحصول على المؤشر الذي يشير إلى بنية SRVNET_RECV بطريقة مشابهة لتسريب عنوان allocation pool.

بعد الحصول على العنوانين: allocation pool و SRVNET_RECV بقيم `0xffffd38439044000` و `0xffffd3843654ddd8` على التوالي، نقوم بتسريب srvnet base address.

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

![](https://assets.kitploit.com/production/public/readmes/24502/239c0c59c0e492097bdaf2eac907812124a89ef8bb31841287202ae80f4d13d0.png)

لقراءة المؤشر AcceptSocket، نحتاج إلى القيام بما يلي:
1. تحضير Alloc A من lookaside list بحيث تكون منطقة "User buffer" ممتلئة بالأصفار. ستحتوي هذه المخازن المؤقتة لاحقًا على المؤشر الذي سنقرأه. هنا، سيتم استخدام Alloc A من Alloc المقابل لعنوان allocation pool الذي قمنا بتسريبه. وبالتالي، ستبدأ منطقة User buffer لـ Alloc A من العنوان 0xffffd38439044050 من خلال استخدام نفس lookaside list.
2. تحضير Alloc B من lookaside list آخر بحيث:
- يشير المؤشر pMdl1 إلى عنوان المؤشر AcceptSocket مطروحًا منه 0x18 (نظرًا لأن إزاحة MappedSystemVa هي 0x18 في بنية MDL).
- يشير المؤشر pMdl2 إلى منطقة "User buffer" لـ Buffer A.
- يتم تعيين الحقل Flags إلى 0x03.

وبالتالي، فإن عنواني المؤشرين Mdl هما على التوالي: mdl1_ptr: `0xffffd3843654de68`، mdl2_ptr: `0xffffd38439045250`.

يمكننا الكتابة فوق حقول بنية SRVNET_BUFFER_HDR عن طريق فك ضغطها من مخزن مؤقت أكبر من خلال التقنية الموصوفة في قسم [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression).

سأشرح هذه الخطوة بمزيد من التفصيل بعد الخطوة 4.

3. عند تحرير Buffer B، ستحدث العمليات التالية:
- ستتم قراءة أعلام MDL من MDL الثاني في buffer A. إذا تم تعيين علم MDL_PARTIAL_HAS_BEEN_MAPPED، سيتم استدعاء MmUnmapLockedPages وقد يتعطل النظام. لهذا السبب يجب ملء المخزن المؤقت بالأصفار في الخطوة 1.
- سيتم تعديل منطقة "User buffer" لـ Alloc A وستحتوي على المعلومات التي نحتاج قراءتها.
4. قراءة المؤشر AcceptSocket من منطقة "User buffer" لـ buffer A.
- استخدام تقنية تسريب العنوان المستخدمة أعلاه لقراءة المؤشر AcceptSocket.

سأشرح الآن الخطوات المذكورة أعلاه بمزيد من التفصيل:

الخطوة 1 بسيطة جدًا ومشابهة لما سبق، لذا لن أتحدث عنها.

في الخطوة 2، أولاً سننشئ حزمة نرسلها إلى خادم SServer بمحتوى كما يلي:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = -0x38
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x10138
Data = ‘A’ * 0x10138 + compress(mdl1_ptr  + ‘\x00’*0x10 + mdl2_ptr) + ‘\xff’*0x10

كما نرى، يحتوي OriginalCompressedSegmentSize على قيمة سالبة ومجموع OriginalCompressedSegmentSize + Offset = 0x10100. ومع ذلك، فإن حجم الحزمة التي يرسلها العميل إلى الخادم أكبر من 0x10100. وبالتالي، فإن التخصيص الأولي (Alloc) الذي ينشئه الخادم قبل فك الضغط سيكون أكبر من التخصيص الذي يحتوي على البيانات بعد فك الضغط. يتم تعيين قيمة OriginalCompressedSegmentSize بالسالب هنا لمساعدة مجموع OriginalCompressedSegmentSize و Offset ليكون مساوياً تماماً لـ 0x10100، دون التأثير على موقع البيانات المضغوطة، لأنه يعتمد على Offset. أما 0x38 فهو إزاحة المؤشر Mdl1 في هيكل SRVNET_BUFFER_HDR.

وهكذا سينشئ الخادم تخصيصاً (Alloc) يحتوي على بيانات العميل كما يلي:

بعد ذلك، سوف يستدعي الدالة SrvNetAllocateBuffer لتخصيص تخصيص بحجم منطقة المخزن المؤقت للمستخدم 0x10100، أي Alloc B وفقاً للخطوات المذكورة أعلاه:

يبدأ عملية فك الضغط، وبطبيعة الحال يتم فك ضغط جزء فقط من البيانات الصالحة:

بناءً على الصور أعلاه، يمكن ملاحظة أن منطقة المؤشرين Mdl في SRVNET_BUFFER_HDR الخاص بـ Alloc B قد تم تعديلها إلى القيمة التي نريدها.

على غرار ما سبق، هذه المرة سنقوم بتعيين العلامة (flag) إلى 3 عن طريق ضبط إزاحة الحزمة المرسلة كما يلي:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = -0x10
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x10110 Data = ‘A’ * 0x10110 + compress(‘\x00\x03’) + ‘\xff’*0x10
root@kitploit:~
أخيرًا سيكون على الشكل التالي:

![](https://assets.kitploit.com/production/public/readmes/24502/467ea07dff7cfaca7062ed50d1605e40586c412f40bcadaf01aa37d862efd3bd.png)

عند تحرير Alloc B، سيتم تشغيل الأكواد التالية:``` c
pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
pMdl1->ByteCount -= 0x50;
pMdl1->ByteOffset += 0x50;
pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
pMdl2->ByteCount = pMdl1->ByteCount;
pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
pMdl2->Size = /* some calculation */;
pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL

كما ورد أعلاه، فإن pMdl1->MappedSystemVa (الإزاحة 0x18) سيحتوي على قيمة pMdl1->MappedSystemVa + 0x50 = 0xffffd3843654de68 + 0x18 + 0x50 = 0xffffd3843654ded0.

قبل تحرير Alloc B، سيكون SRVNET_RECV:

بعد تنفيذ الأسطر الأربعة الأولى من الكود أعلاه:

قبل تحرير Alloc a:

بعد تنفيذ الكود أعلاه بالكامل:

والبايتات التي نحتاج قراءتها من Alloc A هي البايتات الملونة باللون الأزرق أدناه:

وبالتالي، نحتاج فقط إلى استخدام تقنية تسريب البايت بايت واحد (byte-by-byte leak) أعلاه للحصول على عنوان AcceptSocket + 0x50. كما في هذا الجزء سيكون 0xffffd3843ea02418 → AcceptSocket: 0xffffd3843ea023c8

وبالمثل سنفعل لتسريب عنوان AcceptSocket→ srvnet!SrvNetWskConnDispatch

نحتاج إلى تجهيز كل شيء كما يلي:

بعد تحرير Alloc B، ستتغير الأمور كما يلي:

البايتات التي نحتاج معرفتها للحصول على عنوان AcceptSocket-> srvnet!SrvNetWskConnDispatch + 50 ستكون موجودة في Alloc A، وهذه البايتات هي البايتات الملونة باللون الأزرق في الشكل أدناه:

وبالتالي، AcceptSocket-> srvnet!SrvNetWskConnDispatch سيكون 0xfffff80060e9d170، بافتراض أننا نعرف إزاحته (offset) في الوحدة النمطية srvnet.sys، يمكننا حساب عنوان قاعدة srvnet.

في هذا الجزء، قاعدة srvnet هي: 0xFFFFF80060E70000 مع إزاحة srvnet!SrvNetWskConnDispatch تساوي 0x2d170.

بعد ذلك سنستخدم تقنية Write-what-where primitive في CVE-2020-0796 للكتابة حسب الرغبة في ذاكرة معينة.

أولاً سنبحث عن طريقة لتسريب عنوان قاعدة ntoskrnl، من خلال تسريب عنوان الدالة IoSizeofWorkItem التي يستوردها srvnet. لفعل هذا، سنقوم أولاً بإنشاء هيكلين UNICODESTRING كما يلي:``` c // Destination unicode string desLength = 6; desMaximumLength = 6; desBuffer = allocation_pool_object_ptr + 0x1650 + 0x20 + 2;

// Source unicode string srcLength = 6; srcMaximumLength = 6; srcBuffer = srvnet_base_ptr + OFFSETS['srvnet!imp_IoSizeofWorkItem'];

root@kitploit:~
مع `allocation_pool_object_ptr` هو عنوان تجمع التخصيص الذي تم تسريبه و `OFFSETS['srvnet!imp_IoSizeofWorkItem']` هو إزاحة دالة IoSizeofWorkItem التي تم استيرادها بواسطة srvnet.

سيتم تخزين هيكلي UNICODE_STRING هذين في `allocation_pool_object_ptr + 0x1650` من خلال تقنية Write-what-where التي تم العثور عليها في CVE-2020-0796. 
أولاً، سنقوم بتخزين سلسلة Unicode الوجهة في `allocation_pool_object_ptr + 0x1650`، ثم نبدأ في إنشاء حزمة SMB كما يلي:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0xffffffff
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x22
Data:
sentinel = os.urandom(2)  // 16 bits for verification
data = struct.pack('<HHIQ', desLength, desMaximumLength, 0, desBuffer)  // dest unicode string
data += struct.pack('<HHIQ', srcLength, srcMaximumLength, 0, srcBuffer) // src unicode string
data += sentinel
data_to_compress = os.urandom(0x1100 - len(data))
// 0x18 null bytes that override the struct.
data_to_compress += b'\x00'*0x18
// Target address.
data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650)
data = data + compress(data_to_compress)

في الأعلى، تحتوي البيانات على sentinel تم إنشاؤها باستخدام دالة os.urandom(2)، سيكون طولها 2 بايت، وهذه الـ 2 بايت ستساعدنا في معرفة ما إذا كان العنوان الذي سربناه هو العنوان الذي نحتاج إلى تسريبه بالفعل من خلال مقارنته بعد نجاح عملية التسريب.

إذا كان الحجم الإجمالي للحزمة المرسلة من العميل أكبر من 0x1100 (وهذا يعتمد على البيانات العشوائية قبل الضغط)، فمن المؤكد أنه سيتم استخدام allocation_pool_object_ptr لتخزينها على خادم SMB:

بعد ذلك، سيقوم خادم SMB باستدعاء الدالة SrvNetAllocateBuffer لتخصيص مساحة ذاكرة لعملية فك الضغط. ولكن نظرًا لأن مجموع OriginalCompressedSegmentSize و Offset هو 0x21، فإنه سيقوم فقط بتخصيص مساحة تحتوي على مخزن مستخدم بحجم 0x1100:

يحدث خطأ فيضان الكومة (كما تم شرحه في CVE-2020-0796) وبعد أن يقوم خادم SMB بفك الضغط (قبل حدوث نسخ البيانات غير المضغوطة) سيحدث ما يلي:

وبالتالي، يشير المؤشر UserBufferPtr إلى بداية allocation_pool_object_ptr + 0x1650 وعند حدوث عملية النسخ، سيكون allocation_pool_object_ptr كالتالي:

وبالمثل، سنقوم بإدراج sentinel إضافي في الأسفل عن طريق إرسال الحزمة التالية:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0xffffffff
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x2 Data: data = sentinel data_to_compress = os.urandom(0x1100 - len(data)) // 0x18 null bytes that override the struct. data_to_compress += b'\x00'*0x18 // Target address. data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650 + 0x28) data = data + compress(data_to_compress)
root@kitploit:~
بهذا عندما يتلقى خادم SMB الحزمة، فإنه سيخصص منطقة الذاكرة المقابلة، وفي هذه اللحظة تكون المنطقة المخصصة هي allocation_pool_object_ptr وستحتوي على البيانات كما يلي:

![](https://assets.kitploit.com/production/public/readmes/24502/24cc306a702cdb5f575bd1d612ea6b35db87aaf875fd45fa64977e50c61e85ed.png)

بعد عملية فك الضغط ستكون البيانات كما يلي:

![](https://assets.kitploit.com/production/public/readmes/24502/d6442e5a936e1c46ebb9e3a4651e8859dca56c1aa09e9e79ed9155ea105dede2.png)

وبهذا نكون قد أنشأنا سلسلتي unicode وحارسين (sentinel) للتحقق من صحة البيانات التي نسربها.

بعد ذلك سنقوم باستدعاء الدالة `RtlCopyUnicodeString` ونمرر سلسلتي unicode المذكورتين أعلاه إليها.

لاستدعاء الدالة `RtlCopyUnicodeString`، سنقوم أولاً بكتابة فوق المؤشر HandlerFunctions ليصبح عنوان الدالة `RtlCopyUnicodeString`، هذه الدالة مستوردة بواسطة الوحدة srvnet ولها إزاحة (حسب وحدتي) تساوي 0x32288.

وبالتالي باستخدام تقنية write-what-where، سنكتب العنوان 0xFFFFF80060E70000 + 0x32288 - 0x8 إلى HandlerFunctions.

أولاً سنقوم بتسريب مؤشر SRVNET_RECV (0xffffe00f0b593dd8).

![](https://assets.kitploit.com/production/public/readmes/24502/f7c5bde869eb5ba21a0b9dd4e2034417487150592178aa1eb1aef523e246a5c2.png)

سنقوم بحفظ الاتصال لمواصلة إرسال الحزم أدناه.

بعد ذلك سنستخدم تقنية write-what-where لكتابة المؤشر RtlCopyUnicodeString - 0x8 (السبب في -0x8 هو استبدال الدالة RtlCopyUnicodeString بالدالة Srv2ReceiveHandler في HandlerFunctions)

![](https://assets.kitploit.com/production/public/readmes/24502/a88b44518f26ed6f0958772bdd4bf124c7c2403a525944b060a37cf17309a092.png)

ثم سنقوم بكتابة المؤشرين لسلسلتي unicode اللتين تم إنشاؤهما أعلاه بالتتابع في وسيطي HandlerFunction

![](https://assets.kitploit.com/production/public/readmes/24502/691bdb0ce599face8b1f35393db9cc37e1f85a075c4d259a7fb96adee9fb66d2.png)

في هذه اللحظة، وعلى نفس الاتصال، تم استبدال الدالة Srv2ReceiveHandler بالدالة RtlCopyUnicodeString، لذلك عندما نرسل حزمة، سيتم استدعاء الدالة RtlCopyUnicodeString ونسخ سلسلة Unicode.

![](https://assets.kitploit.com/production/public/readmes/24502/2410149b163fd4ebbfb13fa5d2a127cafcfdc82146c33544b0a6cc6905c13d2b.png)

المهمة التالية التي نحتاجها هي تسريب 10 بايت من العنوان من 0xffffd38439045670 إلى 0xffffd3843904567a (بما في ذلك الحارسين على طرفي العنوان المراد تسريبه). ثم نتحقق مما إذا كان البايتان في بداية ونهاية العنوان المسرب هما حارسان، إذا كانا كذلك نكون قد سربنا العنوان الصحيح (0xfffff8068152c380).

بعد تسريب عنوان nt!IoSizeofWorkItem (0xfffff8068152c380)، سنطرح إزاحته (0x12C380) للحصول على عنوان قاعدة ntoskrnl (0xfffff80681400000).

ملاحظة: إزاحة كل ملف وحدة على إصدارات ويندوز المختلفة مختلفة، لذا تأكد من حصولك على ملف الوحدة الصحيح على الجهاز الهدف.

بالمثل، بعد الحصول على عنوان قاعدة ntoskrnl، سنحصل على MiGetPteAddress (0xBA968) ونحصل على عنوان قاعدة PTE (MiGetPteAddress + 0x13):

![](https://assets.kitploit.com/production/public/readmes/24502/5c92df61b331c038bd90b2ccb878b7027bdbf6338602aecdda1b49a867eb42b1.png)

الخطوة التالية، سنقوم بكتابة شيل كود إلى 0xFFFFF78000000800 باستخدام تقنية write-what-where. ثم نعيد حساب عنوان الشيل كود في pte عبر الصيغة أدناه ونمسح بت NX ليكون الشيل كود قابلاً للتنفيذ:``` c
shellcode_addr >>= 9
shellcode_addr &= 0x7FFFFFFFF8
shellcode_addr += pte_base

أخيرًا، سنكتب عنوان shellcode في allocation_pool_object_ptr + 0x50 + 0x1600 ونشرع في استدعاء shellcode عن طريق استبدال هذا العنوان بـ HandlerFunctions وتمرير عنوان nt_base_ptr إلى shellcode.

استمتع بـ RCE فقط :))

المراجع

  • SMBleedingGhost Writeup: Chaining SMBleed (CVE-2020-1206) with SMBGhost
  • SMBleedingGhost Writeup Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE
  • SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE
  • Exploiting SMBGhost (CVE-2020-0796) for a Local Privilege Escalation: Writeup + POC
  • lznt1.py
  • TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL – LEVERAING WRITEWHAT-WHERE VULNERABILITIES IN CREATORS UPDATE
  • VERGILIUS_MDL
  • Exploit Development: Leveraging Page Table Entries for Windows Kernel Exploitation
  • RtlUnicodeStringCopy function
  • UNICODE_STRING structure

DatntSec. Viettel Cyber Security.

تنزيل الأداة
← حجم التخصيص ↓المعالج المنطقي0x11000x21000x41000x81000x101000x201000x401000x801000x100100
المعالج 1📝📝📝📝📝📝📝📝📝
المعالج 2📝📝📝📝📝📝📝📝📝
...
المعالج n📝📝📝📝📝📝📝📝📝