Capital Software Blog

Análisis de un Chrome Zero Day: CVE-2019-5786

1. Introducción

El 1 de marzo, Google publicó un aviso [1] para un uso después de la implementación en Chrome de la API FileReader (CVE 2019-5786). Clement Lecigne, del Grupo de análisis de amenazas de Google, informó que el error se había explotado de forma salvaje y estaba dirigido a plataformas de Windows 7 y 32 bits. El exploit conduce a la ejecución del código en el proceso del Renderer, y se utilizó un segundo exploit para comprometer completamente el sistema host [2]. Este blog es un artículo técnico que detalla el primer error y cómo encontrar más información sobre él. En el momento de escribir este artículo, el informe de error [2b] todavía está sellado. La instalación predeterminada de Chrome instalará las actualizaciones automáticamente, y los usuarios que ejecutan la última versión de Chrome ya están protegidos contra ese error. Para asegurarse de que está ejecutando la versión parcheada, visite chrome: // versión, el número de versión que se muestra en la página debe ser 72.0.3626.121 o mayor.

2. Recopilación de información

2.1 La corrección de errores

La mayor parte de la base de código de Chrome se basa en el proyecto de código abierto Chromium. El error que estamos observando está contenido dentro del código fuente abierto, por lo que podemos ver directamente lo que se corrigió en la nueva versión relacionada con la API FileReader. Convenientemente, Google comparte el registro de cambios para su nueva versión [3].

Podemos ver que solo hay una confirmación que modifica los archivos relacionados con la API FileReader, con el siguiente mensaje:

El mensaje sugiere que tener varias referencias al mismo ArrayBuffer subyacente es algo malo. No está claro lo que significa en este momento, pero los siguientes párrafos trabajarán para descubrir qué sabiduría se esconde en este mensaje.

Para empezar, podemos ver el commit diff [3b] y ver qué ha cambiado. Para facilitar la lectura, aquí hay una comparación de la función antes y después del parche.

El viejo:


El nuevo:

Las dos versiones se pueden encontrar en GitHub en [4a] y [4b]. Este cambio modifica el comportamiento de la función ArrayBufferResult que es responsable de devolver los datos cuando un usuario desea acceder al miembro FileReader.result. 


El comportamiento de la función es el siguiente: si el resultado ya está ‘en caché’, devuélvalo. Si no, hay dos casos; Si los datos han terminado de cargarse, cree un DOMArrayBuffer, almacene en caché el resultado y lo devuelva. Si no, crea un DOMArrayBuffer temporal y lo devuelve en su lugar. La diferencia entre la versión no parcheada y la parcheada es cómo se maneja ese DOMArrayBuffer temporal, en caso de una carga parcial. En un caso, podemos ver una llamada a:

Esto nos llevó a bajar algunos agujeros de conejo más. Comparemos lo que está sucediendo tanto en la situación no parcheada como en la parcheada.

Podemos comenzar con la versión parcheada, ya que es la más sencilla de entender. Podemos ver una llamada a ArrayBuffer :: Create que toma dos argumentos, un puntero a los datos y su longitud (la función se define en el árbol de origen en /third_party/blink/renderer/platform/wtf/typed_arrays/array_buffer.h)

Básicamente, esto crea un nuevo ArrayBuffer, lo envuelve en un scoped_refptr <ArrayBuffer> y luego copia los datos en él. El scoped_refptr es una forma en que Chromium puede manejar el conteo de referencias [5]. Para los lectores que no están familiarizados con la idea, la idea es hacer un seguimiento de cuántas veces se hace referencia a un objeto. Al crear una nueva instancia de scoped_refptr, el recuento de referencia para el objeto subyacente se incrementa; cuando el objeto sale de su ámbito, el recuento de referencia se decrementa. Cuando ese recuento de referencias llega a 0, el objeto se elimina (y para los curiosos, Chrome anulará un proceso si el recuento de referencias se desborda …). A medida que buscamos un posible uso después de la liberación, sabiendo que el buffer se ha vuelto a contar se cierran algunas vías de explotación.

En la versión no parcheada, en lugar de llamar a ArrayBuffer :: Create, el código usa el valor de retorno de ArrayBufferBuilder :: ToArrayBuffer () (de third_party / blink / renderer / platform / wtf / typed_arrays / array_buffer_builder.cc):

Aquí hay otro agujero de conejo para bucear (pero lo mantendremos en un nivel alto). Dependiendo del valor de bytes_used_), la función devolverá su búfer o una versión del mismo (es decir, un nuevo ArrayBuffer de un tamaño más pequeño, que contiene una copia de los datos)

