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-2015-0057 — Traducción del artículo, explotación de la vulnerabilidad CVE-2015-0057 en sistemas de 32 bits y 64 bits. Exploiting the win32k!xxxEnableWndSBArrows use-after-free (CVE 2015-0057) bug on both 32-bit and 64-bit(Aaron Adams of NCC ) | Kitploit
Herramientas/GitHubGitHub/highandhigh/cve-2015-0057
Forensia de MemoriaAnálisis de VulnerabilidadesExplotaciónIngeniería InversaPapers e InvestigaciónAprendizaje y EducaciónExplotación de Binarios
GitHubhighandhigh/cve-2015-0057

CVE-2015-0057

Traducción del artículo, explotación de la vulnerabilidad CVE-2015-0057 en sistemas de 32 bits y 64 bits. Exploiting the win32k!xxxEnableWndSBArrows use-after-free (CVE 2015-0057) bug on both 32-bit and 64-bit(Aaron Adams of NCC )

Ver Repositorio
8hace 9 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-2015-0057 Explotación en sistemas de 32 y 64 bits

Autor: Aaron Adams

Traducción: 55-AA

Nota del traductor: Algunas partes de este artículo se han traducido libremente; consulte el original si tiene dudas.

Terminología:

  • Primitiva de operación: Similar a una función, es un conjunto de operaciones de corrupción para completar una funcionalidad completa y reutilizable, como leer memoria, escribir datos arbitrarios, etc.

Prefacio

A principios de este año, me topé con una interesante vulnerabilidad en win32k.sys (CVE-2015-0057) y logré una explotación estable en sistemas de 32 y 64 bits, que abarca desde XP hasta Windows 8.1 (con algunas excepciones). Este artículo describe en detalle cómo logré la explotación en ambas plataformas, y al final incluyo algunas cosas adicionales. También se describe cómo explotar con permisos de baja integridad en Windows 8.1 con SMEP habilitado.

Este artículo es largo; me esfuerzo por proporcionar tantos detalles como sea posible para mostrar la complejidad de explotar esta vulnerabilidad, en lugar de ocultarlos, aunque también omito algunos. Espero que estos detalles sean útiles para todos.

Introducción

El 10 de febrero de 2015, Microsoft publicó los detalles de MS15-010. Este bug fue descubierto por Udi Yavo de enSilo. Udi dio un excelente análisis en el blog breaking malware: "one bit rule-bypassing windows 10 protections using single bit". Recomiendo leerlo detenidamente para comprender mejor el bug, aunque en este artículo daré todos los detalles posibles, especialmente sobre los obstáculos que deben superarse al desencadenar la vulnerabilidad. La explotación de este bug es muy interesante, y muchos detalles provienen del blog de Udi; a continuación su declaración:

Divulgación razonable: aunque este blog es técnico, no revelaremos ningún código ni detalles completos para evitar que cualquier experto técnico reproduzca la explotación.

Como recompensa adicional por explotar esta vulnerabilidad, obtuvimos una evolución de Pokémon: el Pokémon Técnico. Creo que debo darle crédito a Udi por descubrir el bug, proporcionar la información relevante en su blog y los detalles para explotarlo; todo eso fue muy útil.

Anteriormente, nunca había explotado una vulnerabilidad de win32k.sys, y no estaba familiarizado con las devoluciones de llamada en modo usuario ni con muchas de las API relacionadas, así que también agradezco a conocidos investigadores de seguridad por los recursos novedosos que han publicado en línea, como Skywing, Tarjei Mandt, Alex Ionescu y j00ru. Todas estas personas merecen reconocimiento por proporcionar tanta información técnica de forma abierta. El artículo que más utilicé fue el de Tarjei Mandt: Win32k.sys exploitation paper.

Mientras escribía esta explotación, un excelente ingeniero inverso implementó una explotación estable de CVE-2015-1701, y el código de ejemplo sobre devoluciones de llamada en modo usuario fue muy útil; gracias a ese autor.

Vale la pena señalar que mi análisis a continuación se realizó en Windows 7, porque parece ser la única versión donde todas las estructuras en win32k.sys tienen sus correspondientes símbolos. La mayoría de estos símbolos se pueden usar para estructuras en otras versiones de Win32k.sys. Por alguna razón, Microsoft eliminó estos símbolos a partir de Windows 8.

Finalmente, quiero decir que mi método de explotación es bastante complejo. Es muy posible que exista un método más fácil que no he descubierto. Me encantaría escuchar si alguien usa un método diferente. De cualquier manera, espero que todo esto sea útil para estudiar vulnerabilidades en win32k.sys.

El Bug

A continuación vemos el bug en el desensamblado de win32k!xxxEnableWndSBArrows, un bug bastante sutil:

Sin parche:

root@kitploit:~
.text:FFFFF97FFF1B157D mov r8d, r13d 
.text:FFFFF97FFF1B1580 mov rdx, r14 
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar    ; desencadena callback en modo usuario 
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
 [...] 
.text:FFFFF97FFF1B1519 mov eax, [rbx]           ; referencia al puntero tagSBINFO sin comprobación 
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh 
.text:FFFFF97FFF1B1520 xor eax, esi

En el código anterior, Win32K!xxxdrawscrollbar puede, en las circunstancias adecuadas, hacer una devolución de llamada al espacio de usuario; en el código de usuario, el puntero tagSBINFO podría ser liberado por el atacante, y al regresar al código anterior, la instrucción en 0xFFFFF97FFF1B1519 hará referencia a un puntero inválido.

Con parche:

root@kitploit:~
    .text:FFFFF97FFF1D69C3 xor r8d, r8d 
    .text:FFFFF97FFF1D69C6 mov rdx, rbp 
    .text:FFFFF97FFF1D69C9 call xxxDrawScrollBar        ; desencadena callback en modo usuario
    .text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h]          ; comprueba que el puntero tagSBINFO sea correcto 
 ---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; si es correcto, continúa el flujo original 
 |  .text:FFFFF97FFF1D69D7 mov rcx, rbp 
 |  .text:FFFFF97FFF1D69DA call _ReleaseDC 
 |  .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958     ; salta a la salida de la función 
 |->.text:FFFFF97FFF1D69E4 mov eax, [rbx]               ; usa el puntero tagSBINFO correcto con seguridad 
    .text:FFFFF97FFF1D69E6 xor eax, r14d

En la versión con parche, vemos que se comprueba la validez del puntero tagSBINFO antes de usarlo. La información de la estructura relacionada se proporcionará más adelante.

Fundamentos – Fase 1 de corrupción de memoria

Para implementar esta explotación, realizamos múltiples corrupciones. En una de ellas se desencadena la vulnerabilidad.

La causa técnica de este bug es un UAF (use-after-free) en el montón del escritorio. Al principio, esto me confundió, ya que no estaba familiarizado con el mecanismo de devolución de llamada en modo usuario de win32k.sys ni con su funcionamiento. Por lo tanto, pensé que era una condición de carrera con bloqueos que provocaba el UAF. De hecho, los bloqueos de esa estructura se usan correctamente y el flujo es el esperado. En resumen, la verdadera razón del problema es:

  1. La función win32k!xxxEnableWndSBArrows tiene un puntero a un tagSBINFO en el montón del escritorio, que se lee de la estructura tagWND asociada a la ventana y describe una barra de desplazamiento.
  2. win32k!xxxEnableWndSBArrows llama a una función que provoca una devolución de llamada en modo usuario (esa función puede ser enganchada en modo usuario).
  3. Una vez que el código se ejecuta en el espacio de usuario, las estructuras del montón del escritorio pueden ser modificadas a través de otras llamadas al sistema de win32k, incluida la liberación de la estructura tagSBINFO del montón del escritorio.
  4. Al regresar al modo kernel, win32k!xxxEnableWndSBArrows no vuelve a leer el tagSBINFO desde tagWND ni comprueba si el puntero referenciado anteriormente sigue siendo válido, sino que usa directamente el puntero que ya fue liberado.

Eso es todo; sin considerar la devolución de llamada en modo usuario, esta fase es bastante clara.

Entendiendo cómo controlamos el flujo

Pero, ¿cómo realizamos la corrupción y por qué? Como se menciona en el blog de Udi, puedes establecer o borrar 2 bits en una ubicación que el código del sistema considera como el campo WSBflags en la estructura tagSBINFO. Esta no es la forma habitual de explotar un UAF, pero el artículo da una pista sobre cómo hacerlo, que explicaré en las siguientes secciones. Primero, entendamos cómo manipulamos esos bits.

