Análisis del proceso de ejecución de código remoto en Windows mediante archivos .LNK

Durante el mes de febrero Microsoft lanzó parches para nada más y nada menos que 99 CVEs. Entre estas vulnerabilidades una de ellas destacó particularmente: la identificada con CVE-2020-0729, basada en la ejecución de código remoto.

Uno de los aspectos que convierte a esta vulnerabilidad en algo tan llamativo es que, históricamente, los exploits para vulnerabilidades encontradas en archivos .LNK han sido usados para distribuir malware (como, por ejemplo, Stuxnet) y en la mayoría de los casos, el mero hecho de abrir una carpeta que contenga un archivo .LNK malicioso es suficiente para explotar la vulnerabilidad.

Inicio del análisis

Al iniciar el análisis los investigadores de TrendMicro se dieron cuenta de que al lanzar los parches, Microsoft no había incluido ninguna de las actualizaciones que le correspondían a las DLL normalmente asociadas al procesamiento de archivos LNK, como por ejemplo shell32.dll y windows.storage.dll. No obstante, sí que se podía encontrar la DLL StructureQuery.dll, en base a la que se han nombrado explícitamente vulnerabilidades como la que lleva el identificador CVE-2018-0825. Los investigadores descubrieron que los archivos LNK y esta DLL están relacionados en cuanto a que esta última es usada por Windows Search, que es precisamente donde los archivos LNK y la DLL StructureQuery vienen a encontrarse.

Características de los archivos .LNK

Los archivos .LNK se conocen principalmente por contener estructuras binarias que crean un atajo a un archivo o una carpeta, pero una funcionalidad menos conocida es que pueden contener una búsqueda almacenada. Normalmente, cuando un usuario busca un archivo en Windows 10, se muestra la opción «Buscar herramienta» para permitir al usuario afinar la búsqueda y seleccionar opciones avanzadas para la misma. Esta opción también permite a los usuarios guardar la búsqueda existente para reutilizarla en un futuro, lo cual genera un archivo XML con la extensión «.search-ms» al guardarlo, un formato de archivo que no se encuentra totalmente documentado.

Fuente de la imagen: Zero Day Initiative

Sin embargo, esta no es la única forma de guardar una búsqueda. Si se hace clic y se arrastra a otra carpeta el icono de resultados de búsqueda de la barra de direcciones (resaltado en la siguiente imagen), se crea un archivo .LNK que contiene una versión serializada de los datos que, de otra manera, almacenaríamos en un archivo XML, específicamente del tipo antes mencionado: «search-ms«.

Fuente de la imagen: Zero Day Initiative

Considerando estos aspectos, los investigadores realizaron un análisis del parche para StructureQuery usando BinDiff.

Fuente de la imagen: Zero Day Initiative

Tan solo una de las funciones cambia: StructuredQuery1::ReadPROPVARIANT(), y parece hacerlo de manera considerable, según se puede observar en las gráficas de flujo de la siguiente imagen:

Fuente de la imagen: Zero Day Initiative

Para saber lo que hace esta función en un archivo .LNK es necesario analizar las estructuras que podemos encontrar en un archivo .LNK de una búsqueda almacenada.

Los archivos link de Windows shell tienen múltiples componentes esenciales y opcionales. Cada archivo link de la shell debe tener, al menos, una cabecera de shell link (Shell Link Header), la cual tiene el siguiente formato:

Fuente de la imagen: Zero Day Initiative

El campo LinkFlags clarifica la ausencia de estructuras opcionales así como varias opciones como, por ejemplo, si las cadenas del archivo link shell están codificadas en Unicode o no. Lo que aparece a continuación es un ejemplo del campo LinkFlags:

Fuente de la imagen: Zero Day Initiative

La flag HasLInkTargetIDList, configurada en la mayoría de las ocasiones, se representa con la posición «A», el bit menos significativo del primer byte del campo LinkFlags. Si se configura, la estructura de LinkTargetIDList debe seguir a la cabecera de shell link. La estructura LinkTargetIDList especifica el objetivo del link y tiene la siguiente forma:

Fuente de la imagen: Zero Day Initiative

La estructura IDList contiene el formato de una lista de ID de objetos persistente:

Fuente de la imagen: Zero Day Initiative

El ItemIDList tiene la misma función que la ruta de un archivo, donde cada estructura de ItemID se corresponde con el componente de una ruta en una jerarquía. ItemID puede hacer referencia a sistemas de archivos de carpetas reales, carpetas virtuales como el Panel de Control o Búsquedas almacenadas, u otras formas de datos insertados que sirven como «atajo» para ejecutar funcionalidades específicas. Para la vulnerabilidad de la que hablamos resulta de particular importancia las estructuras ItemIDList e ItemID, presentes en un archivo .LNK que contiene una búsqueda almacenada.