Para resumir lo que tenemos hasta ahora, en todas las rutas de código que hemos visto, todas devuelven una copia de los datos en lugar del búfer real, a menos que ejecutemos el código no parcheado, y el búfer al que intentamos acceder es completamente utilizado` (según el comentario en ArrayBufferBuilder :: ToArrayBuffer ()).

Debido a la implementación del objeto FileReaderLoader, el búfer _-> ByteLength () es el tamaño asignado previamente del búfer, que corresponde al tamaño de los datos que queremos cargar (esto será relevante más adelante). 


Si ahora recordamos el mensaje de confirmación y cuál fue el mal escenario, parece que la única situación para explotar el error es acceder varias veces a ArrayBufferBuilder :: ToArrayBuffer (), antes de que finalice la carga en true, pero después de que los datos estén completamente cargado.

Para resumir esta parte de la revisión del código, veamos el comportamiento de la función DOMArrayBuffer :: Create que se está llamando en ambos casos parchados / no parcheados, el caso que nos interesa es cuando tenemos la siguiente llamada DOMArrayBuffer :: Crear (raw_data _-> ToArrayBuffer ());

Desde third_party / blink / renderer / core / typed_arrays / dom_array_buffer.h:

Algo interesante a considerar es el uso de std :: move, que tiene la semántica de transferir la propiedad.

 
Por ejemplo, en el siguiente fragmento de código:

entonces `b` se apropia de lo que pertenecía a` a` (`b` ahora contiene“ hola ”) y` a` está ahora en un estado algo indefinido (las especificaciones de C ++ 11 lo explican en términos más precisos).

En nuestra situación actual, lo que está sucediendo aquí es algo confuso [6a] [6b]. El objeto devuelto por ArrayBufferBuilder :: ToArrayBuffer () ya es un scoped_refptr <ArrayBuffer>. Creo que el significado de todo esto, es que cuando se llama a ToArrayBuffer (), el refcount en el ArrayBuffer se incrementa en uno, y std :: move se adueña de esa instancia del objeto refcounted (a diferencia del que es propiedad del ArrayBufferBuilder). Llamar a ToArrayBuffer () 10 veces aumentará el refcount en 10, pero todos los valores de retorno serán válidos (a diferencia del ejemplo de juguete con las cadenas `a` y` b` mencionadas anteriormente, donde operar en `a` daría como resultado un inesperado comportamiento). 


Esto cierra un caso obvio de uso después de que el objeto buffer_ del ArrayBufferBuilder se corrompiera si llamáramos a ToArrayBuffer () varias veces durante el punto dulce descrito anteriormente.

2.2 API de FileReader

Otro ángulo de enfoque para descubrir cómo explotar este error es mirar la API que está disponible desde JavaScript y ver si podemos encontrar una manera de alcanzar el punto óptimo que estábamos viendo.

Podemos obtener toda la información que deseemos de los documentos web de Mozilla [7]. Nuestras opciones son bastante concisas; podemos llamar a las funciones readAsXXX en Blob o File, podemos abortar la lectura, y finalmente hay un par de eventos en los que podemos registrar devoluciones de llamada (onloadstart, onprogress, onloadend, …).

Los eventos de onprogress parecen ser los más interesantes, ya que se llaman mientras se cargan los datos, pero antes de que finalice la carga. Si observamos el archivo de origen FileReader.cc, podemos ver que la lógica detrás de la invocación de este evento es disparar cada 50 ms (o así) cuando se reciben los datos.Echemos un vistazo a cómo se comporta esto en un sistema real …

3. Pruebas en un navegador web

3.1 Empezando

Lo primero que queremos hacer es descargar una versión vulnerable del código. Hay algunos recursos muy útiles por ahí [8] donde uno puede descargar compilaciones más antiguas en lugar de tener que construirlas usted mismo.

Algo interesante a tener en cuenta es que también hay un archivo zip separado que tiene `syms` en su nombre. También puede descargar para obtener símbolos de depuración para la compilación (en forma de archivos .pdb). Los depuradores y los desensambladores pueden importar esos símbolos que harán su vida más fácil, ya que cada función será renombrada por su nombre real en el código fuente.

3.2 Adjuntando un depurador

Chromium es un software complejo y varios procesos se comunican entre sí, lo que dificulta la depuración. La forma más eficiente de depurar es iniciar Chromium normalmente y luego adjuntar el depurador al proceso que desea explotar. El código que estamos depurando se está ejecutando en el proceso del procesador, y las funciones que estábamos viendo están expuestas por chrome_child.dll (esos detalles se encontraron por prueba y error, se adjuntaron a cualquier proceso de Chrome y buscar nombres de funciones de interés).

Si desea importar símbolos en x64dbg, una posible solución es ir en el panel de Símbolos, haga clic derecho en el archivo .dll / .exe para el que desea importar los símbolos y seleccione Descargar símbolos. Puede fallar si la configuración del servidor de símbolos no está configurada correctamente, pero seguirá creando la estructura de directorios en el directorio `symbols` de x64dbg, donde puede colocar los archivos .pdb que ha descargado anteriormente.

3.3 Buscando la ruta del código explotable

No es que hayamos descargado una versión de Chromium sin parches, y sabemos cómo adjuntar un depurador, escríbanos un poco de JavaScript para ver si podemos encontrar la ruta del código que nos interesa.

Para resumir lo que está pasando aquí, creamos un Blob que pasamos al FileReader. Registramos una devolución de llamada al evento de progreso y, cuando se invoca el evento, intentamos acceder varias veces al resultado del lector. Hemos visto anteriormente que los datos deben estar completamente cargados (es por eso que verificamos el tamaño del búfer) y si obtenemos múltiples DOMArrayBuffer con el mismo respaldo ArrayBuffer, deberían aparecer como objetos separados para JavaScript (de ahí la igualdad). prueba). Finalmente, para volver a verificar que tenemos dos objetos diferentes respaldados por el mismo búfer, creamos vistas para modificar los datos subyacentes y verificamos que modificar uno modifica el otro también.

Hay un problema desafortunado que no habíamos previsto: el evento de progreso no se llama con frecuencia, por lo que tenemos que cargar una matriz muy grande para forzar al proceso a tomar algún tiempo y desencadenar el evento varias veces. Puede haber mejores maneras de hacerlo (¡tal vez el informe de errores de Google revele una!) Pero todos los intentos de crear un objeto de carga lenta fueron un error (usar un Proxy, extender la clase Blob …). La carga está vinculada a un Mojo Pipe, por lo que exponer MojoJS podría ser una forma de tener más control también, pero parece poco realista en un escenario de atacante, ya que este es el punto de entrada del ataque. Ver [9] para un ejemplo de ese enfoque.

3.4 Causando un choque

Entonces, ahora que hemos descubierto cómo entrar en el camino del código que es vulnerable, ¿cómo lo explotamos? Esta fue definitivamente la pregunta más difícil de responder, y este párrafo está destinado a compartir el proceso para encontrar una respuesta a esa pregunta.

Hemos visto que el ArrayBuffer subyacente se vuelve a contar, por lo que es improbable que podamos liberarlo mágicamente simplemente recolectando basura de parte del DOMArrayBuffer que hemos obtenido. Desbordar el refcount suena como una idea divertida, pero si intentamos a mano modificar el valor del refcount para que esté cerca de su valor máximo (a través de x64dbg) y veamos qué sucede … bueno, el proceso se bloquea. Finalmente, no podemos hacer mucho con esos ArrayBuffers; podemos cambiar su contenido pero no su tamaño, ni podemos liberarlos manualmente … 


Al no estar lo suficientemente familiarizado con la base de código, el mejor enfoque es verter varios informes de errores que mencionan el uso después del uso gratuito, ArrayBuffer, etc., y ver qué hicieron o hablaron las personas. Debe haber alguna suposición en algún lugar de que un DOMArrayBuffer posee su memoria subyacente, y es una suposición que sabemos que estamos rompiendo. 


Después de algunas búsquedas, comenzamos a encontrar algunos comentarios interesantes como este [10a] y este [10b]. Esos dos enlaces hablan de varias situaciones en las que DOMArrayBuffer se externaliza, transfiere y neutraliza. No estamos familiarizados con esos términos, pero según el contexto, cuando esto sucede, la propiedad de la memoria se transfiere a otra persona. Eso suena bastante perfecto para nosotros, ya que queremos que se libere el búfer subyacente (ya que estamos buscando un uso después de la liberación). 


El uso después de gratis en WebAudio nos muestra cómo hacer que nuestro ArrayBuffer sea «transferido», ¡así que intentémoslo!

Y como se ve en el depurador:

La memoria a la que se está haciendo referencia no está en ECX (también tenemos EAX == 0, pero eso se debe a que estamos viendo el primer elemento en la vista). La dirección parece válida, pero no lo es. ECX contiene la dirección donde se almacenaron los datos sin procesar de nuestro búfer (el AAAAA …) pero debido a que se liberó, el sistema desasignó las páginas que lo contenían, lo que provocó la infracción de acceso (estamos tratando de acceder a una dirección de memoria no asignada). ¡Llegamos al uso después del libre que buscábamos!

4. Consideraciones de exploits y próximos pasos.

4.1 Explotación

No es el objetivo de este documento ilustrar cómo presionar más allá del uso después del uso gratuito para obtener la ejecución completa del código (de hecho, Exodus ha publicado un blog y un exploit operativo que coincide aproximadamente con el calendario de esta publicación). Sin embargo, hay algunos comentarios interesantes que hacer. 


Debido a la forma en que activamos el uso después de la liberación, estamos terminando con un búfer muy grande sin asignar. La forma habitual de explotar un uso después de libre es obtener un nuevo objeto asignado en la parte superior de la región liberada para crear algún tipo de confusión. Aquí, estamos liberando la memoria en bruto que se utiliza para respaldar los datos de nuestro ArrayBuffer. Eso es genial porque podemos leer / escribir en una gran región. Sin embargo, un problema en este enfoque es que debido a que la región de la memoria es realmente grande, no hay un objeto que se ajuste. Si tuviéramos un pequeño búfer, podríamos crear muchos objetos que tengan ese tamaño específico y esperamos que uno pueda ser asignado allí. Aquí es más difícil porque debemos esperar hasta que la memoria sea reclamada por los objetos no relacionados. En Windows 10 de 64 bits, es difícil debido a las asignaciones aleatorias y la entropía disponible para las direcciones aleatorias. En Windows 7 de 32 bits, es mucho más fácil, ya que el espacio de direcciones es mucho más pequeño y la asignación de almacenamiento dinámico es más determinista. Asignar un objeto de 10k podría ser suficiente para que algunos metadatos se encuentren dentro del espacio de direcciones que podemos controlar. 


El segundo aspecto interesante es que debido a que vamos a desreferenciar una región que no ha sido asignada, si la asignación de 10k mencionada anteriormente no puede asignar al menos un objeto en esa área que controlamos, entonces no tenemos suerte; Obtendremos una violación de acceso y el proceso morirá. Hay maneras de hacer que este paso sea más confiable, como el método iframe descrito aquí [11] 


Un ejemplo de cómo seguir adelante si uno puede corromper los metadatos de un objeto JavaScript se puede encontrar aquí [12].

4.2 siguiente paso

Una vez que un atacante ha ganado ejecución de código dentro del proceso del renderizador, todavía están limitados por el sandbox. En el exploit que se encuentra en la naturaleza, el atacante usó un segundo día de 0 que se enfocó en el Kernel de Windows para escapar de la caja de arena. Aquí se publicó un artículo que describe la explotación recientemente publicado por 360CoreSec [13].

5. Conclusión

Al observar el compromiso que corrigió el error y buscar sugerencias y soluciones similares, pudimos recuperar el camino probable hacia la explotación. Una vez más, podemos ver que las mitigaciones modernas introducidas en la versión posterior de Windows dificultan mucho la vida de los atacantes y debemos celebrar esas victorias desde el lado defensivo. Además, Google es extremadamente eficiente y agresivo en su estrategia de parches, y la mayoría de su base de usuarios ya se habrá actualizado a la última versión de Chrome.

Enlaces:

[1] https://chromereleases.googleblog.com/2019/03/stable-channel-update-for-desktop.html 
[2] https://security.googleblog.com/2019/03/disclosing-vulnerabilities-to-protect.html 
[2b] https://bugs.chromium.org/p/chromium/issues/detail?id=936448 
[3] https://chromium.googlesource.com/chromium/src/+log/72.0.3626.119..72.0.3626.121?pretty=fuller 
[3b] https://github.com/chromium/chromium/commit/ba9748e78ec7e9c0d594e7edf7b2c07ea2a90449 
[4a] https://github.com/chromium/chromium/blob/17cc212565230c962c1f5d036bab27fe800909f9/third_party/blink/renderer/core/fileapi/file_reader_loader.cc 
[4b] https://github.com/chromium/chromium/blob/75ab588a6055a19d23564ef27532349797ad454d/third_party/blink/renderer/core/fileapi/file_reader_loader.cc 
[5] https://www.chromium.org/developers/smart-pointer-guidelines 
[6a] https://chromium.googlesource.com/chromium/src/+/lkgr/styleguide/c++/c++.md#object-ownership-and-calling-conventions 
[6b] https://www.chromium.org/rvalue-references 
[7] https://developer.mozilla.org/en-US/docs/Web/API/FileReader 
[8] https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/612439/ 
[9] https://www.exploit-db.com/exploits/46475 
[10a] https://bugs.chromium.org/p/v8/issues/detail?id=2802 
[10b] https://bugs.chromium.org/p/chromium/issues/detail?id=761801 
[11] https://blog.exodusintel.com/2019/01/22/exploiting-the-magellan-bug-on-64-bit-chrome-desktop/ 
[12] https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/ 
[13] http://blogs.360.cn/post/RootCause_CVE-2019-0808_EN.html

[social_warfare]

Tabla de Contenido

Share on facebook
Share on twitter
Share on linkedin