Estructura tagSBINFO (idéntica en 32 y 64 bits):

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

El UAF se encuentra en la función win32k!xxxEnableWndSBArrows(), que se usa para habilitar o deshabilitar las flechas de una o dos barras de desplazamiento (horizontal o vertical) en un control de barra de desplazamiento. Un control de barra de desplazamiento es una ventana especial utilizada para manipular barras de desplazamiento. Se puede crear llamando a CreateWindow() con la clase de ventana incorporada "SCROLLBAR".

Prototipo de win32k!xxxEnableWndSBArrows():

root@kitploit:~
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);

El parámetro WSBflags tiene el mismo significado que en WinUser.h, e indica qué barras de desplazamiento se van a manipular:

root@kitploit:~
#define SB_HORZ 0 
#define SB_VERT 1 
#define SB_CTL  2 
#define SB_BOTH 3

El parámetro wArrows indica si las flechas están habilitadas o deshabilitadas. Si se establece, las flechas están deshabilitadas; de lo contrario, están habilitadas. Los dos bits más bajos de wArrows corresponden a la barra horizontal, los dos siguientes a la vertical, y el resto de bits no son relevantes para esta explotación.

El siguiente código está tomado de la función win32k!xxxEnableWndSBArrows(), y si SB_HORZ o SB_BOTH están establecidos, establece o borra los bits correspondientes a las flechas horizontales:

Este bug existe cuando se establecen los indicadores de las barras horizontal y vertical. Después de actualizar la barra horizontal, una vez que la ventana correspondiente a esa barra es visible en el escritorio, win32k!xxxEnableWndSBArrows() llama a win32k!xxxDrawScrollBar(), que, como se mencionó anteriormente, puede desencadenar una devolución de llamada potencial en modo usuario.

Antes de discutir la devolución de llamada en modo usuario, continuemos con lo que sucede después de llamar a win32k!xxxDrawScrollBar(). Esto en realidad tiene la misma lógica que la barra horizontal, solo que con algunos bits diferentes. Si elegimos deshabilitar la barra vertical y asumimos que hemos provocado el UAF, entonces se escribirán 2 bits en algún lugar del bloque de montón tagSBINFO. Por lo tanto, si el valor original era 0x2, ahora se convierte en 0xe. Como se muestra en la siguiente figura.

Este cambio de bits es suficiente para lograr la ejecución de código. No he profundizado en cómo se podría usar la limpieza de bits para la explotación, pero es posible.

El punto clave de lo anterior es que, para poder manipular ambas barras de desplazamiento (horizontal y vertical), debemos crear un control de barra de desplazamiento que tenga ambos elementos. Esto se logra llamando a CreateWindow() con los indicadores WS_HSCROLL y WS_VSCROLL. El código es:

root@kitploit:~
g_hSBCtl = CreateWindowEx( 
    0,                  // Sin estilo extendido 
    "SCROLLBAR",        // clase 
    NULL,               // nombre 
    SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // vertical + horizontal 
    10,                 // x 
    10,                 // y 
    100,                // ancho 
    100,                // alto 
    g_hSpray[UAFWND],   // ventana padre sin modo 
    (HMENU)NULL,
    NULL,               // propietario de la ventana 
    NULL                // parámetros extra 
    );

Se puede asegurar su visibilidad con el siguiente código (normalmente es el valor predeterminado, pero aquí se llama explícitamente):

root@kitploit:~
result = ShowWindow(g_hSBCtl, SW_SHOW);

La barra de desplazamiento está habilitada por defecto. Cuando estemos listos para intentar desencadenar el código de la vulnerabilidad, podemos deshabilitar la barra para corromper los bits que necesitamos:

root@kitploit:~
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);

Desencadenando la vulnerabilidad

Aunque hemos descrito los detalles del bug y cómo activar el código relevante, todavía ignoramos el paso más importante: interceptar la devolución de llamada en modo usuario iniciada por win32k!xxxDrawScrollBar(), para que podamos cambiar el contenido del montón antes de que win32k!xxxEnableWndSBArrows() continúe su ejecución. Necesitamos realmente provocar el bug, pero sin conocer la situación de Win32k.sys ni las API relacionadas, como me pasó al principio, es una aventura.

En un artículo anterior hay un buen diagrama de la pila de llamadas que muestra este proceso en profundidad: se desencadena a través de win32k!xxxDrawScrollBar(), luego se llama a ClientLoadLibrary(), que se despacha a través de KeUserModeCallback(). Necesitamos comprender la situación de KeUserModeCallback() para poder engancharlo en nuestro propio proceso.

He encontrado algunos buenos documentos que mencionan las devoluciones de llamada en modo usuario más o menos. Las partes relacionadas con win32k son muy útiles:

  • https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf (el documento de Tarjei)
  • http://azimuthsecurity.com/resources/recon2012_mandt.pptx (PPT de Tarjei con mucha información adicional)
  • http://www.nynaeve.net/?p=204
  • http://www.cprogramdevelop.com/3825874/
  • http://www.zer0mem.sk/?p=410
  • https://www.reactos.org/wiki/Techwiki:RegisterUserApiHook
  • http://pasotech.altervista.org/windows_internals/Win32KSYS.pdf
  • http://j00ru.vexillium.org/?p=614
  • http://uninformed.org/index.cgi?v=10&a=2#SECTION00042000000000000000

Normalmente, cada proceso tiene una tabla de punteros a funciones de devolución de llamada en modo usuario, a la que apunta PEB->KernelCallBackTable. Cuando el kernel quiere invocar una función en modo usuario, pasa el índice de la función a KeUserModeCallBack(). En el ejemplo anterior, el índice apunta a la función en modo usuario __ClientLoadLibrary().

KeUserModeCallBack() busca la función correspondiente en PEB->KernelCallBackTable según el índice y la ejecuta, llamando finalmente a KiUserModeCallbackDispatch() en modo usuario.

Para enganchar un punto de entrada específico, debemos buscar el índice de __ClientLoadLibrary() en PEB->KernelCallBackTable y reemplazarlo con nuestra propia función. Vale la pena señalar que este índice varía según la versión del sistema operativo y la plataforma de hardware.

Si queremos ver PEB->KernelCallBackTable, podemos usar WinDbg para encontrar la dirección de esta tabla. Comparando plataformas de 32 y 64 bits, no se observan grandes diferencias.

root@kitploit:~
kd> dt !_PEB @$peb 
ntdll!_PEB 
    +0x000 InheritedAddressSpace    : 0 '' 
    +0x001 ReadImageFileExecOptions : 0 '' 
    +0x002 BeingDebugged            : 0 '' 
    +0x003 BitField                 : 0x8 '' 
    +0x003 ImageUsesLargePages      : 0y0 
[...] 
    +0x02c KernelCallbackTable      : 0x76daf620 Void 
    
kd> dds 0x76daf620 
76daf620 76d96443 user32!__fnCOPYDATA 
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA 
76daf628 76da736b user32!__fnDWORD 
76daf62c 76d9d603 user32!__fnNCDESTROY 
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG 
76daf634 76ddf1be user32!__fnINOUTDRAG 
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS 
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL 
[...] 
76daf724 76da3962 user32!__ClientLoadLibrary 

kd> ?? (0x76daf724-0x76daf620)/4 int 0n65

En el ejemplo anterior, sabemos que el índice de __ClientLoadLibrary es 65, y ese es el lugar que debemos enganchar. Después de engancharlo, descubrí que __ClientLoadLibrary es llamado varias veces por el código de win32k. Lo primero que debemos hacer es, antes de que se produzca la llamada que nos interesa, notificar a nuestro código de enganche para saber que realmente hemos llegado al punto que queremos modificar. Por lo tanto, el código de enganche usa una variable global como indicador, y solo realiza la acción cuando este indicador está activado.

