El cliente de Steam eliminó una vulnerabilidad peligrosa que se había escondido allí durante diez años.

imagen

El investigador principal, Tom Court of Context, una compañía de seguridad de la información, habla sobre cómo logró detectar un error potencialmente peligroso en el código del cliente de Steam.

Los jugadores de PC conscientes de la seguridad han notado que Valve lanzó recientemente una nueva actualización del cliente Steam.

En esta publicación, quiero poner excusas para jugar juegos en el trabajo para contar la historia de un error relacionado que existió en el cliente Steam durante al menos diez años, que hasta julio del año pasado podría conducir a la ejecución remota de código (ejecución remota de código, RCE) en total los 15 millones de clientes activos.

Desde julio, cuando Valve (finalmente) compiló su código con la protección contra exploits moderna habilitada, solo podía provocar una falla del cliente, y RCE solo era posible en combinación con una vulnerabilidad de fuga de información por separado.

Declaramos que Valve era vulnerable el 20 de febrero de 2018 y, para crédito de la compañía, lo arregló en la rama beta menos de 12 horas después. La solución se trasladó a la sucursal estable el 22 de marzo de 2018.

imagen

Breve reseña


La base de la vulnerabilidad fue el daño al montón dentro de la biblioteca del cliente Steam, que podría llamarse de forma remota, en esa parte del código que estaba involucrado en la recuperación del datagrama fragmentado de varios paquetes UDP recibidos.

El cliente Steam intercambia datos a través de su propio protocolo (protocolo Steam), que se implementa sobre UDP. Hay dos áreas en este protocolo que son particularmente interesantes debido a la vulnerabilidad:

  • Longitud del paquete
  • La longitud total del datagrama reconstruido.

El error fue causado por la falta de una verificación simple. El código no verificó que la longitud del primer datagrama fragmentado sea menor o igual que la longitud total del datagrama. Esto parece una supervisión común dado que para todos los paquetes posteriores que transmiten fragmentos del datagrama, se realiza la verificación.

Sin errores de fuga de datos adicionales, el daño del montón en los sistemas operativos modernos es muy difícil de controlar, por lo que la ejecución remota de código es difícil de implementar. Sin embargo, en este caso, gracias al propio asignador de memoria de Steam y ASLR que faltaba en el archivo binario steamclient.dll (hasta julio pasado), este error podría usarse como base para una explotación muy confiable.

A continuación se muestra una descripción técnica de la vulnerabilidad y su exploit asociado hasta
implementaciones de ejecución de código.

Detalles de vulnerabilidad


Información necesaria para entender


Protocolo