Cuando un usuario crea un atajo que contiene información sobre una búsqueda, el archivo resultante contiene una estructura IDList que comienza con una Carpeta ItemID Delegada, seguida de lo denominado como «User Property View ItemID» para hacer búsquedas. En general, ItemID comienza de la siguiente manera:

Fuente de la imagen: Zero Day Initiative

El valor de los dos bytes al comienzo de 0x0004 es usado en combinación con ItemSize e ItemType para ayudar a determinar ItemID. Por ejemplo, si ItemSize es 0x14 e ItemType es 0x1F, los dos bytes de 0x0004 son comprobados para ver si su valor es mayor que el de ItemSize. De ser así, se asume que lo que queda de ItemID será un Identificador Único global de 16 bytes (GUID, por sus siglas en inglés). Esta es la estructura típica del primer ItemID encontrado en un archivo .LNK que apunta a otro archivo o carpeta. Si ItemSize es mayor que el tamaño requerido para contener una GUID pero menor que los bytes de 0x0004, los datos restantes tras el GUID se consideran ExtraDataBlock, el cual tiene un campo con un tamaño inicial de 2 bytes seguido por dicha cantidad de bytes.

En el caso de una carpeta ItemID delegada, esos 2 mismos bytes se corresponden con otro campo para el resto de la estructura, llevando a lo mostrado en la siguiente imagen:

Fuente de la imagen: Zero Day Initiative

Todos los GUID en los archivos .LINK son almacenados usando la representación RPC IDL para GUID. Dicha representación se traduce en que los tres primeros segmentos del GUID se almacenan como pequeñas representaciones finales del fragmento en su conjunto, mientras que cada byte en los dos últimos segmentos se considera que son individuales. Por ejemplo, el GUID {01234567-1234-ABCD-9876-0123456789AB} se representa de la siguiente manera en formato binario: x67x45x23x01x34x12xCDxABx98x76x01x23x45x67x89xAB.

La carpeta ItemID delegada es seguida por User Property View ItemID, que tiene una estructura similar a la de la carpeta ItemID delegada:

Fuente de la imagen: Zero Day Initiative

De particular importancia resulta el campo PropertyStoreList, el cual, si está presente, contiene uno o más objetos serializados PropertyStore, cada uno de los cuales tiene la siguiente estructura:

Fuente de la imagen: Zero Day Initiative

El campo Property Store Data es una secuencia de propiedades. Todas las propiedades de cada uno de los Property Store Data pertenecen a la clase identificada por el GUID Property Format. Cada propiedad específica es identificada con un ID numérico conocido como Property ID o PID, el cual, cuando se combina con el GUID Property Format, es nombrado clave de propiedad o PKEY. La PKEY se determina de una forma ligeramente diferente si el GUID Property Format es igual a {D5CDD505-2E9C-101B-9397-08002B2CF9AE}. Cada propiedad, entonces, se considera que forma parte de una «bolsa de propiedades» y tiene la siguiente estructura:

Fuente de la imagen: Zero Day Initiative

Las bolsas de propiedades generalmente contienen elementos con los nombres “Key:FMTID” y “Key:PID”, identificando a la PKEY específica que determina la interpretación del resto de elementos. Las implementaciones de la Bolsa de Propiedades Específicas también requerirá que otros elementos estén presentes en orden para ser válidos.

Si el GUID Property Format no es igual al valor previamente mencionado para la bolsa de propiedades, cada propiedad será identificada con un valor íntegro del PID y tendrá la siguiente estructura:

Fuente de la imagen: Zero Day Initiative

El campo TypedPropertyValue corresponde al valor introducido de una propiedad en un conjunto de propiedades. Asimismo, en las cabeceras proporcionadas con el Windows SDK se definen múltiples PKEY. Sin embargo, muchas de ellas no tienen documentación y solo se pueden identificar examinando referencias en los símbolos de debugging para las librerías asociadas. En el caso de los archivos .LNK que contienen búsquedas almacenadas, el primer PropertyStore en User Property View ItemID tiene un GUID Property Format cuyo valor es {1E3EE840-BC2B-476C-8237-2ACD1A839B22} y que contiene una propiedad con ID 2, que se corresponde con PKEY_FilterInfo.