Ahora hay dos obstáculos:

  1. Si dejamos que el __ClientLoadLibrary original se ejecute normalmente, cuando se desencadene la vulnerabilidad en win32k, encontré que el flujo no llega al modo usuario. No profundicé en esto, solo supongo que la biblioteca que se intenta cargar ya está cargada, por lo que ya no necesita llamar a esta función de carga. Para forzar que ocurra esta carga, hago que la función de enganche de __ClientLoadLibrary no devuelva ningún resultado cada vez, obligándola a intentar cargar repetidamente. Mi trabajo consistió simplemente en devolver NULL en la estructura de parámetros, lo que se logró invirtiendo la función __ClientLoadLibrary() en user32.dll.
  2. La ejecución de EnableScrollBar() eventualmente desencadena __ClientLoadLibrary, y luego llegamos al punto donde queremos explotar a través de win32k!xxxDrawScrollBar(). Por lo tanto, debemos conocer el número de llamadas antes de que aparezca lo que nos interesa. Usando un contador, puedo saber exactamente cuándo se ha desencadenado el bug y se ha entrado en el código de enganche. Afortunadamente, este contador es constante entre plataformas y versiones del sistema operativo.

Por lo tanto, la función de enganche tiene el siguiente aspecto:

root@kitploit:~
void ClientLoadLibraryHook(void * p) 
{ 
    CHAR Buf[PGSZ]; 
    memset(Buf, 0, sizeof(Buf)); 
    if (g_PwnFlag) 
    { 
        dprintf("[+] __ClientLoadLibrary hook called\n"); 
        if (++g_HookCount == 2) 
        {
            g_PwnFlag = 0;      // solo se ejecuta una vez.. 
            ReplaceScrollBarChunk(NULL); 
        }
    } 
    fpClientLoadLibrary(&Buf); // llamada a la función original
}

Una vez determinamos que la llamada actual proviene de win32k!xxxDrawScrollBar(), podemos intentar desencadenar el bug. Ahora solo consideramos desencadenarlo: basta con llamar a DestroyWindow(g_hSBCtl). Esto hará que la estructura tagSBINFO de la ventana se libere, mientras que la estructura de la ventana en sí no se libera inmediatamente porque su contador de referencias aún está en uso por la llamada original, pero tagSBINFO no tiene ese mecanismo de contador, por lo que se libera inmediatamente.

En este punto, hemos desencadenado el bug. Aunque no hemos reasignado el bloque de montón que contenía tagSBINFO, aún podemos escribir los dos bits que representan "deshabilitado" en el montón ya liberado. El siguiente paso es reemplazar este bloque liberado con algo que queramos, para poder hacer algo más interesante que solo establecer unos pocos bits. Para ello, necesitamos conocer algunos antecedentes sobre el montón del escritorio.

Montón del escritorio

win32k.sys utiliza el montón del escritorio para almacenar objetos GUI relacionados con un escritorio determinado. Esto incluye objetos de ventana y sus estructuras asociadas, como listas de propiedades, textos de ventana y barras de desplazamiento. El artículo de Tarjei menciona esto, pero hay que tener especial cuidado: el montón del escritorio es en realidad una versión simplificada de un asignador de backend en modo usuario, y también utiliza RtlAllocateHeap() y RtlHeapFree() para operar. El montón del escritorio está administrado por una estructura _HEAP, y como no tiene un asignador frontal, no hay cosas como LFH (Low Fragmentation Heap) ni listas de vista lateral.

Cada escritorio que se crea tiene un montón de escritorio correspondiente que le da servicio. Esto significa que podemos asignar un nuevo escritorio para obtener un montón "limpio" sobre el que nuestras operaciones sean más predecibles. Sin embargo, esto no tiene sentido para procesos con baja integridad, ya que no se les permite crear un nuevo escritorio.

Ahora el problema principal es rastrear el proceso de asignación (más adelante se cubrirán más detalles sobre metadatos, etc.).

Monitoreo de asignaciones en el montón del escritorio

Para monitorear las asignaciones y liberaciones en el montón del escritorio, suelo usar scripts de WinDbg:

Monitoreo de montón en 64 bits

root@kitploit:~
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";

Monitoreo de montón en 32 bits

root@kitploit:~
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc"; 
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"

Además de estos scripts de depuración, dado que el montón del escritorio es solo una forma simplificada de un asignador de backend en modo usuario, también podemos usar el comando !heap incorporado en WinDbg.

Rellenando los huecos en el montón

Para explotar este bug, necesitamos reemplazar el bloque de montón tagSBINFO que se liberó más recientemente, y también sabemos cómo usar estos bugs típicos, que es corromper datos adyacentes. Esto nos plantea el requisito básico de preasignar algunos bloques de montón cerca de la estructura que queremos corromper. Para predecir dónde se asignará un bloque de montón, debemos controlar todo el diseño del montón (o tanto como sea posible). Para lograrlo, una forma factible es asignar tantos bloques de montón como sea posible para llenar los huecos que se han liberado, de modo que los nuevos bloques asignados sean contiguos. Cuando necesitemos un agujero, podemos hacerlo en una posición predecible (liberando un bloque ya asignado).

Esta parte es una comprensión simple de los factores que afectan la asignación; los scripts de WinDbg anteriores pueden ayudarnos. Tarjei mencionó en su presentación sobre win32k los principales objetos que se asignan en el montón del escritorio, y coinciden con lo que he visto. Estos son:

  • Window
  • Menu
  • Hook
  • CallProcData
  • Input Context

El montón del escritorio es bastante interesante; la mayoría de las asignaciones están directamente relacionadas con objetos de ventana y se administran a través de la estructura tagWND, lo que significa que si queremos asignar un bloque de montón de tamaño arbitrario (pequeño para llenar un agujero pequeño), primero debemos asignar una ventana relacionada. Se puede pensar que la estructura de la ventana es la interfaz de asignación del montón. Otro punto interesante es que muchos bloques de montón asignados a través de operaciones de ventana no se pueden liberar inmediatamente a menos que la ventana misma sea destruida, lo que obviamente afecta al montón. Finalmente, supongamos que asignamos un bloque de tamaño N a través de una ventana, como en el ejemplo anterior; ¿necesitamos asignar muchos bloques de tamaño N? Se puede determinar que las estructuras de ventana asignadas, independientemente de su tamaño, no se almacenan en una lista enlazada. Por lo tanto, cada ventana puede controlar una asignación de montón de tamaño N. Es decir, si necesita asignar una gran cantidad de bloques de montón de tamaño N, debe crear primero muchas ventanas y usar las ventanas para ayudar en la asignación de bloques.

También hay tres tipos de datos importantes que se asignan en el montón del escritorio, que podemos usar indirectamente a través de objetos de ventana para controlar los datos en el montón. Usamos mucho estos tipos de datos para lograr la explotación y construir la distribución del montón. Estos tres tipos de datos son:

  1. Estructura tagPROPLIST: si se asigna lo suficientemente pequeña, puede llenar agujeros pequeños. Un objeto de ventana contiene un tagPROPLIST, que en sistemas de 32 bits es de 0x10 bytes y en 64 bits es de 0x18 bytes.
  2. Texto de ventana: es una cadena UNICODE de tamaño arbitrario asignada en el montón del escritorio, almacenada a través de una estructura _LARGE_UNICODE_STRING incrustada en el tagWND. Tenga en cuenta que el campo strName es una estructura, no un puntero, pero esa estructura contiene un puntero relacionado con el bloque de montón del texto de ventana.
  3. Estructura tagSBINFO: la raíz de la vulnerabilidad, contiene 4 o todos los campos miembro controlables.

La figura 2 muestra las relaciones entre estos tipos de datos:

Para inicializar el montón, creo muchas estructuras tagWND (creando objetos de ventana). Esto puede llenar muchos agujeros grandes en el montón y también nos proporciona una interfaz para asignar otros bloques de montón que necesitamos. En Windows 8 y 8.1, asignar una nueva ventana provoca que se asigne automáticamente una estructura tagPROPLIST (se puede observar con el script de WinDbg mencionado anteriormente). En Windows 7 y versiones anteriores, nosotros mismos asignamos un nuevo tagPROPLIST para llenar agujeros pequeños.

Aquí, todos los objetos de ventana que rociamos no tienen texto de ventana; sin embargo, si es necesario, aún podemos usarlo para asignar o liberar bloques de montón de tamaño arbitrario. Una vez creados, no se puede eliminar la lista de propiedades existente a menos que se destruya la ventana, pero podemos controlar la reasignación de esta lista para que contenga nuevas propiedades; este mecanismo se puede usar para hacer agujeros en lugares anteriores. Todo lo que necesita hacer es establecer una nueva propiedad que no existía en la lista anterior (diferenciada por atomKey).

Verificando el diseño del montón (spray)

