
Informe técnico y PoC para CVE-2024-6769, que combina el secuestro de DLL con el envenenamiento de la caché de activación para escalar de integridad media a alta en sistemas Windows.
Este blog trata sobre dos bugs encadenados: la primera etapa es un bug de secuestro de DLL causado por el remapeo de la unidad ROOT y la segunda es un bug de envenenamiento de la caché de activación gestionado por el servidor CSRSS.
La primera etapa se presentó en detalle en Ekoparty 2023 en la presentación llamada “I'm High” por Nicolás Economou de BlueFrost Security. Él explicó cómo explotar la vulnerabilidad que, en ese momento, Microsoft aún no había parcheado. Esto permitía elevar a un usuario MEDIUM INTEGRITY para que tuviera PRIVILEGIOS ALTOS limitados, pero sin el acceso completo para ser un Administrador total.
La segunda etapa no se presentó en esa conferencia, aunque se sugirieron algunos pasos para comenzar a investigarla.
Para empezar, revisaremos la primera etapa para proporcionar contexto introductorio. A partir de ahí, profundizaremos en mi investigación sobre la segunda etapa, entrando en los detalles de cómo lograr la escalada completa desde HIGH INTEGRITY limitado hasta Administrador total. Esto incluye un PoC funcional completo para ambas etapas en todas las versiones de Windows, que se ha probado con éxito en Windows 10, Windows 11, Windows Server 2022 y Windows Server 2019 con todas las actualizaciones aplicadas.

El único requisito para esta etapa es que el proceso inicial comience en un nivel MEDIUM INTEGRITY y que el usuario pertenezca al grupo Administradores.
La primera etapa de explotación se puede resumir en los siguientes pasos:
Por ejemplo: remapear el disco de
"C:\" a "C:\users\public"
Esto también remapeará la carpeta system32 de
"C:\windows\system32" a "C:\users\public\windows\system32"
Uno de estos programas afectados es CTFMON, que se ejecuta a un nivel de integridad HIGH INTEGRITY, pero sin privilegios de Administrador.
Normalmente, intenta cargar el módulo llamado MsCtfMonitor.dll desde la carpeta system32 real, pero como la unidad ROOT fue remapeada, busca MsCtfMonitor.dll en nuestro system32 falso controlado, donde podemos crear y colocar una DLL manipulada con el mismo nombre.
En este punto, al colocar nuestra versión de MsCtfMonitor.dll en la carpeta system32 falsa, se llama a su función DoMsCtfMonitor y ejecuta nuestro código a un nivel de integridad HIGH INTEGRITY.




Al mismo tiempo, podemos corroborar que el proceso, a pesar de estar en un nivel HIGH INTEGRITY, no tiene privilegios de Administrador:


En su presentación en Ekoparty, Nicolás sugirió los siguientes pasos para completar la explotación:


Aunque esto parece simple, requiere mucho tiempo de ingeniería inversa y depuración.
Al indagar un poco en la historia de este vector de ataque, quedó claro que el envenenamiento de la caché de contextos de activación se ha utilizado en algunos exploits. Por lo tanto, vale la pena aprender cómo se ha realizado la explotación anteriormente para obtener contexto e ideas adicionales. Los detalles de esta explotación están disponibles en el artículo de Zero Day Initiative, Activation Context Cache Poisoning: Exploiting CSRSS for Privilege Escalation.
La caché de activación se utiliza cuando un programa va a cargar una biblioteca que requiere una versión específica.
Por ejemplo, si una aplicación va a cargar C:\Windows\System32\comctl32.dll, no hay garantía de que la comctl32.dll en esa ubicación sea la versión que la aplicación necesita. Este es un caso de uso básico de la caché de contextos de activación. El programa puede enviar una solicitud al servidor CSRSS para procesar una nueva entrada de contexto de activación que se agregará a la caché, de modo que el programa pueda cargar la versión específica de la biblioteca necesaria.
Para este propósito, se utiliza el llamado manifiesto, que está en formato XML. Normalmente está incrustado como recurso en un archivo EXE o DLL. Alternativamente, Windows buscará un archivo de manifiesto en la misma carpeta donde se encuentra el ejecutable del programa.
La URL mencionada anteriormente tiene algunos ejemplos de archivos de manifiesto utilizados por exploits antiguos, como engañar al sistema para que cargue la biblioteca advapi32.dll desde un directorio controlado por el atacante, al que se llegaba mediante la técnica de PATH TRAVERSAL.

