
Exploit de prova de conceito para CVE-2023-21768, uma vulnerabilidade de escrita arbitrária no kernel do Driver de Função Auxiliar do Windows (AFD.sys) que possibilita escalonamento de privilégio local via I/O ring.
De acordo com a descrição detalhada da CVE-2023-21768 publicada pelo Microsoft Security Response Center (MSRC), a vulnerabilidade existe no Ancillary Function Driver (AFD), cujo nome de arquivo no sistema é afd.sys. O módulo AFD é o ponto de entrada no kernel da WinSock API. Nesta análise, usarei-a para explorar a elevação de privilégio no Windows 11.
Baixe duas versões do afd.sys do Winbindex, uma versão mais recente antes do patch e uma versão após o patch. Em seguida, use o Bindiff para comparar essas duas versões.

Comparando as duas versões de forma geral, vemos que apenas uma função tem diferença: AfdNotifyRemoveIoCompletion. Veja mais detalhes sobre as diferenças desta função entre as duas versões.

Não há muitas diferenças entre as duas versões. Na versão pós-patch, há instruções assembly adicionais para definir parâmetros e chamar a função ProbeForWrite. De acordo com a documentação da Microsoft, esta função é usada para verificar se um endereço realmente pertence ao modo de usuário, tem permissão de escrita e está alinhado corretamente. Análise mais detalhada deste código:
pré-patch afd.sys version 10.0.22621.608

pós-patch afd.sys version 10.0.22621.1105

Ambas verificam o valor de r15_1; se for 0, escrevem o valor de var_304 no ponteiro especificado em um campo de struct_1. Se for diferente de 0, ProbeForWrite é chamado para garantir que o ponteiro aponte para um endereço válido. Na versão pré-patch, o valor de var_304 é escrito no ponteiro após essa verificação, que está ausente. A partir deste patch, podemos inferir que podemos chamar este código com o valor de arg3_1->field_18 sob controle. Se for possível definir um valor de endereço de kernel em field_18, podemos escrever var_304 em um endereço de memória do kernel.
=> tipo de bug: escrita arbitrária no kernel (Write-Where)
Agora precisamos encontrar uma forma de acionar o bug. A função AfdNotifyRemoveIoCompletion é chamada diretamente na função AfdNotifySock.

Da mesma forma, ao procurar a referência cruzada de AfdNotifySock, vemos que ela não é chamada diretamente por nenhuma outra função, mas o endereço da função está armazenado em um endereço no .rdata

Este endereço está imediatamente antes de AfdIrpCallDispatch.

Para acionar o bug, chamarei DeviceIoControl com IOCTL_AFD_NOTIFY_SOCK e AfdNotifySock será chamado.
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, um objeto DRIVER_OBJECT é criado no kernel, definido da seguinte forma:
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;
O último componente MajorFunction é uma matriz contendo as funções de despacho do driver para lidar com a comunicação entre kernel e modo de usuário. A função de despacho correspondente à chamada DeviceIoControl é armazenada em MajorFunction[IRP_MJ_DEVICE_CONTROL].
#define IRP_MJ_DEVICE_CONTROL 0x0e
A partir da função DriverEntry do afd.sys, podemos ver que o driver criou o objeto de dispositivo "\Device\Afd":

Atribui MajorFunction[IRP_MJ_DEVICE_CONTROL] = AfdDispatchDeviceControl, então ao chamar DeviceIoControl para se comunicar com o kernel, essa função será chamada.

No AFD, existem duas tabelas de despacho: AfdIrpCallDispatch e AfdImmediateCallDispatch.

É fácil ver que AfdDispatchDeviceIoControl calcula um índice a partir do IoControlCode e obtém o valor correspondente ao índice da tabela AfdIoctlTable para validar com o IoControlCode.

A partir da distância entre o endereço inicial de AfdImmediateCallDispatch e o endereço onde AfdNotifySock está armazenado, calculamos que o índice é 73, com código de controle 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;
}
funciona!

Como dito no início, a vulnerabilidade ocorre quando podemos passar um ponteiro não validado por meio de uma struct. Essa struct é passada diretamente do modo de usuário através de lpInBuffer do DeviceIoControl. Em seguida, é passada para AfdNotifySock como o quarto parâmetro e para AfdNotifyRemoveIoCompletion como o terceiro parâmetro.