Curiosamente, el montón del escritorio está mapeado en el espacio de usuario, aunque es de solo lectura. Esto significa que podemos verificar el diseño que hemos construido y asegurarnos de que funciona correctamente. Primero debemos determinar dónde está mapeado el montón del escritorio en modo usuario. Esto se menciona en el documento de Tarjei sobre win32k. En el TEB hay una estructura no documentada, Win32ClientInfo, relacionada con esto, cuya definición aproximada es:

root@kitploit:~
typedef struct _CLIENTINFO { 
    ULONG_PTR CI_flags; 
    ULONG_PTR cSpins; 
    DWORD dwExpWinVer; 
    DWORD dwCompatFlags; 
    DWORD dwCompatFlags2; 
    DWORD dwTIFlags; 
    PDESKTOPINFO pDeskInfo; 
    ULONG_PTR ulClientDelta; // incompleto. Ver reactos 
} CLIENTINFO, *PCLIENTINFO;

Donde la estructura PDESKTOPINFO se define como:

root@kitploit:~
typedef struct _DESKTOPINFO { 
    PVOID pvDesktopBase; 
    PVOID pvDesktopLimit;   // incompleto. Ver reactos
} DESKTOPINFO, *PDESKTOPINFO;

El primer campo pvDesktopBase apunta a la dirección del montón del escritorio en modo kernel, lo anotamos. El campo ulClientDelta de Win32ClientInfo es la diferencia entre una dirección en modo kernel y una en modo usuario, y con esta información podemos obtener lo que necesitamos.

Sin embargo, no queremos analizar la estructura del montón nosotros mismos, sino que queremos tener un identificador de user32, como el valor de un HWND, que se pueda convertir a una dirección mapeada en modo usuario, para poder determinar si está asociado a otras asignaciones del montón. Para encontrar este identificador, necesitamos localizar una estructura llamada gSharedInfo, que normalmente se encuentra en uer32.dll y se exporta a partir de Windows 7, por lo que es fácil de encontrar.

En la mayoría de los sistemas, esta estructura se define como:

root@kitploit:~
kd> dt !tagSHAREDINFO 
win32k!tagSHAREDINFO 
    +0x000 psi                  : Ptr32 tagSERVERINFO 
    +0x004 aheList              : Ptr32 _HANDLEENTRY 
    +0x008 HeEntrySize          : Uint4B 
    +0x00c pDispInfo            : Ptr32 tagDISPLAYINFO 
    +0x010 ulSharedDelta        : Uint4B 
    +0x014 awmControl           : [31] _WNDMSG 
    +0x10c DefWindowMsgs        : _WNDMSG 
    +0x114 DefWindowSpecMsgs    : _WNDMSG

En la estructura anterior, aheList apunta a un array de _HANDLEENTRY, cada _HANDLEENTRY contiene un identificador que apunta a una dirección en modo kernel. Podemos usar la "diferencia entre la dirección de modo kernel y modo usuario" para obtener una dirección de modo usuario utilizable. Desafortunadamente, esto no es posible en versiones anteriores a Windows 7, porque gSharedInfo no se exporta. El artículo de Tarjei dice que la función no documentada CsrClientConnectToServer se puede usar para obtener una copia de gSharedInfo, pero no encontré un ejemplo viable. Molestamente, la estructura necesaria para implementar esta función tiene una longitud que varía según el sistema, por lo que, según mi experiencia, no se puede confiar completamente en lo que se ve en ReactOS.

Una vez que calculamos la ubicación mapeada, podemos construir una función que nos indique la posición del objeto de ventana en el montón del escritorio. Luego, si queremos saber dónde se asignó el bloque de montón correspondiente a la lista de propiedades o al texto, solo necesitamos analizar la estructura en modo usuario.

Reemplazando tagSBINFO con tagPROPLIST

Ahora, finalmente nos acercamos a la explotación de esta vulnerabilidad. Ya tenemos un método para controlar los bloques de montón, un método para verificar si la posición del bloque es correcta y podemos desencadenar el bug. Por lo tanto, ahora podemos finalmente reemplazar el bloque tagSBINFO liberado con una lista de propiedades tagPROPLIST seleccionada. Tenga en cuenta que, dado que tagPROPLIST es solo la cabecera de una lista grande, podemos hacer que el tamaño de la lista coincida con el tamaño del bloque de la barra de desplazamiento. La parte posterior de tagPROPLIST es básicamente un array de estructuras tagPROP, o lista de propiedades; por lo tanto, no distinguiré entre array y lista. La estructura tagPROPLIST en sistemas de 64 bits es:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
+0x000 cEntries     : Uint4B 
+0x004 iFirstFree   : Uint4B 
+0x008 aprop        : tagPROP 
    +0x000 hData    : Ptr64 
    +0x008 atomKey  : Uint2B 
    +0x00a fs       : Uint2B

Como se mencionó anteriormente, el objeto de ventana tiene una lista de propiedades asociada. Esta lista se crea mediante la función SetProp(). Se utiliza para buscar propiedades existentes mediante atomKey; si la propiedad no existe, se crea un nuevo elemento en la lista de propiedades. Si no hay ninguna lista de propiedades, se crea una y se enlaza a la estructura tagWND.

Si ya hemos rociado un tagWND con el bug y hemos creado los elementos tagPROPLIST asociados, el diseño final es como se muestra en la Figura 3:

Una vez configurado, podemos asignar el control de barra de desplazamiento que vamos a explotar. Esto dará como resultado la Figura 4:

Luego, manipulamos la barra de desplazamiento para desencadenar la devolución de llamada en modo usuario en nuestro hook. En la función hook, intentamos destruir la ventana para liberar la estructura tagSBINFO. Esto lleva a la Figura 5:

En 64 bits, la estructura tagSBINFO tiene 0x28 bytes, y un elemento del array tagPROPLIST tiene 0x18 bytes, de los cuales 0x10 bytes son el tagPROP predeterminado. Por lo tanto, una lista de propiedades con dos elementos tiene 0x28 bytes (0x8 + 0x10 + 0x10), lo que es perfecto. Suponiendo que hemos rociado la memoria para llenar el agujero. Solo necesitamos una ventana que tenga una lista de propiedades; inmediatamente después de liberar la estructura tagSBINFO (como se muestra en la figura anterior), agregamos un nuevo elemento a la lista de propiedades. Este proceso primero libera el bloque tagPROPLIST anterior de 0x18 bytes. Como el montón ya ha sido rociado, no hay bloques libres cerca, por lo que no se produce fusión de bloques, y no hay suficiente espacio para acomodar los nuevos 0x28 bytes. De esta manera, la posición recién liberada de tagSBINFO se utiliza (su tamaño es exactamente 0x28 bytes). Esta situación se muestra en la Figura 6:

Al regresar de la función hook, se desencadena el UAF y se escriben algunos bits en el campo cEntries de tagPROPLIST. El valor original de cEntries era 0x2, lo que indica que creamos dos elementos en la lista de propiedades. Después del desbordamiento, se convierte en 0xe, con los bits 3 y 4 (contando desde 1) establecidos en 1.

En este punto, hemos completado el desbordamiento del nuevo montón y aumentado el número de elementos de la lista de propiedades, que ahora es mayor que 0xc. A continuación, desbordaremos los bloques de montón adyacentes, lo que llamamos corrupción de fase 2.

Abuso de la lista de propiedades – Corrupción de fase 2

En el blog de Udi se explicó hasta aquí. Esto se denomina "desbordamiento de montón típico", pero según mi experiencia, es difícil lograr lectura/escritura arbitraria o ejecución de código a partir de esto. Observamos nuevamente la estructura tagPROPLIST en 64 bits:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
+0x000 cEntries     : Uint4B 
+0x004 iFirstFree   : Uint4B 
+0x008 aprop        : tagPROP 
    +0x000 hData    : Ptr64 
    +0x008 atomKey  : Uint2B 
    +0x00a fs       : Uint2B

La corrupción de fase 1 nos proporciona un array tagPROPLIST corrupto, lo que nos permite aumentar los elementos tagPROP. tagPROPLIST solo tiene dos campos:

  • cEntries: indica el número de elementos que posee.
  • iFirstFree: indica el índice del primer elemento libre. Una lista llena (que necesita una nueva asignación) significa iFirstFree == cEntries.

Cuando se inserta un nuevo elemento en la lista, se llama a una función que escanea cada elemento hasta encontrar el índice iFirstFree adecuado. Si no lo encuentra, verifica si iFirstFree es mayor que cEntries. Si el atomKey correspondiente no está en la lista, se comprueba si iFirstFree != cEntries. Si no son iguales, se inserta un elemento en la posición del índice iFirstFree; si son iguales, se asigna una nueva lista de propiedades que pueda contener el elemento insertado, se copian los elementos existentes y se inserta el nuevo.