Por supuesto, algunos vectores de ataque utilizados fueron parcheados y se descubrieron algunas técnicas nuevas. Además, en el parche de octubre de 2022 para Windows 11 22H2, se agregó una nueva comprobación.
Después de que se implementó este parche, la comprobación al registrar un contexto de activación (ACTX) solo se puede omitir si el proceso que agrega la nueva entrada a la caché tiene el mismo RID o un RID mayor que el proceso que la usará.
En winnt.h podemos ver los valores RID:

La propuesta para omitir esta comprobación es crear una solicitud con un contexto de activación desde el proceso CTFMON donde se ejecuta la DLL manipulada. Esta DLL manipulada tiene RID=0x3000 y, después de que la entrada se agregue a la caché, TCMSETUP con RID=0x3000 cargará tapi32.dll.
Durante mi intento de seguir los pasos, probé todas las combinaciones posibles para registrar el ACTX usando CreateActCtx. Esto resultó imposible, ya que siempre había una comprobación que lo evitaba.
Es importante señalar que esta función se encuentra en el espacio de usuario (userland) y es exportada por kernel32.dll. Las comprobaciones se pueden evitar parcheando la DLL en memoria, lo cual no es muy elegante, pero es posible y debería funcionar.

La diapositiva de la presentación de Nicolás sugiere usar LOW LEVEL. Sin embargo, al notar la carita guiñando el ojo, quedó claro que usar CreateActCtx no es la mejor opción al explotar este bug sin un parche.

Una llamada a procedimiento local avanzada (ALPC, por sus siglas en inglés) es un mecanismo de comunicación entre procesos que se utiliza para enviar mensajes a alta velocidad dentro del sistema operativo Windows. A diferencia de la API estándar de Windows, ALPC no está disponible directamente para las aplicaciones. En cambio, es un mecanismo interno al que solo pueden acceder los componentes del sistema operativo Windows. (Y nosotros.)
Al investigar más a fondo, noté que algunos exploits antiguos de envenenamiento de caché usaban ALPC para comunicarse directamente con el servidor. Un ejemplo se puede ver en el artículo de Philip Tsukerman, Activation Contexts—A Love Story.
La función CsrClientCallServer implementa la interfaz ALPC entre los procesos Win32 y el proceso CSRSS.
Por lo tanto, se debe intentar realizar una llamada al proceso CSRSS, que actúa como servidor, mediante CsrClientCallServer.
Mientras buscaba ejemplos en exploits antiguos, encontré una página en Packet Storm sobre un problema relevante de desbordamiento de búfer en el montón.
Cuando se llama al servidor CSRSS con el paquete correcto, este se recibe en la función BaseSrvSxsCreateActivationContextFromMessage, que pertenece al módulo sxssrv.dll.
La función tiene un solo argumento: el puntero al paquete recibido. Para hacerle ingeniería inversa, creé una estructura personalizada TotalMessage.
El paquete de la estructura TotalMessage tiene sus primeros 0x40 bytes de HEADER, seguidos del Activation Context Message incrustado, cuya estructura es _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG.
La estructura TotalMessage se puede ver a continuación:

Y aquí está la estructura _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG:

Dentro de esta estructura hay seis UNICODE_STRING correspondientes al idioma o CultureFallbacks, AssemblyDirectory, TextualAssemblyIdentity, AssemblyName, y dos estructuras _BASE_MSG_SXS_STREAM que contienen un UNICODE_STRING cada una.
A continuación se muestra la estructura _BASE_MSG_SXS_STREAM:

Dada la dificultad de crear un paquete válido que el servidor acepte, vale la pena detallar cómo hacerlo.
El valor del campo Flags dentro de _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG es muy importante, ya que hay muchas combinaciones. Sin el valor de flag correcto, no se puede aprovechar el bug.
Por ejemplo, tome el código de mi MsCtfMonitor.dll. Después de muchos intentos, concluí que el único valor de flags correcto para esta explotación del bug es 0x41:

La combinación de diferentes valores podría dar como resultado un valor de flag de ruta incorrecto:

La misma estructura TotalMessage tendrá un encabezado con tamaño=0x40 bytes. Los 0x1f8 bytes restantes están reservados para la estructura _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG:
struct TotalMessage
{
signed __int64 pad[8];
_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG message;
};
El tamaño para la asignación es 0x40+0x1f8:

Luego armé las cadenas y realicé un contexto de caché de activación para tapi32.dll. Esta es una DLL que se usa muy raramente y que carga un proceso llamado TCMSETUP. Tiene un nivel de integridad de PRIVILEGIOS ALTOS (RID=0x3000) con los mismos privilegios que un Administrador.

En el código de mi DLL, se llama a la función CaptureUnicodestring. Esta termina llamando a CsrCaptureMessageString:
NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString,
PCWSTR String, ULONG Length = 0) {
if (Length == 0) {
Length = lstrlenW(String);
}
return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length * 2,
Length * 2 + 2, OutputString);
}
Este paso es necesario para preparar el paquete correctamente, permitiendo que el sistema copie las cadenas de mi paquete al proceso CSRSS. Esto mantiene las cadenas válidas y reemplaza mis punteros por punteros válidos en su contexto.
También agregué un manifiesto XML incrustado, con el idioma "Tasks". Este es un idioma inexistente, pero será la clave de la explotación (Créditos a Nico por esto):

Otro detalle importante en mi código es cuando se crea CaptureBuffer. La función CsrAllocateCaptureBuffer tiene un argumento que define cuántas UNICODE_STRING debe gestionar y copiar al servidor.
En mi caso usé “4” cadenas:

El argumento con el valor “4” se muestra a continuación:

Para llegar al servidor de activación, la función CsrClientCallServer enviará mi paquete desde mi MsCtfMonitor.dll con el mismo ApiNumber 0x1001001E que los exploits antiguos mencionados anteriormente.
El blog de Geoff Chappell proporciona más detalles sobre CsrClientCallServer:

Aquí está la llamada a CsrClientCallServer:

Y aquí está el paquete a enviar, construido en mi DLL:

El valor Manifest.Offset apunta a mi manifiesto XML incrustado:

Un comando interesante para registrar el proceso de activación es sxstrace, que se usa en una consola de administrador dentro del objetivo.
Este comando habilita el seguimiento y guarda los resultados del registro en sxstrace.etl. (Presione ENTER para finalizar el seguimiento.)
sxstrace trace -logfile:sxstrace.etl
El archivo sxstrace.etl sin procesar se puede convertir a un formato legible:
sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt
Si el paquete es correcto, debería llegar a la función BaseSrvSxsCreateActivationContextFromMessage en el módulo sxssrv del proceso csrss. Por lo tanto, al depurar el kernel remoto, el contexto debe cambiarse a este proceso. Luego, los símbolos del modo usuario deben recargarse para poner un breakpoint en ella:

Usé IDA PRO para depurar el kernel con el plugin de Windbg:

Cuando se detiene en BaseSrvSxsCreateActivationContextFromMessage, RCX apuntará a la estructura TotalMessage:

Después de los primeros 0x40 bytes de HEADER (rellenados por el sistema con algunos valores como el PID del proceso cliente, etc...), se puede ver mi mensaje de activación, que pertenece a la estructura _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG:

Nótese que los punteros a las cadenas no tienen el mismo valor que cuando los envié:

Pero apuntaban correctamente a las cadenas:

Cuando el paquete se envió del cliente al servidor, el sistema copió las cadenas de mi proceso al proceso CSRSS y cambió los punteros en mi paquete para que fueran válidos en su contexto.
Después de eso, la función BaseSrvSxsCreateActivationContextFromMessage comprueba si las cadenas son válidas.

En un bucle comprueba seis cadenas, pero pasa la comprobación perfectamente. En mi caso solo pasé cuatro cadenas y las otras dos son cero.
Después de otras comprobaciones menores, llama a BaseSrvSxsCreateActivationContextFromStructEx, que es la función más importante en el proceso de activación:

Una vez que se llega a BaseSrvSxsCreateActivationContextFromStructEx, r8 apuntará a _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG, que es el mensaje de activación:

Evalúa el valor de flags. En mi caso, el valor era 0x41 frente a 0xD:

La función test se puede omitir usando la opción de flag que corresponde a validar la arquitectura del procesador (1).

Después, obtiene el RID del proceso que llama y lo almacena para una comparación posterior. En este caso, el RID es 0x3000, ya que CTFMON tiene un nivel HIGH INTEGRITY.

La parte más importante de esta función es la llamada a BaseSrvActivationContextCacheLookupEntry:

Busca en la caché de contextos de activación para determinar si hay alguna entrada para tapi32.dll.
Llama a una función llamada BaseSrvActivationContextCacheCompareEntries, que compara ciertas partes de la entrada del mensaje de activación con todas las entradas existentes en la caché:

Compara el valor LastWriteTime enviado en mi paquete con el mismo valor en todas las entradas.
Anteriormente había calculado este valor usando GetFileTime en tapi32.dll y lo envié dentro de mi paquete de activación:

Como no hay ninguna entrada para tapi32.dll, las comparaciones no coincidirán. Como se esperaba, devuelve el error 0xC0000225. Después de eso, comprobará mi ACTX para ver si es adecuado para agregarse a la caché:

El servidor necesita leer mi manifiesto XML incrustado y la dirección Manifest.Offset que apuntaba a él. Sin embargo, en este nuevo contexto, todavía no es un puntero válido. Vale la pena poner un breakpoint en este valor para ver cómo y cuándo se lee mi manifiesto XML incrustado usando ese valor.
Para verificar dónde CSRSS lee mi manifiesto XML incrustado que se envió en mi solicitud ACTX, se deben poner breakpoints en Manifest.Offset. Además, se deben agregar breakpoints cada vez que se detenga, si copia a otra dirección.

Se detiene en el breakpoint al leer la dirección del valor Manifest.Offset.
Usará esta dirección para leer mi manifiesto XML incrustado desde el proceso CTFMON mediante NtReadVirtualMemory, ya que la dirección colocada en el campo Manifest.Offset pertenece a ese contexto:

Mi manifiesto XML incrustado se lee y se copia al búfer de destino:

Cambie al contexto del proceso CTFMON y verifique que mi manifiesto XML incrustado está en la dirección Manifest.Offset que envié anteriormente. En mi caso, era 0x7ff93a261470.

La lectura del manifiesto XML incrustado se llama desde SxSGenerateActivationContext. Como no se encuentra en ninguna entrada válida de la caché, intenta “generarlo” usando el manifiesto incrustado:

A partir de ahí, comienza a analizar mi manifiesto XML incrustado.
Mirando la última pila de llamadas, decidí poner un breakpoint en la llamada a RtlReadOutOfProcessMemoryStream para detenerme cuando el búfer estuviera completamente lleno.

Ahora se puede colocar un breakpoint de acceso en la cadena "Tasks" para detenerse cuando el servidor la lea o procese.

Aquí está la cadena tasks dentro del manifiesto XML incrustado:

Se detiene varias veces leyendo y copiando:

Se detiene en CharEncoder::wideCharFromUtf8 cuando convierte la cadena “tasks” a carácter ancho:

Luego se detiene en el analizador XML:

Continúa analizando los atributos XML, como implica el nombre de la función parseAttributes.

Luego, se detiene en memcpy llamado desde ValidateElementAttributes:

Se puede colocar otro breakpoint donde copia:

Valida el atributo de idioma, como implica el nombre de la función SxspValidateLanguageAttribute:

Se detiene nuevamente en memcpy, pero esta vez es llamado desde SxspCreateAssemblyIdentityfromIdentityElement:

Una vez más, se detiene en memcpy, esta vez llamado desde SxsInsertAssemblyIdentityAttribute+0xc48:
Luego se detiene en SxsInsertAssemblyIdentityAttribute:

Llama a memcpy una última vez, en este caso desde BufferedStream::prepairForInput:

Luego lee la cadena tasks aquí:

Después, la lee desde aquí:


Continúa leyendo desde aquí:


Los nombres de estas funciones llamaron mi atención. En el nombre ProbingCandidate se incluyen las mismas palabras (probing manifests) utilizadas en el archivo de registro txt de SXS.

Se detiene nuevamente aquí:

A continuación, usa GetFileAttributesExW para comprobar si el primer archivo mencionado en el registro txt de SXS existe. Como no existe, devuelve cero.

El orden de la comprobación de archivos se puede ver en el archivo de registro:

El segundo archivo no existe porque es la ruta en la carpeta tasks hacia tapi32.dll:

A partir de ahí, parece estar “probando” la existencia de tapi32.manifest en tasks:

Luego llega a CProbedAssemblyInformation::ProbeManifestExistence:


Comprueba si mi archivo de manifiesto existe en la carpeta tasks. Como sí existe, no devuelve ningún error:

Bueno, se encontró el tapi32.manifest en la carpeta "tasks".
El servidor fue forzado a buscar un archivo de manifiesto en la subcarpeta "tasks" de system32 por mi manifiesto XML incrustado con el valor de idioma "tasks" dentro:


Si se siguen colocando puntos de interrupción para ver dónde usa la ruta, se detiene en EncodingStream::Read, donde se lee el contenido del archivo tapi32.manifest.

A continuación, analizará el contenido del archivo TAPI32.manifest. Si hay un error, lo mostrará en el registro SXS TRACE, lo que facilita su corrección.

Si mi archivo TAPI32.manifest se analiza correctamente, volverá a BaseSrvSxsCreateActivationContextFromStructEx sin errores. Esto evita que se imprima un mensaje con la cadena FAILED.
En mi caso, la generación del contexto de activación fue exitosa, usando mi archivo TAPI32.manifest.


Luego llegué a la llamada donde mi entrada se insertará en la caché.
Se pasa sin ningún problema, devolviendo cero. Este es el valor correcto y la entrada con el TAPI32.manifest elaborado se inserta correctamente.

Mi entrada se incluye en la caché de activación y el servidor devuelve una respuesta OK a la llamada de la DLL desde CTFMON.
El archivo de registro txt muestra el proceso completo.
Lee el manifiesto XML incrustado. Como su idioma es "Tasks", busca un nuevo archivo de manifiesto en la subcarpeta "Tasks" de system32, igual que buscaría un manifiesto en la subcarpeta de system32 llamada "en-us" si el idioma estuviera configurado como "en-us".

El archivo de registro txt de SXS muestra el mensaje “Activation Context generation succeeded”!

Después de que mi entrada ACTX se agregue a la caché, si se ejecuta tcmsetup.exe, cargará tapi32.dll y debería usar mi archivo de manifiesto para cargar imm32.dll.
Sin embargo, no es tan simple, ya que no puede cargar imm32.dll porque hay algunas comprobaciones que pueden impedir que se cargue.
Las comprobaciones se realizan en una llamada posterior a la misma función BaseSrvSxsCreateActivationContextFromStructEx, así que elimina todos los puntos de interrupción y deja solo uno en ella.
Desde ahí podemos ejecutar TCMSETUP.EXE desde una consola, aunque mi PoC ejecuta TCMSETUP desde MsCtfMonitor.dll una vez completado el envenenamiento de la caché de activación:

Se detiene en el punto de interrupción muchas veces. Cada vez que se detiene, observa la estructura a la que apunta r8 para ver si corresponde a una solicitud relacionada con tapi32.dll.

Después de muchas detenciones por otros módulos, aparece una solicitud para TCMSETUP.exe:

Vemos en la pila de llamadas que proviene del momento en que se crea el proceso. Se llama para comprobar si tiene alguna entrada en la caché de activación para TCMSETUP.
Déjalo en ejecución hasta que llegue la llamada para TAPI32.dll. Antes de que esto ocurra, habrá varias llamadas para TCMSETUP.

Finalmente, el paquete que llega debe ser muy similar al que hice anteriormente desde mi DLL cuando inserté la entrada en la caché. Sin embargo, ahora se detiene cuando TCMSETUP intenta cargar TAPI32.dll.

En este punto noté algunos valores importantes en este paquete.

Extendiendo desde el inicio de la estructura _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG 0x40 bytes hacia arriba, se asigna la estructura TotalMessage. El PID del proceso que realiza la solicitud de TAPI32.dll es TCMSETUP, ya que quiere cargar la DLL.

Cambiando el contexto al proceso TCMSETUP, se puede ver que el valor Manifest.Offset apunta a algún manifiesto XML incrustado.


Abre tapi32.dll en NOTEPAD para ver que el manifiesto XML incrustado recibido es el mismo que el incluido en el archivo.
TCMSETUP lee el recurso del archivo previamente para leer el manifiesto y colocarlo en el paquete como manifiesto XML incrustado.
Después de eso, la comparación se realiza nuevamente mediante la función BaseSrvActivationContextCacheCompareEntries, que se llama desde BaseSrvSxsCreateActivationContextFromStructEx. Ahora mi entrada para tapi32.dll también está en la caché.

BaseSrvActivationContextCacheCompareEntries se llama dentro de un bucle para comparar la solicitud real con cada entrada de la caché de contexto de activación (como la mía).
Al principio compara ambos valores LastWriteTime; como son iguales, continúa comparando más valores.
Este valor LastWriteTime es crucial. Si hay valores diferentes, descartará mi entrada en caché y mi imm32.dll no se cargará.
Continúa y se detiene en la siguiente comprobación.

Ahora comprueba el valor ResourceName, que debe ser 0x7c en ambos.

Luego compara el idioma del paquete ACTX real, que es "en-us", con el idioma de mi entrada en caché. El idioma de mi entrada en caché también es "en-us".

Mi paquete tiene el mismo valor de idioma:

Luego compara la arquitectura del procesador, que en este caso será 9 en ambos casos:


Luego compara ambos Manifest.path.

Construí la misma ruta sin codificación fija usando el valor del Directorio del Sistema:

Luego compara el AssemblyDirectory, que también es el mismo:


Si todas las comparaciones son correctas, devuelve cero. Esto significa que encontró mi entrada en la caché de activación y se utilizará.
Recuerda que cuando envié mi solicitud por primera vez para agregar la entrada, la comparación devolvió un error ya que no había ninguna entrada en la caché para TAPI32.dll. Como mi entrada se agregó previamente, ahora devuelve cero.
Después de eso, compara los RID de TCMSETUP con los de CTFMON; como ambos tienen RID = 0x3000, el proceso continúa.
Una explicación completa del parche de RID está disponible en un blog del Zero Day Initiative.

Este es el código para este parche:

R15 tiene el RID del llamador TCMSETUP = 0x3000 y el buffer ha almacenado el RID=0x3000 del proceso CTFMON.
Como se mencionó anteriormente, Microsoft agregó este parche de comprobación de RID en octubre de 2022.
Una vez implementado ese parche, si se intenta agregar la entrada de tapi32.dll a la caché usando el mismo MsCtfMonitor.dll desde un proceso de NIVEL DE INTEGRIDAD MEDIO (0x2000), la entrada se agregará a la caché, pero fallará. Esto se debe a que se almacena el RID del proceso llamador 0x2000 y cuando se intenta ejecutar TCMSETUP con RID=0x3000 para cargar imm32, se comparan los RID y la entrada se elimina.
En ese caso hipotético, R15 tendrá el RID=0x3000 del proceso TCMSETUP que solicitó cargar tapi32.dll, la variable "buffer" habrá almacenado el RID=0x2000 del proceso que agregó la entrada a la caché, que tiene un NIVEL DE INTEGRIDAD MEDIO.

