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

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

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

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

كلاهما يتحقق من قيمة 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.

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

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

لتفعيل الثغرة، سأستدعي DeviceIoControl مع IOCTL_AFD_NOTIFY_SOCK وسيتم استدعاء AfdNotifySock.
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 في النواة، وهو معرّف على النحو التالي:
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].
#define IRP_MJ_DEVICE_CONTROL 0x0e
من خلال دالة DriverEntry الخاصة بـ afd.sys، يمكننا أن نرى أن برنامج التشغيل أنشأ device object "\Device\Afd":

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

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

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

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

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;
}
لقد نجح الأمر!

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

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