El campo atomKey corresponde a LPCTSTR lpString. Como se documenta en MSDN para SetProp(), el llamante puede pasar un puntero a cadena o un valor atom de 16 bits. Cuando se pasa un puntero a cadena, se convierte automáticamente a un valor atom antes de guardarse en la lista de propiedades. Como podemos pasar cualquier valor atom a SetProp(), tenemos la capacidad de controlar estos dos bytes, pero con algunas restricciones. Los datos de atomKey que corrompamos no deben repetirse, porque si configuramos una nueva propiedad, reemplazará la existente con el mismo valor atom. Además, el campo fs no es controlable; su valor es 0 cuando atomKey < 0xBFFF, lo que corresponde a valores atom enteros. El valor de fs es 2 cuando atomKey >= 0xC000.

Otra cosa a tener en cuenta es que tagPROP tiene solo 0xc bytes. Esta estructura en sistemas de 64 bits está alineada a 0x10 bytes, por lo que al insertar un tagPROP, hay 4 bytes adicionales que no pueden ser corrompidos. El último punto importante es que los primeros 8 bytes de un bloque tagPROPLIST definen el tamaño de la lista de elementos, lo que significa que cada nuevo tagPROP insertado se escribirá siempre en una posición alineada a 8 bytes.

Para cada tagPROP insertado, en sistemas de 64 bits la situación es:

root@kitploit:~
* Offset 0x0: 8 bytes de datos completamente controlables (hData)
* Offset 0x8: 2 bytes de datos mayormente controlables (atomKey)
* Offset 0xa: 2 bytes de datos no controlables (fs)
* Offset 0xc: 4 bytes de datos que no se pueden modificar (padding)

Esta situación es mucho mejor que 2 bits, pero aún no es perfecta. A menos que podamos sobrescribir algo con los primeros 8 bytes, que provienen del campo hData completamente controlable, de lo contrario estaremos muy limitados. Si necesitamos escribir en campos más profundos de una estructura adyacente, no podemos evitar la corrupción incontrolable de algunos valores. Dediqué algún tiempo a buscar varios objetos en el montón del escritorio, considerando las limitaciones de corrupción anteriores. Para sortear estas limitaciones y lograr lectura/escritura arbitraria, la única forma que se me ocurrió fue corromper el campo strName del tagWND, que es una estructura _LARGE_UNICODE_STRING:

root@kitploit:~
kd> dt !_LARGE_UNICODE_STRING 
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           : Uint4B 
    +0x004 MaximumLength    : Pos 0, 31 Bits 
    +0x004 bAnsi            : Pos 31, 1 Bit 
    +0x008 Buffer           : Ptr64 Uint2B

Si podemos corromper el campo Buffer de esta estructura, podríamos leer o escribir hasta MaximumLength bytes desde una dirección dada manipulando el texto de la ventana. Eso es lo que haré. Puede haber notado esta estructura en los capítulos anteriores, sobre cómo crear un bloque de montón de tamaño y valor arbitrarios en el montón del escritorio, por lo que la misma situación se aplica aquí.

Ahora sabemos cómo corromper datos usando elementos de la lista tagPROPLIST, qué partes podemos controlar y, lo que es más importante, las limitaciones que enfrentaremos, que difieren entre 32 y 64 bits. Lo que hicimos antes en 64 bits no funciona en 32 bits. Pronto pasaremos de la corrupción de fase 2 (es decir, escribir a través de la estructura tagPROP) a otra "primitiva de operación" de corrupción mediante la cual podemos escribir datos completamente controlables, a lo que me refiero como corrupción de fase 3.

Construyendo una primitiva de lectura/escritura – Corrupción de fase 3

64 bits

El plan objetivo es corromper el campo strName del tagWND adyacente. Ya sabemos que es una estructura LARGE_UNICODE_STRING, pero echemos un vistazo a la estructura tagWND en más detalle, que se ve así:kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [SNIPPED FLAGS] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [SNIPPED FLAGS] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [SNIPPED FLAGS] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [SNIPPED FLAGS] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [SNIPPED FLAGS] +0x120 bChildNoActivate : Pos 11, 1 Bit

Arriba está la estructura de 64 bits; podemos ver que el desplazamiento de la estructura _LARGE_UNICODE_STRING que queremos sobrescribir es 0xd8. También notarás un campo importante al comienzo de esta estructura. Originalmente esperaba poder manipularlo a fondo, pero _THRDESKHEAD tiene muchos punteros que nos obligan a mantener la claridad, y desafortunadamente, no podemos controlar dónde escribimos, por las limitaciones que discutimos anteriormente.

Definición de la estructura _THRDESKHEAD:

root@kitploit:~
kd> dt !_THRDESKHEAD 
win32k!_THRDESKHEAD 
    +0x000 h        : Ptr64 Void 
    +0x008 cLockObj : Uint4B 
    +0x010 pti      : Ptr64 tagTHREADINFO 
    +0x018 rpdesk   : Ptr64 tagDESKTOP 
    +0x020 pSelf    : Ptr64 UChar

El problema con _THRDESKHEAD no solo nos desconcierta, sino que también nos lleva a reconsiderar la restricción de alineación. Independientemente del desplazamiento al que se coloque el nuevo elemento de la lista tagPROP, nuestra operación de escritura sobrescribirá directamente el inicio de _LARGE_UNICODE_STRING:

root@kitploit:~
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           <-- hData (completamente controlable) sobrescribe aquí 
    +0x004 MaximumLength    <-- y aquí 
    +0x004 bAnsi            <-- y aquí 
    +0x008 Buffer           <-- atomKey y fs (parcialmente controlable) sobrescribe aquí

Está claro que queremos sobrescribir el puntero Buffer para poder acceder a memoria en una dirección arbitraria. Sin embargo, incluso si podemos atacar de forma segura otros campos de esta estructura, no podemos controlar el puntero que necesitamos.

No podemos corromper datos arbitrarios; la solución a este problema ya no es la corrupción de tagPROPLIST, sino que se convierte en un mecanismo de corrupción completamente diferente.

En versiones posteriores a Windows XP, la cabecera del bloque del montón (es decir, _HEAP_ENTRY) del asignador del modo usuario de backend (como el montón del escritorio del kernel) se almacena en el montón y se encuentra antes del contenido real del bloque. El montón del escritorio en sí es gestionado por la estructura _HEAP, lo que nos da cierta libertad al explotar este bloque.

La definición de la estructura _HEAP_ENTRY es la siguiente:

root@kitploit:~
kd> dt !_HEAP_ENTRY 
ntdll!_HEAP_ENTRY 
    +0x000 PreviousBlockPrivateData : Ptr64 Void 
    +0x008 Size             : Uint2B 
    +0x00a Flags            : UChar 
    +0x00b SmallTagIndex    : UChar 
    +0x00c PreviousSize     : Uint2B 
    +0x00e SegmentOffset    : UChar 
    +0x00f UnusedBytes      : UChar

La cabecera del bloque tiene un total de 0x10 bytes; los primeros 8 bytes son PreviousBlockPrivateData, que se utiliza para contener los datos reales del bloque anterior cuando el tamaño solicitado supera el normal de 0x10 (se redondea a 8 bytes si es menor de 8). Esto se describe brevemente en la entrada del blog de Leviathan, además de artículos anteriores sobre el montón en modo usuario. Size y PreviousSize indican el tamaño del bloque actual y el del bloque anterior, en unidades de 0x10 bytes. Flags indica si el bloque está libre o no. Si el modo seguro _HEAP_ENTRY está habilitado en _HEAP, SmallTagIndex contendrá la suma de verificación XOR de los datos del bloque.

Aunque la restricción de alineación juega en nuestra contra, ciertamente existe. Si llamas a tagPROPLIST, siempre tendrá al menos 0x18 bytes, más 0x10 bytes por tagPROP. Para un tagPROPLIST de dos elementos de 0x28 bytes, se colocará en un bloque de montón de 0x20 bytes, y los bytes adicionales representados por PreviousBlockPrivateData utilizarán el bloque de montón adyacente. Esto significa que cuando agregamos un tercer elemento de la lista, el bloque adyacente se corrompe, y los 8 bytes controlables de hData sobrescribirán la parte superior de _HEAP_ENTRY.

