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

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

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

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

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2023-21768 — إثبات مفهوم لاستغلال CVE-2023-21768، وهي ثغرة كتابة عشوائية في نواة نظام التشغيل Windows عبر برنامج تشغيل الوظائف المساعدة (AFD.sys)، مما يتيح تصعيد الامتيازات محليًا عبر حلقة الإدخال/الإخراج (I/O ring). | Kitploit
أدوات/GitHubGitHub/h1bana/cve-2023-21768
تصعيد الامتيازاتتحليل الثغرات الأمنيةالاستغلالاستغلال الملفات الثنائية
GitHubh1bana/cve-2023-21768

CVE-2023-21768

إثبات مفهوم لاستغلال CVE-2023-21768، وهي ثغرة كتابة عشوائية في نواة نظام التشغيل Windows عبر برنامج تشغيل الوظائف المساعدة (AFD.sys)، مما يتيح تصعيد الامتيازات محليًا عبر حلقة الإدخال/الإخراج (I/O ring).

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

الأكثر شعبية

عرض الكل →

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

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

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

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

CVE-2023-21768

برنامج تشغيل الوظائف الإضافية لويندوز لـ WinSock

وفقًا للوصف التفصيلي لـ CVE-2023-21768 الذي نشرته Microsoft Security Response Center (MSRC)، توجد الثغرة في Ancillary Function Driver (AFD)، واسم ملفه في النظام هو afd.sys. وحدة AFD هي نقطة دخول النواة (kernel entry point) لـ WinSock API. في هذا التحليل سأستخدمها لاستغلال تصعيد الامتيازات على ويندوز 11.

تحليل الفروقات في التصحيح وسبب الجذر

قم بتحميل نسختين من afd.sys من Winbindex، أحدث نسخة قبل التصحيح، ونسخة بعد التصحيح. ثم استخدم Bindiff لمقارنة هاتين النسختين. bindiff

بمقارنة النسختين بشكل عام، نرى أن هناك دالة واحدة فقط مختلفة وهي AfdNotifyRemoveIoCompletion. دعونا نلقي نظرة أكثر تفصيلًا على الاختلافات في هذه الدالة بين النسختين. bindiff

لا توجد اختلافات كثيرة بين النسختين. في الإصدار بعد التصحيح (post-patch)، تمت إضافة تعليمات assembly لتعيين المعاملات واستدعاء دالة ProbeForWrite. وفقًا لـ الوثيقة الخاصة بشركة Microsoft، تُستخدم هذه الدالة للتحقق مما إذا كان العنوان ينتمي حقًا إلى وضع المستخدم (user-mode)، ولديه صلاحية الكتابة، ومحاذي بشكل صحيح أم لا. تحليل أكثر تفصيلًا لهذا الكود:

  • ما قبل التصحيح afd.sys version 10.0.22621.608 code1

  • ما بعد التصحيح afd.sys version 10.0.22621.1105 code2

كلاهما يتحقق من قيمة r15_1، إذا كانت تساوي 0 فسيتم كتابة قيمة var_304 إلى المؤشر المحدد في أحد حقول struct_1. إذا كانت مختلفة عن 0، فسيتم استدعاء ProbeForWrite للتأكد من أن المؤشر يشير إلى عنوان صالح. في إصدار pre-patch يتم بعد ذلك كتابة القيمة عند var_304 إلى المؤشر، وهذا الفحص مفقود. من هذا التصحيح، يمكننا تخمين أنه يمكننا استدعاء هذا الكود بقيمة arg3_1->field_18 التي نتحكم فيها. إذا تمكنا من تعيين قيمة عنوان kernel في field_18، فيمكننا كتابة var_304 إلى عنوان في ذاكرة kernel.

=> bug type: arbitrary kernel Write-Where

الآن نحتاج إلى إيجاد طريقة لتفعيل (trigger) الثغرة. يتم استدعاء الدالة AfdNotifyRemoveIoCompletion مباشرة داخل الدالة AfdNotifySock. crossRef

وبالمثل، عند البحث عن cross reference للدالة AfdNotifySock نجد أنها لا تُستدعى مباشرة من أي دالة أخرى، ولكن عنوان الدالة يُحفظ في عنوان داخل .rdata cross2