El campo TypedPropertyValue de PKEY_FilterInfo consiste en una propiedad VT_STREAM. Normalmente, dicha propiedad está formada por un tipo 0x0042, 2 bytes de relleno, y un IndirectPropertyName que especifica el nombre de un stream alterno que, o bien contiene un paquete PropertySetStream para el almacenamiento simple de propiedades, o el elemento stream «CONTENTS» para un almacenamiento complejo de propiedades. Este nombre se especifica con la cadena de caracteres «prop» seguido de una cadena decimal que es corresponde con un identificador de propiedad en un paquete PropertySet. Sin embargo, debido a que los archivos .LNK utilizan propiedades almacenadas serializadas que se encuentran en las propiedades de VT_STREAM, IndirectPropertyName solo se comprueba para ver si comienza con la cadena «prop«. El valor en sí es ignorado. Esto genera la siguiente estructura de TypedPropertyValue:

Fuente de la imagen: Zero Day Initiative

El contenido del campo Stream Data depende de la PKEY específica a la que pertenece la propiedad stream. En el caso de PKEY_FilterInfo, Stream Data básicamente contiene una estructura PropertyStoreList con más estructuras serializadas PropertyStore y tiene la siguiente apariencia:

Fuente de la imagen: Zero Day Initiative

El conjunto de PropertyStoreList en el stream PKEY_FilterInfo es una versión serializada de la etiqueta «condiciones» en un archivo .search-ms. Lo siguiente es un ejemplo de la etiqueta de condiciones:

Fuente de la imagen: Zero Day Initiative

La funcionalidad precisa del elemento atributo no está documentada públicamente. Sin embargo, un elemento de este tipo contiene un GUID que corresponde a CONDITION_HISTORY, y un CLSID que corresponde a la clase CConditionHistory en StructuredQuery, lo que significaría que el conjunto de condiciones y atributos representa el historial de peticiones de búsqueda antes de ser almacenado. Cuando esta estructura es serializada y pasa a ser una propiedad almacenada, se coloca en la estructura PKEY_FilterInfo PropertyStoreList, que toma la forma de una bolsa de propiedades con el GUID Property Format anteriormente mencionado. De manera más específica, la estructura Condiciones serializada se encuentra en en VT_STREAM Property, la cual se identifica con el nombre «Condición». Esto resulta en un objeto PropertyStore que tiene la siguiente estructura:

Fuente de la imagen: Zero Day Initiative

El objeto Condición es generalmente un objeto de «Condición simple» (Leaf Condition) o «Condición Compuesta» (Compound Condition) que contiene una serie de objetos que normalmente incluyen uno o más objetos de Condición Simple y posiblemente objetos adicionales de Condición Compuestas. Ambos objetos condicionales comienzan con la siguiente estructura:

Fuente de la imagen: Zero Day Initiative

El GUID de Condición será {52F15C89-5A17-48E1-BBCD-46A3F89C7CC2} para las Condiciones Simples y {116F8D13-101E-4FA5-84D4-FF8279381935} para las Condiciones Compuestas. El campo Atributos consiste en estructuras de atributos, donde el número de estructuras de atributos se define con el campo «Número de Atributos». Cada estructura de atributo corresponde al elemento de un atributo del archivo .search-ms y comienza de la siguiente manera:

Fuente de la imagen: Zero Day Initiative

La estructura restante de un atributo depende del AttributeID y del CLSID. Para el atributo anteriormente mencionado CONDITION_HISTORY el valor será {9554087B-CEB6-45AB-99FF-50E8428E860D} y tiene un CLSID de {C64B9B66-E53D-4C56-B9AE-FEDE4EE95DB1}. Lo que queda de la estructura será un objeto del tipo ConditionHistory que tendrá la siguiente forma (los campos son nombrados igual que los atributos que coinciden con el elemento del atributo XML):

Fuente de la imagen: Zero Day Initiative

Si el valor de has_nested_conditions es mayor que cero, el atributo CONDITION_HISTORY tendrá un objeto condicional anidado, el cual podría tener a su vez más atributos anidados con condiciones anidadas, y así sucesivamente.

Una vez que el atributo de mayor importancia es leído, así como todas sus estructuras asociadas, las estructuras de Condición Compuesta y Condición Simple son las siguientes, con inicios relacionados con el final del campo Attributes:

Fuente de la imagen: Zero Day Initiative

El campo numfixedObjects determina cuántas condiciones adicionales (normalmente Condiciones Simples) seguirán de manera inmediata.

La estructura restante de una Condición Simple es como se muestra en la siguiente imagen, con inicios relacionados con el final del campo Attributes:

Fuente de la imagen: Zero Day Initiative