Lo que queremos hacer es aprovechar esto para, de alguna manera, escribir datos arbitrarios en la posición superior de Buffer. Primero, modificamos el diseño del montón para acercarnos al bloque de montón tagPROPLIST que corrompemos; durante el proceso de control, tenemos un pequeño bloque de montón que contiene una cadena de texto relacionada con la ventana, al que llamaremos "bloque de sobrescritura". Adyacente a este "bloque de sobrescritura", colocamos un tagWND para corromper ese tagWND. La siguiente figura 7 ilustra este proceso. Nota que hemos omitido los bloques de montón previamente rociados para ahorrar espacio, por lo que estos ahora deben considerarse implícitos.

A continuación, insertamos un tercer tagPROP en la lista tagPROPLIST, que sobrescribirá los últimos 8 bytes de _HEAP_ENTRY y los primeros 8 bytes del "bloque de sobrescritura". De esta manera, podemos modificar el _HEAP_ENTRY del "bloque de sobrescritura" para que su tamaño sea mayor que su tamaño real y pueda contener la estructura tagWND adyacente.

Ahora liberamos el "bloque de sobrescritura" que acabamos de corromper, para que el administrador del montón lo coloque en la lista libre correspondiente a un tamaño de bloque (cada lista libre corresponde a un tamaño fijo) mayor que el tamaño real del "bloque de sobrescritura". Luego, reutilizamos este bloque modificando el texto de la ventana (que podemos controlar completamente). Sin embargo, esto presenta un pequeño problema que debemos resolver. Cuando se libera el "bloque de sobrescritura", el administrador del montón intenta encontrar el bloque anterior adyacente, lo que depende del campo Size corrompido. El administrador del montón verifica si este bloque adyacente está libre para fusionarlo. De cualquier manera, queremos controlarlo y establecer una marca de "en uso". Modificando ligeramente nuestro diseño del montón podemos lograrlo. En este punto, colocamos un bloque falso cuya cabecera tiene la marca "en uso" y cuyo campo PreviousSize se establece con el valor del campo Size corrompido, lo cual podemos lograr simplemente asignando el texto de ventana de otra ventana. El nuevo diseño del montón es el siguiente (figura 8):

Ahora podemos liberar el "bloque de sobrescritura" corrompido actualizando el texto de la cadena asociada a la ventana para que tenga una longitud mayor que los 0x10 bytes originales. De esta forma, el "bloque de sobrescritura" corrompido se libera primero y se coloca en la lista libre, pero su tamaño es el corrompido, que es mayor que el real. Podemos ajustar este tamaño según nuestras necesidades. Luego, nuestros datos de cadena se escriben en este "bloque de sobrescritura", y lo utilizamos para corromper el tagWND adyacente con datos arbitrarios. Como se muestra a continuación (figura 9):

Esta es la corrupción de la fase 3. Ahora podemos sobrescribir el puntero strName.Buffer con cualquier dato que queramos. Sin embargo, corromper otros datos de tagWND todavía presenta algunos problemas, pero esto no es un problema porque el montón del escritorio está mapeado en el espacio de usuario. Por lo tanto, antes de corromper todo, leemos todo el contenido de tagWND, modificamos el contenido de la estructura strName según lo deseado, y enviamos todos los datos modificando el texto de la ventana.

A través de strName no solo tenemos una "primitiva" de lectura/escritura arbitraria, sino que también podemos modificar strName repetidamente, lo que permite el mecanismo de modificación del texto de la ventana. Siempre que la longitud de la cadena escrita no supere el valor de MaximumLength, podemos seguir usando el mismo bloque. Por lo tanto, cada vez que queremos modificar la dirección de strName para leer un valor en algún lugar, adjuntamos una nueva cadena con nuestros datos para actualizar el "bloque de sobrescritura". Esta reutilización se muestra en la figura 10. Nota que nuevamente he ampliado el nivel de detalle de la vista general para mostrar cada corrupción en detalle.

Esto significa que finalmente solo necesitamos corromper dos cosas adicionales (además del elemento de la lista tagPROPLIST original):

  1. La cabecera del "bloque de sobrescritura". Podemos leer esto antes de la corrupción, por lo que sabemos cómo restaurarlo después de usarlo. Curiosamente, incluso podemos reparar el bloque del tercer elemento de la lista tagPROPLIST simplemente enviando el atomKey que usamos para la corrupción a la ubicación original.
  2. La estructura strName, que podemos modificar fácilmente escribiendo el texto de la ventana. La operación requiere que el valor de retorno se establezca en nulo.

Ahora, si queremos leer algunos bytes de alguna ubicación de memoria, consultamos el texto de la ventana a través de la función InternalGetWindowText(), donde tenemos la entrada de strName corrompida. Podemos leer el número de bytes declarado por el campo Length. De manera similar, si queremos escribir en una ubicación arbitraria de la memoria, usamos la función NtUserDefSetText() para actualizar el texto de la ventana corrompida, pero la cantidad escrita no debe superar el valor declarado en el campo MaximumLength (que también podemos establecer). De esta forma, el búfer existente se reutiliza y apunta a la dirección de memoria que queremos.

Codificación del montón en Windows 8 y 8.1

Aunque el asignador de backend en modo usuario utiliza codificación del montón desde Windows Vista, el montón del escritorio nunca lo habilitó hasta Windows 8. Por lo tanto, en sistemas posteriores a Windows 8, cuando sobrescribimos el "bloque de sobrescritura", surge un obstáculo. Sin embargo, la estructura _HEAP que contiene el montón incluye esta cookie y se usa para codificar toda la cabecera del montón, por lo que podemos leer esta cookie del montón del escritorio mapeado en el espacio de usuario y luego codificar la cabecera del "bloque de sobrescritura" invirtiendo el código del asignador e imitando su operación, que el asignador puede aceptar.

Sistemas de 32 bits

Primero, hay que notar que en sistemas de 32 bits la estructura tagPROP tiene 8 bytes, no 0xc bytes como en sistemas de 64 bits, y nuestro campo hData controlable tiene solo 4 bytes, no 8 bytes como en sistemas de 64 bits. Además, no hay bytes de relleno adicionales; en sistemas de 64 bits hay 8 bytes de relleno, por lo que la estructura tiene exactamente 8 bytes. Esto significa que no podemos corromper completamente la cabecera del bloque adyacente si solo podemos controlar parcialmente los datos. En algunas versiones de Windows es posible porque podemos controlar los campos más importantes, pero en Windows 8 y 8.1 la cabecera del montón está codificada, y finalmente podemos sobrescribir inseguramente parte de la cabecera del montón a través del campo fs. La cabecera _HEAP_ENTRY de sistemas de 32 bits se ve similar, pero carece del campo PreviousBlockPrivateData.

Todavía no podemos corromper todas las partes de tagWND porque no podemos evitar punteros truncados. Y todavía no he encontrado un objeto que cumpla con esto; dado que _LARGE_UNICODE_STRING funciona bien en sistemas de 64 bits, pensé en usarlo también en sistemas de 32 bits.

Mi idea es que si podemos corromper el campo iFirstFree de la estructura tagPROPLIST (el índice del primer elemento liberado en la lista de propiedades) incrementando el valor del índice, podríamos hacer que apunte a una posición más lejana en el montón. Por ejemplo, podríamos hacer que apunte a la parte superior de tagWND.strName. La figura 11 muestra esta idea:

Para hacer el proceso más claro, ahora utilizamos dos estructuras tagPROPLIST: "Lista de propiedades A" para el UAF, y "Lista de propiedades B". Necesitamos saber exactamente qué partes del tagPROP insertado en "Lista de propiedades A" sobrescribirán el campo iFirstFree de "Lista de propiedades B". También debemos recordar que solo podemos escribir 8 bytes a la vez, por lo que debemos insertar al menos un tagPROP adicional en "Lista de propiedades A", la primera corrupción en la cabecera adyacente, y la segunda en los campos tagPROPLIST de "Lista de propiedades B". Esto puede variar según el sistema operativo y el tamaño del bloque, y en mi explotación debe adaptarse a varios diseños del montón. La figura 12 muestra cómo corrompemos. Nota que en la figura, el primer tagPROPLIST no se descompone en campos individuales, por lo que tagPROP[0] está implícito. Sin embargo, en el segundo tagPROPLIST, se descompone para mostrar nuestros miembros internos y así exponer nuestro proceso de corrupción. Por eso se muestra tagPROP[0]:

Primero notamos que si escribimos 8 bytes por cada tagPROP, eso significa que solo podemos controlar parcialmente la sobrescritura de iFirstFree (ya que proviene de los campos atomKey y fs), que es lo que más nos interesa. Porque podemos controlar completamente al menos dos bytes clave a través del valor de atomKey; cuando este valor es lo suficientemente pequeño, el campo fs se convierte en 0. Por lo tanto, usamos el valor de hData para sobrescribir cEntries con un valor razonable y utilizamos atomKey para hacer que iFirstFree apunte a tagWND, donde se encuentra el puntero strName.Buffer que queremos sobrescribir. Si no podemos sobrescribir directamente los valores de Length y MaximumLength, entonces podemos asignar previamente una cadena a la ventana objetivo para asegurarnos de que su longitud ya esté establecida en algún valor.

Veamos la estructura tagWND de 32 bits para ver qué obtenemos. Nota que esta vez uso el parámetro -b para que podamos calcular fácilmente el desplazamiento de Buffer en strName.

root@kitploit:~
kd> dt -b !tagWND 
win32k!tagWND 
    +0x000 head         : _THRDESKHEAD 
        +0x000 h        : Ptr32 
        +0x004 cLockObj : Uint4B 
        +0x008 pti      : Ptr32 
        +0x00c rpdesk   : Ptr32 
        +0x010 pSelf    : Ptr32 
    +0x014 state        : Uint4B 
    +0x014 bHasMeun     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x014 bDestroyed   : Pos 31, 1 Bit 
    +0x018 state2       : Uint4B 
[SNIPPED FLAGS] 
    +0x018 bWMCreateMsgProcessed    : Pos 31, 1 Bit 
    +0x01c ExStyle                  : Uint4B 
    +0x01c bWS_EX_DLGMODALFRAME     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x01c bUIStateFocusRectHidden  : Pos 31, 1 Bit 
    +0x020 style        : Uint4B 
    +0x020 bReserved1   : Pos 0, 16 Bits 
[SNIPPED FLAGS] 
    +0x020 bWS_POPUP    : Pos 31, 1 Bit 
    +0x024 hModule      : Ptr32 
    +0x028 hMod16       : Uint2B 
    +0x02a fnid         : Uint2B 
    +0x02c spwndNext    : Ptr32 
    +0x030 spwndPrev    : Ptr32 
    +0x034 spwndParent  : Ptr32 
    +0x038 spwndChild   : Ptr32 
    +0x03c spwndOwner   : Ptr32 
    +0x040 rcWindow     : tagRECT 
        +0x000 left     : Int4B 
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x050 rcClient     : tagRECT 
        +0x000 left     : Int4B
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x060 lpfnWndProc  : Ptr32 
    +0x064 pcls         : Ptr32 
    +0x068 hrgnUpdate   : Ptr32 
    +0x06c ppropList    : Ptr32 
    +0x070 pSBInfo      : Ptr32 
    +0x074 spmenuSys    : Ptr32 
    +0x078 spmenu       : Ptr32 
    +0x07c hrgnClip     : Ptr32 
    +0x080 hrgnNewFrame : Ptr32 
    +0x084 strName      : _LARGE_UNICODE_STRING 
        +0x000 Length   : Uint4B 
        +0x004 MaximumLength : Pos 0, 31 Bits 
        +0x004 bAnsi    : Pos 31, 1 Bit 
        +0x008 Buffer   : Ptr32 
    +0x090 cbwndExtra   : Int4B 
    +0x094 spwndLastActive  : Ptr32 
    +0x098 hImc         : Ptr32 
    +0x09c dwUserData   : Uint4B 
    +0x0a0 pActCtx      : Ptr32 
    +0x0a4 pTransform   : Ptr32 
    +0x0a8 spwndClipboardListenerNext   : Ptr32 
    +0x0ac ExStyle2                     : Uint4B 
    +0x0ac bClipboardListener           : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x0ac bChildNoActivate             : Pos 11, 1 Bit

El desplazamiento de strName es 0x84, el de Buffer es 0x8c. Sabemos que tenemos el índice del elemento de la lista tagPROP y que podemos escribir 8 bytes. Por lo tanto, podemos saber fácilmente si iFirstFree indexa en la dirección de desplazamiento 0x88 de la ventana que apunta a MaximumLength. Dado que solo podemos controlar dos bytes de Buffer, la operación de escritura no es factible, y dado que nuestro objetivo es usar esto como nuestra "primitiva" de lectura/escritura arbitraria, este resultado no es aceptable. Si escribimos un índice que apunte a 0x90, sobrescribiremos cbwndExtra, que no es lo que buscamos.

Revisando lo que podemos controlar al hacer el diseño del montón, y luego mirando si hay desplazamientos interesantes en tagWND que podamos controlar. En el desplazamiento 0x70 de tagWND está el campo pSBInfo. Este desplazamiento es divisible por 8, por lo que podemos sobrescribir este puntero con parte de los datos de hData de un tagPROP falso.

¿Podríamos sobrescribir pSBInfo para que apunte directamente a strName dentro de la misma estructura tagWND? Tal vez podamos usar la API de barras de desplazamiento para corromper strName y lograr nuestro objetivo.

pSBInfo apunta a una estructura tagSBINFO, que se mencionó en el proceso UAF inicial.

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

Recordamos que WSBflags no nos da mucho control, pero al menos sabemos que se establece en 1 cuando se habilita la barra de desplazamiento y en 0 cuando se deshabilita. Este campo de bandera no se puede establecer en un valor arbitrario; al invertir las funciones relevantes, descubrimos que si no se cambia el estado de la barra de desplazamiento, este campo permanece sin cambios. Los valores en la estructura tagSBDATA parecen más interesantes. Si leemos la documentación de SetScrollInfo(), podemos entender bien el significado de estos valores. Parece que podemos pasar parámetros a SetScrollInfo() a través de la estructura SCROLLINFO. Siempre que haya un control de barra de desplazamiento cerca de la ventana que queremos corromper, podemos manipular directamente el puntero pSBInfo (enviará un mensaje de ventana especial al control de ventana asociado). Obviamente podemos controlar incondicionalmente posMin y posMax. Los campos page y pos son un poco más problemáticos, ya que están restringidos a ciertos rangos; ahora intentamos evitarlos. Establecemos la bandera SIF_RANGE en la estructura SCROLLINFO para declarar dónde queremos establecer los valores mínimo y máximo.

Queremos sobrescribir Buffer con datos arbitrarios, lo que significa que queremos que posMin lo sobrescriba, por lo que podemos sobrescribir pSBInfo para que apunte a strName.MaximumLength. Siempre que no habilitemos o deshabilitemos la barra de desplazamiento, el campo WSBflags no se sobrescribirá, lo que garantiza la integridad de strName.MaximumLength. Esto significa que, independientemente de cómo establezcamos posMin (a través de nMin de SCROLLINFO), sobrescribirá Buffer, y posMax se escribirá en cbwndExtra. Esto no es un gran problema; en sistemas de 64 bits, podemos leer este valor de antemano y restaurarlo después. La idea general del desbordamiento se muestra en la figura 13:

Ahora ilustramos el proceso de ataque en sistemas de 32 bits con la figura 14. Antes de corromper cualquier dirección lejana del UAF, primero retrocedemos un paso y observamos los bloques de montón y el diseño del montón relevantes en la figura. Ahora que conocemos más detalles, lo siguiente que hay que hacer es obvio.

A continuación, insertamos dos elementos de propiedad en la "Lista de propiedades A", lo que corromperá los datos cercanos a "Lista de propiedades A" gracias a la corrupción UAF anterior, y al mismo tiempo haremos que iFirstFree de "Lista de propiedades A" apunte a pSBInfo. Nota que esto también corromperá el valor cercano de pSBInfo, pero podemos leerlo de antemano para restaurarlo después de la corrupción.

Insertamos un nuevo tagPROP en la "Lista de propiedades B", cuyo identificador atom es diferente de los existentes en la lista, de modo que este tagPROP se inserta en el siguiente índice libre. Esto hace que pSBInfo sea corrompido para que apunte a strName.MaximumLength dentro del mismo tagWND.

Finalmente, actualizamos la barra de desplazamiento para corromper el campo strName.Buffer (como se muestra en la figura 17):