En las versiones más recientes de Windows, el envenenamiento de la caché no funcionará si el proceso que solicita que se agregue la entrada es inferior al ejecutor y la entrada se elimina. Las versiones anteriores publicadas antes de este parche funcionarán sin problemas con cualquier RID.

Volviendo a este caso, la comprobación de RID se supera y ambos procesos tienen el mismo RID=0x3000. En consecuencia, la entrada no se elimina y continúa sin errores.
El servidor devuelve la respuesta a TCMSETUP. Cuando carga tapi32.dll, usará mi entrada con el archivo tapi32.manifest, que cargará imm32.dll desde la carpeta tasks.
Este es el camino completo desde LoadLibrary hasta donde TCMSETUP realiza la solicitud a la caché de activación
al cargar tapi32.dll.

BasepCreateActCtx es quien realiza la solicitud al servidor CSRSS. Hay que intentar ver cuándo termina cargando el módulo IMM32.dll.
Mirando kernel32.dll, llama a CsrBasepCreateActCtxCommon. Dentro hay una llamada al servidor similar a la realizada desde mi DLL para insertar mi entrada en la caché.

Usa el mismo ApiNumber que la mía.
Al ejecutar TCMSETUP, se puede colocar un punto de interrupción allí cuando regrese del servidor, después de que se acepte mi archivo tapi32.manifest.

Esta es la pila de llamadas completa hasta que se produce la llamada al servidor en CsrBasepCreateActCtxCommon.

Se colocan puntos de interrupción en el retorno de algunas funciones de la pila de llamadas.

Cuando se detiene, se puede observar que imm32.dll se cargó desde la carpeta "tasks":

La validación se puede realizar con PROCESS MONITOR para confirmar que TCMSETUP carga IMM32.dll desde la carpeta "tasks".

El proceso CMD recién ejecutado tiene privilegios HIGH.

Además, tiene los mismos privilegios que Administrator.
Con estos privilegios ahora podemos instalar cualquier programa que requiera elevación a administrador y escribir en cualquier carpeta. Por ejemplo, escribir en SYSTEM32 o en cualquier carpeta de instalación de programas, como se puede ver en el VIDEO DEMO a continuación.
Aquí están los privilegios previos a la explotación (nivel de integridad Medium, no Administrador):

Y ahora aquí están los privilegios después de la explotación (nivel de integridad High, Administrador COMPLETO):

En este punto, es una buena oportunidad para elevar fácilmente a SYSTEM, colocando alguna DLL elaborada en una carpeta del sistema.


Mira el video aquí y la prueba de concepto funcional está aquí
Envié un mensaje ACTX elaborado al servidor CSRSS.
Este mensaje ACTX tenía un manifiesto XML incrustado con un offset que apuntaba a él.
Cuando el servidor lo recibió, usó ese offset para leer el manifiesto XML incrustado desde el contexto del proceso CTFMON.
El manifiesto XML incrustado fue analizado. Si se aceptaba, intentaba cargar un segundo manifiesto externo desde una carpeta externa.
La carpeta a leer dependía del campo de idioma en el manifiesto XML incrustado controlado por mí.
En mi caso, el manifiesto XML incrustado tenía "tasks" como idioma. Por esa razón, buscó un manifiesto externo en el subdirectorio "tasks" de system32 y lo encontró.
Analizó el archivo tapi32.manifest creado por mí y lo aceptó, lo que le permitió cargar la IMM32.dll externa desde la misma carpeta "tasks".
Gracias a Nicolas Economou, ya que su presentación fue el punto de partida de mi investigación y de la publicación de esta entrada de blog.
Ricardo Narvaja