هذا العنوان يقع مباشرة قبل AfdIrpCallDispatch. cross3

لتفعيل الثغرة، سأستدعي DeviceIoControl مع IOCTL_AFD_NOTIFY_SOCK وسيتم استدعاء AfdNotifySock.

root@kitploit:~
BOOL DeviceIoControl(
  [in]                HANDLE       hDevice,
  [in]                DWORD        dwIoControlCode,
  [in, optional]      LPVOID       lpInBuffer,
  [in]                DWORD        nInBufferSize,
  [out, optional]     LPVOID       lpOutBuffer,
  [in]                DWORD        nOutBufferSize,
  [out, optional]     LPDWORD      lpBytesReturned,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);

التحليل العكسي والتصحيح

لكل برنامج تشغيل (driver)، يتم إنشاء كائن DRIVER_OBJECT في النواة، وهو معرّف على النحو التالي:

root@kitploit:~
typedef struct _DRIVER_OBJECT {
  CSHORT             Type;
  CSHORT             Size;
  PDEVICE_OBJECT     DeviceObject;
  ULONG              Flags;
  PVOID              DriverStart;
  ULONG              DriverSize;
  PVOID              DriverSection;
  PDRIVER_EXTENSION  DriverExtension;
  UNICODE_STRING     DriverName;
  PUNICODE_STRING    HardwareDatabase;
  PFAST_IO_DISPATCH  FastIoDispatch;
  PDRIVER_INITIALIZE DriverInit;
  PDRIVER_STARTIO    DriverStartIo;
  PDRIVER_UNLOAD     DriverUnload;
  PDRIVER_DISPATCH   MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;

المكوّن الأخير MajorFunction هو مصفوفة تحتوي على دوال dispatch الخاصة ببرنامج التشغيل لمعالجة الاتصالات بين النواة ووضع المستخدم. دالة dispatch المقابلة لاستدعاء DeviceIoControl تُحفظ في MajorFunction[IRP_MJ_DEVICE_CONTROL].

root@kitploit:~
#define IRP_MJ_DEVICE_CONTROL           0x0e

من خلال دالة DriverEntry الخاصة بـ afd.sys، يمكننا أن نرى أن برنامج التشغيل أنشأ device object "\Device\Afd": code3

يتم تعيين MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl، لذلك عند استدعاء DeviceIoControl للتواصل مع النواة، سيتم استدعاء هذه الدالة. code4

يوجد في AFD جدولان للتوزيع (dispatch table) وهما AfdIrpCallDispatch و AfdImmediateCallDispatch. dispatchtable1 dispatchtable2

يمكننا بسهولة ملاحظة أن AfdDispatchDeviceIoControl يحسب subscript من خلال IoControlCode ويأخذ القيمة المقابلة للـ subscript من AfdIoctlTable للتحقق منها باستخدام IoControlCode. 1

من المسافة بين عنوان بداية AfdImmediateCallDispatch والعنوان الذي يُخزّن AfdNotifySock، نحسب أن الـ index هو 73، ورمز التحكم (control code) هو 0x12127 ioctl

root@kitploit:~
int main() {
    WSADATA WSAData;
    SOCKET s;
    SOCKADDR_IN sa;
    int ierr;

    WSAStartup(0x2, &WSAData);
    s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    memset(&sa, 0, sizeof(sa));
    sa.sin_port = htons(135);
    sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    sa.sin_family = AF_INET;
    ierr = connect(s, (const struct sockaddr*)&sa, sizeof(sa));

    char outBuf[100];
    DWORD bytesRet;
    DWORD inbuf1[100];

    memset(inbuf1, 0, sizeof(inbuf1));

    DeviceIoControl((HANDLE)s, 0x12127, (LPVOID)inbuf1, 0x30, outBuf, 0, &bytesRet, NULL);
    return 0;
}

لقد نجح الأمر!

bp1

كما ذكرنا منذ البداية، تحدث الثغرة عندما نتمكن من تمرير مؤشر غير مُتحقق منه (unvalidated pointer) عبر struct. يتم تمرير هذا الـ struct مباشرة من وضع المستخدم عبر lpInBuffer الخاص بـ DeviceIoControl. ثم يُمرَّر إلى AfdNotifySock كمعامل رابع ويُمرَّر إلى AfdNotifyRemoveIoCompletion كمعامل ثالث.

para1 para2 para3

نظرًا لأننا لا نعرف ما يتضمنه الـ struct، تركت IDA ينشئ الـ struct تلقائيًا. الآن نحتاج إلى إيجاد طريقة لتمرير البيانات إلى هذا الـ struct وتجاوز الفحوصات اللازمة للوصول إلى الكود المعيب. نبدأ من الدالة AfdNotifySock:

check1

أولاً، يجب أن يكون حجم الـ struct مساويًا 0x30 بايت.

check2

القيم التالية يجب أن تكون غير صفرية:

check3

شيء آخر لاحظته أثناء التصحيح هو أنه كان يقفز إلى الفشل عند فحص الـ UserBuffer السابق، لذلك عند استدعاء DeviceIoControl يجب تعيين هذه القيمة إلى NULL. بعد تعيين القيم أعلاه، تمكّنت من تجاوز الفحص check2.

debug1 debug2

الفحص التالي الذي يجب تجاوزه:

check4

يجب أن تُرجع ObReferenceObjectByHandle القيمة STATUS_SUCCESS حتى يتسنى تجاوز هذا الفحص. أي أنه يجب علينا تمرير handle صالح. بحثت ولم أجد أي مكان يشرح كيفية إنشاء IoCompletionObjectType. لذلك اتبعت مقال التحليل https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/. استخدام الدالة NtCreateIoCompletion لإنشاء IoCompletionObjectType وتمرير الـ handle الخاص بها إلى ObReferenceObjectByHandle. بعد تجاوز هذا الفحص، يقفز تدفق البرنامج إلى حلقة، ولا يوجد في هذه الحلقة أي شيء ينقل إلى مسار الفشل، لذلك قمت ببساطة بتعيين القيمة عند dword20 إلى 0x1 للخروج من الحلقة.

check5

بعد الخروج من الحلقة، سيستدعي البرنامج AfdNotifyRemoveIoCompletion. نواصل التحليل مع الدالة AfdNotifyRemoveIoCompletion:

check6

أولاً، يفحص البرنامج حقلاً آخر في الـ struct، ويجب أن يكون غير صفري. ثم يُضرب في 0x20، ويُستخدم كمعامل لاستدعاء الدالة ProbeForWrite مع حقل آخر في الـ struct. هنا يكفي استخدام عنوان في ذاكرة وضع المستخدم بصلاحية كتابة و dwLen = 1. الفحص الأخير قبل أن نتمكن من تفعيل الثغرة هو أن القيمة المرجعة من استدعاء الدالة IoRemoveCompletion يجب أن تكون STATUS_SUCCESS. بعد البحث، اكتشفت أن الدالة NtRemoveIoCompletion عند استدعائها تستدعي الدالة IoRemoveCompletion. وفقًا لهذه الوثيقة، تعمل الدالة NtRemoveIoCompletion كـ "استدعاء انتظار" (waiting call) وتنتهي عندما يكتمل سجل واحد على الأقل في Io Completion Object محدد. يُضاف السجل عندما تكتمل عملية الإدخال/الإخراج (I/O).

root@kitploit:~
NtRemoveIoCompletion(
  IN HANDLE               IoCompletionHandle,
  OUT PULONG              CompletionKey,
  OUT PULONG              CompletionValue,
  OUT PIO_STATUS_BLOCK    IoStatusBlock,
  IN PLARGE_INTEGER       Timeout OPTIONAL );

بالإضافة إلى ذلك، هناك معامل اختياري آخر وهو Timeout، وعند الوصول إلى قيمة المهلة تنتهي الدالة. ومع ذلك، فإن تعيين timeout = 0 فقط لا يكفي لجعل الدالة تعود، بل ستعيد رمز خطأ المهلة (timeout error code). يمكننا استخدام الدالة NtSetIoCompletion لزيادة عداد عمليات الإدخال/الإخراج المعلقة في IoCompletionObjectType بمقدار 1 وإنهاء الدالة NtRemoveIoCompletion قبل المهلة. بعد عدة محاولات، لاحظت أن القيمة المكتوبة تكون دائمًا 0x1.

الاستغلال - تصعيد الامتيازات المحلي (LPE) باستخدام IORING

نظرًا لأننا نستطيع كتابة القيمة 0x1 إلى عنوان في وضع النواة، يمكننا استخدام هذه الثغرة للحصول على قدرة كاملة للقراءة/الكتابة في أي عنوان من خلال الاستفادة من I/O ring (آلية إدخال/إخراج جديدة أطلقتها Microsoft). كتب Yarden Shafir تحليلًا مفصلاً للغاية حول هذه الطريقة، يمكنك قراءته هنا. إحدى العمليات التي يمكن للتطبيق تنفيذها هي تخصيص جميع المخازن المؤقتة (buffers) لعمليات الإدخال/الإخراج المستقبلية، ثم تسجيلها مع I/O ring. تتم الإشارة إلى المخازن المسجلة مسبقًا عبر I/O object:

root@kitploit:~
typedef struct _IORING_OBJECT
{
    USHORT Type;
    USHORT Size;
    NT_IORING_INFO UserInfo;
    PVOID Section;
    PNT_IORING_SUBMISSION_QUEUE SubmissionQueue;
    PMDL CompletionQueueMdl;
    PNT_IORING_COMPLETION_QUEUE CompletionQueue;
    ULONG64 ViewSize;
    ULONG InSubmit;
    ULONG64 CompletionLock;
    ULONG64 SubmitCount;
    ULONG64 CompletionCount;
    ULONG64 CompletionWaitUntil;
    KEVENT CompletionEvent;
    UCHAR SignalCompletionEvent;
    PKEVENT CompletionUserEvent;
    ULONG RegBuffersCount;
    PVOID RegBuffers;
    ULONG RegFilesCount;
    PVOID* RegFiles;
} IORING_OBJECT, *PIORING_OBJECT;

إذا كانت ثغرة أمنية، مثل الثغرة المذكورة في هذه المقالة، تسمح لك بتحديث/تعديل الحقلين RegBuffersCount و RegBuffers، فيمكن استخدام API I/O ring القياسي لقراءة وكتابة ذاكرة النواة. ومع ذلك، فإن استخدام الدالة NtQuerySystemInformation يتطلب صلاحية Medium IL. من أجل تصعيد الامتيازات (LPE) من Low IL، نحتاج إلى طريقة ما لتسريب (leak) عنوان kernel.

بعد أن يشير IoRing->RegBuffers إلى fakeBuffer، الذي يتحكم فيه المستخدم، يمكننا استخدام عمليات I/O ring العادية للقراءة والكتابة في أي عنوان نريده عن طريق تحديد index يشير إلى الـ fake لاستخدامه كمخزن مؤقت:

  • عملية القراءة + عنوان kernel: ستقوم النواة "بالقراءة" من ملف نختاره إلى عنوان kernel المحدد، مما يؤدي إلى كتابة عشوائية.
  • عملية الكتابة + عنوان kernel: ستقوم النواة "بكتابة" البيانات الموجودة في العنوان المحدد إلى ملف نختاره، مما يؤدي إلى قراءة عشوائية.

لمزيد من الفهم، يمكنك قراءة تحليل Yarden Shafir في الرابط أعلاه.

المشكلة

بعد محاولة إنشاء كائن IO Ring والكتابة باستخدام كود الإثبات (POC) أعلاه، تعطل ويندوز بعد استدعاء DeviceIOControl /_ \ لذلك استخدمت طريقة الاستدعاء المباشر لدوال Ntfunction (˘・_・˘)

النطاق المتأثر

  • ويندوز 11 21H1/22H2 قبل إصدار النظام (OS build) 22000.1455/22621.1105
  • ويندوز سيرفر 2022 قبل إصدار النظام (OS build) 20348.1487

التصحيح

  • أضاف التصحيح كودًا يستدعي ProbeForWrite
  • إصدارات التصحيح:
    • ويندوز 11 21H1: KB5022287 (OS Build 22000.1455)
    • ويندوز 11 22H2: KB5022303 (OS Build 22621.1105)
    • ويندوز سيرفر 2022: KB5022291 (OS Build 20348.1487)

POC

تنزيل الأداة