Hay que saber que, a diferencia del caso de 64 bits, no podemos corromper el valor de longitud de strName. Podemos asignar previamente un texto de ventana de longitud adecuada para que su valor ya esté en uso. Luego, ya sea que queramos leer o escribir algunos datos desde una dirección del kernel, solo necesitamos llamar a SetScrollInfo() para operar en la ventana objetivo actualizando el valor de Buffer, y luego usar las API de texto de ventana para la operación.

¡Ahora tenemos una "primitiva" de lectura/escritura arbitraria reutilizable en sistemas de 32 bits!

Ejecución de código

Todo a partir de ahora asume que tenemos una "primitiva" de lectura/escritura arbitraria. Por lo tanto, cuando digo filtrar/leer algún valor o sobrescribirlo, me refiero a ejecutar la "primitiva" establecida en la fase de corrupción anterior. Esta "primitiva" es prácticamente la misma en ambas plataformas. Lo único que queda por hacer es sobrescribir un puntero de función y hacer que apunte a algún payload de ShellCode. El método común es sobrescribir el segundo elemento de nt!HalDispatchTable, que corresponde a la función HalQuerySystemInformation(). Luego, en modo usuario, llamamos a la función NtQueryInternalProfile() para activarlo.

Necesitamos conocer la dirección base de carga del módulo del kernel para calcular la dirección del kernel de nt!HalDispatchTable. Para ello, podemos llamar a NtQuerySystemInformation() en modo usuario para obtener información del módulo, que incluye la dirección base del módulo.

root@kitploit:~
// El valor de enumeración 11 representa SystemModuleInformation, que no está documentado... 
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);

Luego, cargamos ntoskrnl.exe en modo usuario para encontrar el desplazamiento de nt!HalDispatchTable, lo que nos da su dirección en el espacio del kernel. A continuación, usamos la "primitiva de lectura" para leer la dirección del kernel de HaliQuerySystemInformation() (que no está exportada) para modificarla, y luego usamos la "primitiva de escritura" para corromper el puntero de esa función para que apunte a la dirección del shellcode (puede ser en el espacio de direcciones del kernel o del usuario, como se detalla más adelante). El número de bytes leídos y escritos es el mismo en sistemas de 32 y 64 bits.

Bypass de SMEP

Windows 8 y 8.1 introdujeron soporte para SMEP, y algunos productos de seguridad también pueden habilitarlo en Windows 7, por lo que asumimos que está presente. SMEP nos impide ejecutar código en el espacio de usuario con privilegios de kernel, lo que hace que modificar la entrada de nt!HalDispatchTable para que apunte a una dirección de espacio de usuario sea inviable. Por lo tanto, queremos que apunte a una ubicación controlable dentro del espacio del kernel, cuyo código pueda modificar el registro cr4 para deshabilitar SMEP, para luego saltar al espacio de usuario. El artículo de MWR presenta un truco interesante en 64 bits: mapear las entradas de la tabla de páginas por sí mismo para obtener una dirección efectiva en el kernel para cualquier dirección virtual. Luego, usando la "primitiva de escritura", modificamos directamente la entrada de la tabla de páginas y cambiamos sus bits de máscara. Porté este truco a sistemas de 32 bits, pero hay diferencias entre sistemas con PAE habilitado y sin PAE.

Para lograrlo, el método obvio es mapear una dirección de usuario al espacio del kernel y luego usar la "primitiva de escritura" para que la entrada de la tabla de páginas tenga permisos de sistema en lugar de permisos de usuario. Esto es lo primero que debemos hacer. Cuando implementé esto en Windows 8, me encontré con un problema interesante. Windows 8 y posteriores, el administrador de escritorio (dwm.exe) escanea periódicamente las ventanas del escritorio y consulta sus nombres, no investigué la razón exacta. Esta operación no envía un mensaje a la ventana, pero tiene una función de manejo de ventanas correspondiente que llama a GetInternalWindowText(). Por lo tanto, el problema es que al usar el campo strName de la estructura de la ventana para sobrescribir la entrada de la tabla de páginas que contiene el shellcode, esa memoria pertenece a la tabla de páginas de nuestro propio espacio de proceso. Cuando dwm.exe obtiene el nombre de la ventana desde el kernel, la entrada de la tabla de páginas modificada hace que el kernel verifique si strName.Buffer es nulo, lo que indirectamente hace referencia a esa dirección, que es inválida y provoca un bloqueo del sistema.

Para satisfacer las consultas de dwm.exe, utilicé una dirección del kernel como payload. De esta forma, independientemente de cómo cargue el proceso actual cualquier cosa, la entrada de la tabla de páginas asociada a esa dirección siempre será válida. Elegí colocarlo en el montón del escritorio, porque podemos calcular su dirección del kernel mediante el método mencionado anteriormente. Todavía usamos el método de mapeo propio de la entrada de la tabla de páginas; en este punto, la tabla de páginas ya está marcada como de alto privilegio, pero no como ejecutable. Por lo tanto, solo necesitamos establecer el bit de ejecución.

Los pasos son los siguientes:

  1. Crear un búfer de texto de ventana que contenga el payload de la fase 1 y calcular su dirección del kernel.
  2. Usar la técnica de "mapeo propio de la entrada de la tabla de páginas" para calcular la entrada de la tabla de páginas correspondiente a la dirección base del kernel obtenida en la fase 1.
  3. Usar la "primitiva de escritura" para establecer el bit de máscara ejecutable de la entrada de la tabla de páginas.
  4. Sobrescribir nt!HalDispatchTable para que apunte a la dirección del kernel encontrada en la fase 1.
  5. Llamar a NtQueryInternalProfile() para saltar al payload.
  6. Deshabilitar la máscara SMEP en cr4 y saltar al payload en espacio de usuario de la fase 2.
  7. Ejecutar el payload en espacio de usuario de la fase 2 para escalar privilegios y retornar.
  8. Restaurar la máscara SMEP en cr4 para evitar la detección de PatchGuard y realizar la limpieza correspondiente, luego retornar.

Bypass de la sandbox de integridad baja

En Windows 8.1 hay otro problema: NtQuerySystemInformation() verifica el SID de integridad baja, lo que significa que solo los procesos de integridad media o superior pueden obtener la dirección base del kernel. Esto se puede solucionar fácilmente con el conocido truco de sidt. Guardamos la dirección de la IDT en modo usuario (no requiere verificación de permisos) y luego usamos la "primitiva de lectura" para leer el índice de IDT que necesitamos, que generalmente apunta al espacio de direcciones del kernel, por lo que podemos filtrar la dirección del kernel del manejador de interrupción y luego buscar el desplazamiento en el archivo PE del módulo del kernel.

Una vez que tenemos la dirección base de carga del kernel, podemos calcular la dirección de nt!HalDispatchTable.

El método habitual es cargar el archivo ntoskrnl.exe e interpretar sus desplazamientos de símbolos, sumándolos a la dirección base de carga del kernel filtrada. Sin embargo, en una sandbox de modo mejorado esto no funciona, ya que hay restricciones del sistema de archivos que impiden leer C:\windows\system32\ntoskrnl.exe. Para evitar esta limitación, utilizamos nuestra "primitiva de fuga" para analizar los desplazamientos de los símbolos necesarios desde la imagen PE del kernel en memoria.

Conclusión

Eso es todo el material. Gracias por leer. Usando las técnicas presentadas en este documento, pude lograr una explotación estable en sistemas de 32 y 64 bits, incluyendo XP, Vista, 7, 8, 8.1 y Server 2012. En Windows 2003 y 2008 no es posible por defecto, ya que no se pueden hookear las devoluciones de llamada en modo usuario, por lo que no se pueden atacar estos sistemas a menos que se cumplan las condiciones requeridas. El proceso de explotación es bastante complejo y hay muchos obstáculos que superar, pero esto también nos ha brindado mucha diversión y cosas que aprender; muchos de los métodos viables y los resultados de investigación utilizados en este documento ya han sido mencionados en artículos de otros investigadores. Hasta donde sé, solo hay una mitigación que puede evitar la explotación de win32k.sys, que es la que usa la sandbox de Google Chrome, que bloquea efectivamente las llamadas al sistema del kernel de win32k en tiempo de ejecución. Espero cualquier mejora o comentario; si tienes alguna deficiencia con respecto a algunas de las técnicas que he presentado, dímelo y actualizaré este documento. Puedes contactarme a través de Twitter @fidgetingbits o por correo electrónico a [email protected].

Descargar herramienta