Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2023-21768 — 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. | Kitploit
Ferramentas/GitHubGitHub/h1bana/cve-2023-21768
Escalada de PrivilégiosAnálise de VulnerabilidadesExploraçãoExploração de Binários
GitHubh1bana/cve-2023-21768

CVE-2023-21768

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.

Ver Repositório
3há 3 anosAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2023-21768

Driver de Função Auxiliar do Windows para WinSock

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.

Análise de Diff de Patch e Causa Raiz

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. bindiff

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. bindiff

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 code1

  • pós-patch afd.sys version 10.0.22621.1105 code2

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. crossRef

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 cross2

Este endereço está imediatamente antes de AfdIrpCallDispatch. cross3

Para acionar o bug, chamarei DeviceIoControl com IOCTL_AFD_NOTIFY_SOCK e AfdNotifySock será chamado.

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
);

engenharia reversa e depuração

Para cada driver, um objeto DRIVER_OBJECT é criado no kernel, definido da seguinte forma:

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;

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].

root@kitploit:~
#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": code3

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

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

É 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. 1

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 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;
}

funciona!

bp1

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.

para1 para2 para3

Como ainda não sabemos o que a struct contém, deixei o IDA criar a struct automaticamente. Agora precisamos encontrar uma maneira de passar dados para essa struct e contornar as verificações necessárias para alcançar o código com falha. Começando pela função AfdNotifySock:

check1

Primeiro, o tamanho da struct deve ser igual a 0x30 bytes.

check2

Os valores precisam ser diferentes de zero:

check3

Outra coisa: ao depurar, vi que ele pulava para falha na verificação do UserBuffer anterior, então ao chamar DeviceIoControl, esse valor deve ser definido como NULL. Após definir os valores acima, consegui passar pela verificação 2.

debug1 debug2

Próxima verificação a ser contornada:

check4

ObReferenceObjectByHandle deve retornar STATUS_SUCCESS para passar por essa verificação. Ou seja, preciso passar um handle válido. Tentei pesquisar, mas não encontrei onde falava sobre como criar IoCompletionObjectType. Então segui a análise de https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/. Usei a função NtCreateIoCompletion para criar um IoCompletionObjectType e passar para ObReferenceObjectByHandle o handle dele. Após contornar essa verificação, o fluxo do programa entra em um loop. Nesse loop, não há nenhum ponto que faça o fluxo ir para falha, então simplesmente defini o valor em dword20 como 0x1 para sair do loop.

check5

Depois de sair do loop, o programa chama AfdNotifyRemoveIoCompletion. Continuando a análise da função AfdNotifyRemoveIoCompletion:

check6

Primeiro, o programa verifica outro campo da struct; ele deve ser diferente de zero. Em seguida, é multiplicado por 0x20 e usado como parâmetro para chamar a função ProbeForWrite junto com outro campo da struct. Aqui, basta usar um endereço pertencente à memória do modo de usuário com permissão de escrita e dwLen = 1. A última verificação antes de podermos acionar o bug é que o valor retornado pela chamada da função IoRemoveCompletion deve ser STATUS_SUCCESS. Após pesquisar, descobri que a função NtRemoveIoCompletion, quando chamada, invoca a função IoRemoveCompletion. De acordo com esta documentação, a função NtRemoveIoCompletion funciona como uma "chamada de espera" e termina quando pelo menos um registro de conclusão é adicionado em um Io Completion Object especificado. O registro é adicionado quando uma operação de E/S é concluída.

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

Além disso, há um parâmetro opcional Timeout; quando o tempo limite é atingido, a função termina. No entanto, definir apenas timeout = 0 não é suficiente para a função retornar; ela retornará um código de erro de tempo limite. Podemos usar a função NtSetIoCompletion para incrementar o contador de E/S pendentes no IoCompletionObjectType em 1 e encerrar a função NtRemoveIoCompletion antes do tempo limite. Após várias tentativas, vi que o valor escrito é sempre 0x1.

exploit - LPE com IORING

Com a capacidade de escrever o valor 0x1 em um endereço no modo kernel, podemos usar esse bug para obter capacidades completas de leitura/escrita arbitrária de endereços, aproveitando o anel de E/S (um novo mecanismo de E/S introduzido pela Microsoft). Yarden Shafir escreveu uma análise muito detalhada sobre isso, que você pode ler aqui. Uma das operações que um aplicativo pode realizar é alocar todos os buffers para operações futuras de E/S e, em seguida, registrá-los com o anel de E/S. Os buffers pré-registrados são referenciados por meio do objeto I/O:

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;

Se uma vulnerabilidade de segurança, como a mencionada neste artigo, permitir que você atualize/modifique os campos RegBuffersCount e RegBuffers, então é possível usar a API padrão do anel de E/S para ler e escrever no kernel. No entanto, usar a função NtQuerySystemInformation requer privilégio Medium IL. Para LPE a partir de Low IL, é necessário alguma forma de vazar um endereço do kernel.

Depois que IoRing->RegBuffers aponta para um fakeBuffer controlado pelo usuário, podemos usar operações normais do anel de E/S para criar leitura e escrita em qualquer endereço desejado, especificando um índice no fake para usar como buffer:

  • Operação de leitura + endereço do kernel: o kernel "lerá" de um arquivo que escolhemos para o endereço do kernel especificado, resultando em escrita arbitrária.
  • Operação de escrita + endereço do kernel: o kernel "escreverá" dados do endereço especificado em um arquivo que escolhemos, resultando em leitura arbitrária.

Para entender melhor, você pode ler a análise de Yarden Shafir no link acima.

problema

Após tentar criar um objeto IO Ring e escrever usando o código POC acima, o Windows travou após chamar DeviceIOControl /_ \ então usei a abordagem de chamar diretamente as funções Nt (˘・_・˘)

Faixa de Afetação

  • Windows 11 21H1/22H2 anteriores ao build do SO 22000.1455/22621.1105
  • Windows Server 2022 anteriores ao build do SO 20348.1487

O Patch

  • O patch adicionou o código que chama ProbeForWrite
  • Versões do patch:
    • Windows 11 21H1: KB5022287 (OS Build 22000.1455)
    • Windows 11 22H2: KB5022303 (OS Build 22621.1105)
    • Windows Server 2022: KB5022291 (OS Build 20348.1487)

POC

Baixar ferramenta