
Эксплойт с доказательством концепции для CVE-2023-21768, уязвимость произвольной записи в ядро драйвера вспомогательных функций Windows (AFD.sys), позволяющая локальное повышение привилегий через кольцо ввода-вывода.
Согласно подробному описанию CVE-2023-21768, опубликованному Microsoft Security Response Center (MSRC), уязвимость существует в Ancillary Function Driver (AFD), файл которого в системе называется afd.sys. AFD модуль является kernel entry point для WinSock API. В этом анализе я буду использовать его для повышения привилегий в Windows 11.
Скачиваем две версии afd.sys из Winbindex: одну – самую последнюю до исправления, и одну – после исправления. Затем используем Bindiff для сравнения этих версий.

Сравнивая две версии в целом, видим, что только одна функция имеет различия – AfdNotifyRemoveIoCompletion. Посмотрим подробнее на различия этой функции между двумя версиями.

Различий не так много. В версии после исправления добавлены инструкции ассемблера для установки параметров и вызова функции 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, чтобы убедиться, что указатель указывает на корректный адрес. В версии до исправления затем запись значения var_304 по указателю происходит без проверки. Из этого исправления можно предположить, что мы можем вызвать этот код с контролируемым значением arg3_1->field_18. Если можно установить адрес ядра в field_18, то мы сможем записать var_304 в область памяти ядра.
=> bug type: arbitrary kernel Write-Where
Теперь нужно найти способ вызвать баг. Функция AfdNotifyRemoveIoCompletion вызывается непосредственно в функции AfdNotifySock.

Аналогично, ищем перекрёстные ссылки на 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_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 – это массив функций диспетчеризации драйвера для обработки взаимодействий между ядром и user-mode. Функция диспетчеризации, соответствующая вызову DeviceIoControl, хранится в MajorFunction[IRP_MJ_DEVICE_CONTROL].
#define IRP_MJ_DEVICE_CONTROL 0x0e
Из функции DriverEntry afd.sys видно, что драйвер создаёт объект устройства "\Device\Afd":

Присваивает MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl, поэтому при вызове DeviceIoControl для взаимодействия с ядром будет вызвана эта функция.

В AFD есть две диспетчерские таблицы: AfdIrpCallDispatch и AfdImmediateCallDispatch.

Легко заметить, что AfdDispatchDeviceIoControl вычисляет индекс по IoControlCode и берёт соответствующее значение из AfdIoctlTable для проверки по IoControlCode.

По расстоянию от начального адреса AfdImmediateCallDispatch до адреса хранения AfdNotifySock вычисляем индекс 73, что даёт управляющий код 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;
}
it works!

Как было сказано в начале, уязвимость возникает, когда мы можем передать непроверенный указатель через структуру. Эта структура передаётся непосредственно из user-mode через lpInBuffer в DeviceIoControl. Затем она передаётся в AfdNotifySock как четвёртый параметр и в AfdNotifyRemoveIoCompletion как третий параметр.
