Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2023-21768 — 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. | Kitploit
Herramientas/GitHubGitHub/h1bana/cve-2023-21768
Escalada de PrivilegiosAnálisis de VulnerabilidadesExplotaciónExplotación de Binarios
GitHubh1bana/cve-2023-21768

CVE-2023-21768

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.

Ver Repositorio
3hace 3 añosAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

CVE-2023-21768

Windows Ancillary Function Driver for WinSock

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.

Patch Diff and Root Cause Analysis

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

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

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 code1

  • Post-patch afd.sys version 10.0.22621.1105 code2

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

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

Esta dirección se encuentra justo antes de AfdIrpCallDispatch. cross3

Para provocar el bug, llamaré a DeviceIoControl con IOCTL_AFD_NOTIFY_SOCK y se llamará a AfdNotifySock.

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

reverse and debug

Para cada driver, se crea un objeto DRIVER_OBJECT en el kernel, definido así:

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;

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

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

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

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

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

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

it works!

bp1

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.

para1 para2 para3

Como aún no sé qué contiene la estructura, dejo que IDA cree la estructura automáticamente. Ahora necesito encontrar cómo pasar datos a esta estructura y omitir las verificaciones necesarias para llegar al código con el error. Empezando desde la función AfdNotifySock:

check1

Primero, el tamaño de la estructura debe ser de 0x30 bytes.

check2

Los valores deben ser diferentes de 0:

check3

Otra cosa es que al depurar vi que saltaba a fallo en la verificación de UserBuffer anterior, por lo que al llamar a DeviceIoControl este valor se establece en NULL. Después de establecer los valores anteriores, pasé la verificación check2.

debug1 debug2

Siguiente verificación a omitir:

check4

ObReferenceObjectByHandle debe devolver STATUS_SUCCESS para pasar esta verificación. Es decir, debo pasar un handle válido. Buscando, no encontré ninguna parte que hable sobre cómo crear IoCompletionObjectType. Así que seguí el análisis de https://securityintelligence.com/posts/patch-tuesday-exploit-wednesday-pwning-windows-ancillary-function-driver-winsock/. Usando la función NtCreateIoCompletion para crear un IoCompletionObjectType y pasar su handle a ObReferenceObjectByHandle. Después de omitir esta verificación, el flujo del programa entra en un bucle, en el cual no hay ninguna parte que cause un fallo, así que simplemente establezco el valor en dword20 como 0x1 para salir del bucle.

check5

Después de salir del bucle, el programa llama a AfdNotifyRemoveIoCompletion. Continuamos analizando la función AfdNotifyRemoveIoCompletion:

check6

Primero, el programa verifica otro campo de la estructura, debe ser diferente de 0. Luego se multiplica por 0x20, y se usa como parámetro para llamar a ProbeForWrite junto con otro campo de la estructura. Aquí solo necesito usar una dirección en la memoria de modo usuario con permiso de escritura y dwLen = 1. La última verificación antes de poder provocar el error es que el valor de retorno de la llamada a la función IoRemoveCompletion debe ser STATUS_SUCCESS. Después de buscar, supe que la función NtRemoveIoCompletion, al ser llamada, invoca a IoRemoveCompletion. Según esta documentación, la función NtRemoveIoCompletion actúa como una "llamada de espera" y finaliza cuando hay al menos un registro completado en un objeto Io Completion especificado. El registro se agrega cuando se completa el proceso de E/S.

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

Además, hay otro parámetro opcional, Timeout, cuando se alcanza el valor de tiempo de espera, la función finaliza. Sin embargo, solo establecer timeout = 0 no es suficiente para que la función retorne, sino que devuelve un código de error de tiempo de espera. Podemos usar la función NtSetIoCompletion para incrementar el contador de E/S pendientes en IoCompletionObjectType en 1 y finalizar la función NtRemoveIoCompletion antes del tiempo de espera. Después de varios intentos, vi que el valor escrito siempre es 0x1.

exploit - LPE with IORING

Con la capacidad de escribir el valor 0x1 en una dirección del kernel, podemos usar este error para tener capacidad completa de lectura/escritura arbitraria de direcciones aprovechando I/O ring (un nuevo mecanismo de E/S lanzado por Microsoft). Yarden Shafir escribió un análisis muy detallado sobre este método, puedes leerlo aquí. Una de las operaciones que una aplicación puede realizar es asignar todos los búferes para sus futuras operaciones de E/S y luego registrarlos con I/O ring. Los búferes previamente registrados se referencian a través del 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;

Si una vulnerabilidad, como la mencionada en este artículo, permite actualizar/modificar los campos RegBuffersCount y RegBuffers, entonces se puede usar la API estándar de I/O ring para leer y escribir en el kernel. Sin embargo, el uso de la función NtQuerySystemInformation requiere un privilegio Medium IL. Para LPE desde Low IL, se necesita alguna forma de filtrar una dirección del kernel.

Después de que IoRing->RegBuffers apunte a un fakeBuffer controlado por el usuario, podemos usar las operaciones normales de I/O ring para crear lecturas y escrituras en cualquier dirección que deseemos especificando un índice en el fake para usarlo como buffer:

  • Operación de lectura + dirección del kernel: el kernel "leerá" desde un archivo que elijamos en la dirección del kernel especificada, resultando en una escritura arbitraria.
  • Operación de escritura + dirección del kernel: el kernel "escribirá" datos en la dirección especificada en un archivo que elijamos, resultando en una lectura arbitraria.

Para entender mejor, puedes leer el análisis de Yarden Shafir en el enlace anterior.

issue

Después de intentar crear un objeto IO Ring y escribir usando el código POC anterior, Windows se bloqueó después de llamar a DeviceIOControl /_ \ así que usé el método de llamar directamente a las funciones Ntfunction (˘・_・˘)

Affect range

  • Windows 11 21H1/22H2 anteriores a os build 22000.1455/22621.1105
  • Windows Server 2022 anteriores a os build 20348.1487

The patch

  • El parche agregó el código que llama a ProbeForWrite
  • Versiones del parche:
    • 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

Descargar herramienta