La presencia del estructuras TokenInformationComplete depende de si la flag que le precede está configurada. Si no lo está, la estructura no está presente, y por tanto le seguirá la siguiente flag. Si, por el contrario, está configurada, encontraremos la estructura que se observa en la imagen:

Fuente de la imagen: Zero Day Initiative

El siguiente conjunto muestra la estructura más simple posible de un archivo .LNK con una búsqueda almacenada, habiendo sido eliminadas las estructuras más irrelevantes para reducir su complejidad:

Fuente de la imagen: Zero Day Initiative

Debemos tener en mente, según los investigadores, que una búsqueda con una Condición Simple resulta en la estructura más simple posible, mientras que, frecuentemente, un archivo .LNK con una búsqueda almacenada comenzará con una Condición Compuesta y muchas estructuras relacionadas, incluyendo muchas Condiciones Simples.

La vulnerabilidad

La vulnerabilidad se basa en cómo se trata el campo PropertyVariant. El campo PropertyVariant de una Condición Simple difícilmente corresponde a una estructura PROPVARIANT. Es importante mencionar que StructuredQuery parece tener una implementación ligeramente personalizada de la estructura PROPVARIANT ya que los bytes de relleno especificados en la documentación de Microsoft generalmente no aparecen.

También es importante tener en cuenta que el valor de 0x1000 (o VT_VECTOR) combinado con otro tipo significa que habrá varios valores del tipo especificado.

A la hora de parsear el campo PropertyVariant, es la función vulnerable StructuredQuery1::ReadPROPVARIANT() la que se encarga del proceso:

Fuente de la imagen: Zero Day Initiative

La función comprueba si se trata de VT_UI4 (0x0013), y en caso de que no, entra en una declaración switch.

La vulnerabilidad en sí consiste en cómo se trata a una PropertyVariant que tiene VT_VARIANT (0x000C). VT_VARIANT se usa normalmente en combinación con VT_VECTOR, el cual resulta en una serie de estructuras PropertyVariant. En otras palabras, es como tener un array donde los miembros del mismo pueden ser cualquier tipo de dato.

Cuando el tipo de PropertyVariant se configura como VT_VARIANT (0x000C), el campo completo se comprueba para ver si se ha configurado VT_VECTOR:

Fuente de la imagen: Zero Day Initiative

Si no se ha configurado, un buffer de 24 bytes será localizado por medio de una llamada a CoTaskMemAlloc() y el buffer pasa a una llamada recursiva a ReadPROPVARIANT() con la intención de que dicho buffer se complete con la propiedad que siga inmediatamente al campo VT_VARIANT. Sin embargo, el buffer no se inicia (se completa con bytes nulos) antes de ser transferido a ReadPROPVARIANT().

Si la propiedad anidada tiene el tipo VT_CF (0x0047), una propiedad cuya funcionalidad pretendida es contener un redirector al portapapeles, ReadPROPVARIANT(), ejecuta la misma verificación de VT_VECTOR y si no está configurado, intenta escribir los próximos 4 bytes del stream en una localización a la que se apunta mediante un valor de 8 bytes en el buffer de 24 bytes previamente localizado.

Fuente de la imagen: Zero Day Initiative

Debido al hecho de que el buffer no se inicia, la información será escrita en una posición no definida de la memoria, lo que puede llevar a la ejecución de código aleatorio. El intento de escritura de datos se puede ver en la siguiente imagen:

Fuente de la imagen: Zero Day Initiative

Esencialmente, si un atacante puede manipular la memoria de la manera correcta para que el buffer no iniciado contenga un valor controlado por el usuario malicioso, esta persona podría escribir cualquier dato de 4 en 4 bytes en la dirección de memoria que hayan considerado oportuna.

Conclusión

Para solventar esta vulnerabilidad se procedió a completar el buffer de 24 bytes con bytes nulos, asegurando así que el atacante no pueda utilizar información del resto del buffer procedente de usos anteriores de la memoria. El parche se lanzó en febrero (aunque el día 10 de marzo de este año publicaron otra vulnerabilidad relacionada con archivos .LNK, aunque no está relacionada con la que se explica en este post).

Más información

CVE-2020-0729: Remote code execution through .LNK files

CVE-2018-0825 – StructuredQuery Remote Code Execution Vulnerability

CVE-2020-0729 – LNK Remote Code Execution Vulnerability

CVE-2020-0684 – LNK Remote Code Execution Vulnerability

Powered by WPeMatico

Gustavo Genez

Informático de corazón y apasionado por la tecnología. La misión de este blog es llegar a los usuarios y profesionales con información y trucos acerca de la Seguridad Informática.