
Prueba de concepto de exploit para CVE-2023-21768, una vulnerabilidad de escritura arbitraria en el kernel del controlador de función auxiliar de Windows (AFD.sys) que permite la escalada local de privilegios mediante el anillo de E/S.
Según la descripción detallada de CVE-2023-21768 publicada por Microsoft Security Response Center (MSRC), la vulnerabilidad existe en el Ancillary Function Driver (AFD), cuyo nombre de archivo en el sistema es afd.sys. El módulo AFD es el punto de entrada del kernel de WinSock API. En este análisis, lo usaremos para explotar la escalada de privilegios en Windows 11.
Descargue dos versiones de afd.sys desde Winbindex, una versión cercana anterior al parche y una versión posterior al parche. Luego use Bindiff para comparar estas dos versiones.

Comparando las dos versiones en general, vemos que solo una función tiene diferencias: AfdNotifyRemoveIoCompletion. Vea más detalles sobre las diferencias de esta función entre las dos versiones.

No hay muchas diferencias entre las dos versiones. En la versión posterior al parche, se agregan instrucciones assembly para establecer parámetros y llamar a la función ProbeForWrite. Según la documentación de Microsoft, esta función se usa para verificar si una dirección realmente pertenece al modo usuario, tiene permiso de escritura y está alineada correctamente. Analicemos más en detalle este código:
Pre-patch afd.sys version 10.0.22621.608

Post-patch afd.sys version 10.0.22621.1105

Ambas verifican el valor de r15_1, si es 0 escriben el valor de var_304 en el puntero indicado en un campo de struct_1. Si es distinto de 0, se llama a ProbeForWrite para asegurar que el puntero apunte a una dirección válida. En la versión pre-patch, luego se escribe el valor de var_304 en el puntero, esta verificación falta. A partir de este parche, podemos suponer que podemos llamar a este código con el valor de arg3_1->field_18 controlado. Si podemos establecer un valor de dirección del kernel en field_18, entonces podemos escribir var_304 en una dirección de memoria del kernel.
=> tipo de bug: escritura-Where arbitraria en el kernel
Ahora necesitamos encontrar cómo provocar el bug. La función AfdNotifyRemoveIoCompletion se llama directamente dentro de la función AfdNotifySock.

De manera similar, al buscar la referencia cruzada de AfdNotifySock, vemos que no se llama directamente desde ninguna otra función, pero la dirección de la función se almacena en una dirección en .rdata.

Esta dirección se encuentra justo antes de AfdIrpCallDispatch.

Para provocar el bug, llamaré a DeviceIoControl con IOCTL_AFD_NOTIFY_SOCK y se llamará a 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
);
Para cada driver, se crea un objeto DRIVER_OBJECT en el kernel, definido así:
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;
El último componente, MajorFunction, es un array que contiene las funciones de dispatch del driver para manejar la comunicación entre kernel y modo usuario. La función dispatch correspondiente a la llamada a DeviceIoControl se almacena en MajorFunction[IRP_MJ_DEVICE_CONTROL].
#define IRP_MJ_DEVICE_CONTROL 0x0e
Desde la función DriverEntry de afd.sys, podemos ver que el driver creó el objeto de dispositivo "\Device\Afd":

Asigna MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl, por lo que al llamar a DeviceIoControl para comunicarse con el kernel, se llamará a esta función.

En AFD hay dos tablas de dispatch: AfdIrpCallDispatch y AfdImmediateCallDispatch.

Se puede ver fácilmente que AfdDispatchDeviceIoControl calcula un subíndice mediante IoControlCode y obtiene el valor correspondiente al subíndice de AfdIoctlTable para verificarlo con IoControlCode.

Desde la distancia entre la dirección de inicio de AfdImmediateCallDispatch y la dirección donde se almacena AfdNotifySock, calculamos el índice como 73, con el código de control 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!

Como se dijo al principio, la vulnerabilidad ocurre cuando podemos pasar un puntero no validado a través de una estructura. Esta estructura se pasa directamente desde el modo usuario mediante lpInBuffer de DeviceIoControl. Luego se pasa a AfdNotifySock como el cuarto parámetro y se pasa a AfdNotifyRemoveIoCompletion como el tercer parámetro.