Terceros (por ejemplo, https://imfreedom.org/wiki/Steam_Friends ), basados ​​en el análisis del tráfico generado por el cliente Steam, realizaron ingeniería inversa y crearon documentación detallada del protocolo Steam. Inicialmente, el protocolo fue documentado en 2008 y no ha cambiado mucho desde entonces.

El protocolo se implementa como un protocolo de transmisión con el establecimiento de una conexión a través de un flujo de datagramas UDP. Los paquetes, de acuerdo con la documentación en el enlace anterior, tienen la siguiente estructura:

imagen

Aspectos importantes:

  • Todos los paquetes comienzan con 4 bytes de " VS01 "
  • packet_len describe la longitud de la información útil (para datagramas no fragmentados, el valor es igual a la longitud de los datos)
  • type describe el tipo de paquete, que puede tener los siguientes valores:
    • Autenticación de llamada 0x2
    • 0x4 Aceptar conexión
    • 0x5 Restablecer conexión
    • 0x6 Un paquete es un fragmento de un datagrama
    • El paquete 0x7 es un datagrama separado
  • Los campos de origen y destino son identificadores asignados para enrutar correctamente los paquetes en múltiples conexiones dentro del cliente Steam
  • En caso de que el paquete sea un fragmento de un datagrama:
    • split_count indica la cantidad de fragmentos en los que se divide el datagrama
    • data_len indica la longitud total del datagrama recuperado
  • El procesamiento inicial de estos paquetes UDP ocurre en la función CUDPConnection :: UDPRecvPkt dentro de steamclient.dll

Cifrado


La información útil del paquete de datagramas es encriptada por AES-256 usando una clave, que se negocia entre el cliente y el servidor en cada sesión. La negociación clave se realiza de la siguiente manera:

  • El cliente genera una clave aleatoria AES de 32 bytes, y RSA la cifra con la clave pública Valve antes de enviarla al servidor.
  • El servidor, que tiene una clave privada, puede descifrar este valor y aceptarlo como una clave AES-256, que se utilizará en la sesión
  • Después de acordar la clave, toda la información útil de la sesión actual se cifra con esta clave.

Vulnerabilidad


La vulnerabilidad está presente dentro del método RecvFragment de la clase CUDPConnection . No hay símbolos en la versión de lanzamiento de la biblioteca steamclient, sin embargo, al buscar a través de líneas binarias en una función que nos interesa, se encuentra un enlace a " CUDPConnection :: RecvFragment ". El ingreso de esta función se realiza cuando el cliente recibe un paquete UDP que contiene un datagrama de Steam de tipo 0x6 (un "fragmento de un datagrama").

1. La función comienza verificando el estado de la conexión para asegurarse de que esté en el estado " Conectado ".
2. Luego, el campo data_len en el datagrama de Steam se verifica para asegurarse de que contiene menos de 0x20000060 bytes (parece que este valor se elige arbitrariamente).
3. Si se pasa la verificación, la función verifica si la conexión recolecta fragmentos de algún datagrama, o si es el primer paquete de la secuencia.

imagen

4. Si este es el primer paquete en la secuencia, entonces el campo split_count se verifica para ver cuántos paquetes se extenderá esta secuencia
5. Si la secuencia se divide en varios paquetes, entonces el campo seq_no_of_first_pkt se verifica para asegurarse de que coincida con el número de serie del paquete actual. Esto asegura que el paquete sea el primero en la secuencia.
6. El campo data_len se verifica nuevamente contra el límite de 0x20000060 bytes. Además, se verifica que split_count es menor que 0x709b paquetes.

imagen

7. Si se cumplen estas condiciones, se establece un valor booleano para indicar que ahora estamos recopilando fragmentos. También comprueba que todavía no tenemos un búfer asignado para almacenar fragmentos.

imagen

8. Si el puntero al búfer de colección de fragmentos no es cero, entonces el búfer de colección de fragmentos actual se libera y se asigna un nuevo búfer (vea el rectángulo amarillo en la figura a continuación). Aquí es donde aparece el error. Se espera que el búfer de recopilación de fragmentos se asigne en el tamaño de bytes data_len . Si todo fue exitoso (y el código no verifica, un error menor), la información útil del datagrama se copia en este búfer usando memmove , confiando en que el número de bytes para copiar se indica en packet_len .

La supervisión más importante del desarrollador fue que no se realiza la comprobación " packet_len es menor o igual que data_len ". Esto significa que es posible transferir data_len menos que packet_len y tener hasta 64 KB de datos (debido a que el campo packet_len tiene 2 bytes de ancho) copiados en un búfer muy pequeño, lo que hace posible explotar la corrupción del montón.

imagen

Explotación de vulnerabilidad


Esta sección asume que hay una solución para ASLR. Esto lleva al hecho de que antes de comenzar la operación, se conoce la dirección de inicio de steamclient.dll.

Suplantación de paquetes


Para que el cliente reciba los paquetes UDP atacantes, debe examinar el datagrama saliente (cliente -> servidor), que se envía para encontrar los identificadores de la conexión cliente / servidor, así como el número de serie. Luego, el atacante debe suplantar las direcciones IP y los puertos de origen / destino junto con los identificadores de cliente / servidor y aumentar el número de serie aprendido en uno.

Gestión de la memoria


Para asignar memoria de más de 1024 (0x400) bytes, se utiliza un asignador de sistema estándar. Para asignar memoria menor o igual a 1024 bytes, Steam usa su propio asignador que funciona igual en todas las plataformas compatibles. Este artículo no discutirá en detalle este distribuidor, con la excepción de los siguientes aspectos clave:

  1. Se solicitan grandes bloques de memoria al asignador del sistema, que luego se divide en fragmentos de un tamaño fijo para su uso bajo las solicitudes de asignación de memoria del cliente Steam.
  2. La selección se realiza secuencialmente, entre los fragmentos utilizados no hay metadatos que los separen.
  3. Cada bloque grande almacena su propia lista de memoria libre, implementada como una lista individualmente vinculada.
  4. La parte superior de la lista de memoria libre indica el primer fragmento libre en la memoria, y los primeros 4 bytes de este fragmento indican el siguiente fragmento libre (si existe).

Asignación de memoria


Cuando se asigna memoria, el primer bloque libre se desconecta de la parte superior de la lista de memoria libre, y los primeros 4 bytes de este bloque correspondientes a next_free_block se copian en la variable miembro freelist_head dentro de la clase del asignador .

Memoria libre


Cuando se libera un bloque, el campo freelist_head se copia en los primeros 4 bytes del bloque liberado ( next_free_block ), y la dirección del bloque liberado se copia en la variable miembro freelist_head de la clase de distribuidor.

Cómo obtener una grabación primitiva


Se produce un desbordamiento del búfer en el montón y, según el tamaño de los paquetes que causaron la corrupción, la asignación de memoria puede controlarse mediante el asignador estándar de Windows (al asignar memoria de más de 0x400 bytes) o por el propio asignador de Steam (al asignar memoria de menos de 0x400 bytes). Debido a la falta de medidas de seguridad en mi propio distribuidor de Steam, decidí que era más fácil usarlo para un exploit.

Volvamos a la sección sobre administración de memoria: se sabe que la parte superior de la lista de bloques de memoria libre de un tamaño dado se almacena como una variable miembro de la clase de distribuidor, y el puntero al siguiente bloque libre de la lista se almacena como los primeros 4 bytes de cada bloque libre de la lista.

Si hay un bloque libre al lado del bloque en el que ocurrió el desbordamiento, el daño en el montón nos permite sobrescribir el puntero next_free_block . Si considera que se puede preparar un montón para esto, entonces el puntero reescrito next_free_block se puede establecer en una dirección para escribir, después de lo cual la posterior asignación de memoria se escribirá en este lugar.

Qué usar: datagramas o fragmentos


Se produce un error con corrupción de memoria en el código responsable del procesamiento de fragmentos de datagramas (paquetes de tipo 6). Después de la aparición de daños, la función RecvFragment () se encuentra en un estado en el que espera recibir más fragmentos. Sin embargo, si llegan, se realiza una verificación:

fragment_size + num_bytes_already_received < sizeof(collection_buffer)

Pero obviamente, este no es el caso, porque nuestro primer paquete ya ha violado esta regla (la existencia de un error es posible omitir esta verificación) y se producirá un error. Para evitar esto, debe evitar el método CUDPConnection :: RecvFragment () después de la corrupción de la memoria.

Afortunadamente, CUDPConnection :: RecvDatagram () todavía puede recibir y procesar paquetes enviados de tipo 7 (datagramas) hasta que RecvFragment () sea ​​válido, y esto puede usarse para iniciar la primitiva de grabación.

Problemas de cifrado


Se espera que los paquetes recibidos por RecvDatagram () y RecvFragment () estén encriptados. En el caso de RecvDatagram (), el descifrado se realiza casi inmediatamente después de la recepción. En el caso de RecvFragment (), ocurre después de recibir el último fragmento de la sesión.

El problema de explotar la vulnerabilidad surge porque no conocemos la clave de cifrado que se crea en cada sesión. Esto significa que cualquier código OP / código de shell que enviemos será "descifrado" usando AES256, lo que convertirá nuestros datos en basura. Por lo tanto, es necesario encontrar un método de operación, que sea posible casi inmediatamente después de recibir el paquete, antes de que los procedimientos de descifrado puedan procesar la información útil contenida en el búfer de paquetes.

Cómo lograr la ejecución del código


Dada la restricción de descifrado descrita anteriormente, la operación debe realizarse antes del descifrado de los datos entrantes. Esto impone restricciones adicionales, pero la tarea aún es factible: puede reescribir el puntero para que apunte al objeto CWorkThreadPool almacenado en un lugar predecible dentro de la sección de datos del archivo binario. Aunque se desconocen los detalles y la funcionalidad interna de esta clase, se puede suponer por su nombre que admite un grupo de subprocesos que puede usar cuando necesite "trabajar". Después de estudiar varias líneas de depuración en un archivo binario, puede comprender que entre tales trabajos hay cifrado y descifrado ( CWorkItemNetFilterEncrypt , CWorkItemNetFilterDecrypt ), por lo que cuando estas tareas se ponen en cola, se utiliza la clase CWorkThreadPool . Al sobrescribir este puntero y escribir el lugar deseado en él, podemos simular el puntero vtable y la vtable asociada con él, lo que nos permite ejecutar código, por ejemplo, cuando se llama CWorkThreadPool :: AddWorkItem () , lo que debe suceder antes de cualquier proceso de descifrado.

La siguiente figura muestra la explotación exitosa de la vulnerabilidad hasta la etapa de obtener el control sobre el registro EIP.

imagen

A partir de ahora, puede crear una cadena ROP que conduzca a la ejecución de código arbitrario. El siguiente video muestra cómo un atacante inicia de forma remota una calculadora de Windows en una versión completamente parcheada de Windows 10.


Para resumir


Si llega a esta parte del artículo, ¡gracias por su persistencia! Espero que entiendan que este es un error muy simple que fue bastante fácil de explotar debido a la falta de medios modernos de protección contra exploits. El código vulnerable probablemente era muy antiguo, pero por lo demás funcionó bien, por lo que los desarrolladores no vieron la necesidad de examinarlo o actualizar sus scripts de compilación. La lección aquí es que es importante que los desarrolladores revisen periódicamente el código antiguo y creen sistemas para asegurarse de que cumplan con los estándares de seguridad modernos, incluso si la funcionalidad del código en sí no cambia. Fue increíble encontrar en 2018 un error tan simple con consecuencias tan graves en una plataforma de software muy popular. ¡Esto debería ser un incentivo para buscar tales vulnerabilidades para todos los investigadores!

Finalmente, vale la pena hablar sobre el proceso de divulgación responsable de información. Reportamos este error a Valve en una carta a su equipo de seguridad ( security@valvesoftware.com ) aproximadamente a las 4 pm GMT y solo 8 horas después, se creó una solución y se lanzó en el cliente beta de Steam. Gracias a esto, Valve ahora ocupa el primer lugar en nuestra tabla (imaginaria) del concurso "Quién solucionará la vulnerabilidad más rápido", una agradable excepción en comparación con la divulgación de errores a otras compañías, que a menudo resulta en un largo proceso de aprobación.

Una página que describe los detalles de todas las actualizaciones del cliente.

Source: https://habr.com/ru/post/es413003/


All Articles