Hijacking del flujo de control de un programa de WebAssembly
Aunque WebAssembly ya ha demostrado ser una superficie de ataque fértil para el navegador, a medida que crece el número de códigos de aplicaciones web que se pasan de Javascript a WebAssembly va surgiendo la necesidad de investigar y proteger los programas de WebAssembly. El diseño de WebAssembly obvia los ataques comunes que podrían heredarse de lenguajes de desarrollo como C y C++, pero todavía hay espacio para la explotación de vulnerabilidades.
En este tutorial, veremos las garantías de control de flujo que proporciona WebAssembly, las debilidades conocidas y cómo usar el control de integridad de flujo (CFI, por sus siglas en inglés) en Clang en programas de WebAssembly para mitigar algunos riesgos relacionados con el secuestro del control del flujo. Sobre la marcha, secuestraremos el flujo de control de un programa de ejemplo de WebAssembly al aprovecharnos de una vulnerabilidad (ficticia) de confusión de tipos. Además, adaptaremos algo de código de la serie de entradas de blog "Let's talk about CFI" (Hablemos sobre el CFI) de Trail of Bits; si no estás familiarizado con el control de integridad de flujo, estas entradas de blog son un buen punto de partida.
Esta es la primera de dos partes de una entrada de blog sobre aspectos relacionados con la seguridad de WebAssembly. La segunda parte de esta serie analizará un conjunto de cuestiones de seguridad, incompleto pero interesante, sobre las aplicaciones de software que proporcionan un entorno para ejecutar programas de WebAssembly, especialmente navegadores web.
Ten en cuenta que esta entrada de blog no incluye una explicación detallada del funcionamiento de WebAssembly. Si no estás familiarizado con WebAssembly, puedes consultar webassembly.org, la página de la guía oficial para desarrolladores, o buscar en la web alguna de las muchas entradas de blog, tutoriales y vídeos disponibles para obtener más información.
WebAssembly: el camino hacia una aplicación web cercana
WebAssembly es una iniciativa abierta de toda la industria en conjunto para incorporar un lenguaje de ensamblado seguro y eficiente a la web. La tecnología de WebAssembly ha sido desarrollada de forma colaborativa por los principales proveedores de navegadores web, como Mozilla, Google, Microsoft y Apple, además de por empresas de tecnología web no dedicadas a los navegadores, como Fastly. Los módulos de WebAssembly se pueden descargar y ejecutar desde la mayoría de navegadores que se usan hoy en día. Grandes proyectos de desarrollo como AutoCAD y QT, entre otros, sacan cada vez más partido a WebAssembly para implementar aplicaciones rápidas y seguras diseñadas a partir de códigos base C/C++ en plataformas de sobremesa, móviles y navegadores. El ecosistema de WebAssembly está creciendo con rapidez, con nuevas herramientas, aplicaciones e ideas anunciadas con regularidad (por ejemplo, esta es una de las muchas listas que ofrecen contenidos seleccionados).
WebAssembly es el sucesor lógico de Google Native Client, una tecnología de aislamiento de fallos de software que permitió a los desarrolladores implementar aplicaciones nativas en Google Chrome. WebAssembly proporciona un diseño refinado que se ha beneficiado enormemente de las lecciones aprendidas de Native Client, con aspectos destacados como un sistema de tipos sólido que cabe en una sola página, integridad de control de flujo, indeterminismo limitado/local, garantías de protección de memoria, etc. Para obtener más información sobre el diseño de WebAssembly, consulta el artículo del PLDI (Congreso sobre diseño e implementación de lenguajes de programación) de 2017 o webassembly.org.
Hay muchas posibilidades de que veamos en WebAssembly buena parte de la lógica que tradicionalmente se implementa en Javascript, incluidos los controles de seguridad, en algún momento futuro. También cabe esperar que los desarrolladores web hagan cosas nuevas, interesantes y arriesgadas con WebAssembly, a medida que la tecnología va ganando popularidad.
El diseño de WebAssembly admite protección de memoria para evitar la incorporación de algunos de los problemas endémicos que sufren C, C++ y otros lenguajes potenciales. En la siguiente sección se analiza esta característica de WebAssembly.
Protección de memoria en programas de WebAssembly
La documentación de seguridad de protección de memoria en WebAssembly explica que muchas clases de errores de protección de memoria y tipos de exploits asociados, como el pisado de pila, ROP, etc. se obvian en programas de WebAssembly. Por lo tanto, si asumimos que el programa que compila y ejecuta el código de WebAssembly funciona correctamente, ese tipo de ataques no son posibles. Esto es algo estupendo, y lógico si tenemos en cuenta la arquitectura tan sumamente calculada de WebAssembly.
Sin embargo, la documentación aclara más adelante:
«No obstante, la semántica de WebAssembly no obvia otras clases de errores. Aunque los atacantes no pueden realizar ataques de inyección de código de forma directa, es posible secuestrar el flujo de control de un módulo mediante ataques de reutilización de código contra llamadas indirectas».
Podemos explorar las implicaciones prácticas de este diseño mediante un escenario de explotación de una vulnerabilidad de confusión de tipos.
La víctima: una vulnerabilidad ficticia de confusión de tipos
Usaremos un programa de ejemplo de confusión de tipo de una llamada virtual de C++ simplificado de la serie de entradas "Let's talk about CFI" (Hablemos de CFI) del blog Trail of Bits como ejemplo.
El concepto de este ejemplo es que el atacante engaña de algún modo al programa para llamar a un método de una instancia del tipo incorrecto. Esto ocurre de muchas formas distintas «de forma natural» (y no se limita a C++), pero un escenario común es que el programa lea una instancia (o lógica de selección de instancias) de una fuente de datos que no es de confianza (como la red) y, sin comprobar si la instancia es del tipo esperado, llama a algún método de objeto (o función) de esta. Si un atacante puede incluir en el programa una instancia de un tipo no esperado, a veces puede tomar también el control del programa (o causar otros efectos negativos).
Más adelante, se abarcarán los cambios realizados al código de Trail of Bits; puedes encontrar los códigos de ejemplo utilizados en este tutorial en Github.
Configuración de herramientas del tutorial opcionales
En esta sección, se explica la configuración de las herramientas en caso de que quieras probar en casa. Si no es así, siéntete libre de omitir esta sección.
Usaremos Docker Ubuntu 16.04 como huésped para compilar código vulnerable para destinos nativos y de WebAssembly. Compartiremos un directorio para que podamos usar las herramientas instaladas en nuestro host para editar archivos y utilizaremos un navegador web host para ejecutar WebAssembly.
docker run -v "$(pwd):/src" -t -i ubuntu:16.04 bash
Usaremos Clang para compilar en destinos nativos y [emscripten](https://github.com/kripken/emscripten\) para compilar destinos de WebAssembly. Aunque puede que no sean los mismos en tu caso, estos son algunos de los comandos que ejecuté para instalar todas las herramientas en el huésped Ubuntu: root@2dc5f92b98cf:/src# apt-get update && apt-get install -y cmake build-essential python2.7 nodejs git wget tmux root@2dc5f92b98cf:/src# apt-get install clang-5.0 && ln -s /usr/bin/clang-5.0 /usr/bin/clang && ln -s /usr/bin/clang++-5.0 /usr/bin/clang++ root@2dc5f92b98cf:/src# wget https://s3\.amazonaws.com/mozilla\-games/emscripten/releases/emsdk\-portable.tar.gz && tar -xf emsdk-portable.tar.gz && cd emsdk-portable root@2dc5f92b98cf:/src/emsdk-portable# ./emsdk update && ./emsdk install latest && ./emsdk activate latest ```
Para crear el contenido de este tutorial, ejecuté una sesión tmux (a la que me referiré en adelante como [tmux]`) para su uso como entorno de emscripten. La sesión [tmux] utilizará la cadena de herramientas empscriten para destinos WASM, y el shell huésped normal utilizará la cadena de herramientas Clang de Ubuntu para destinos nativos. Este es el entorno emscripten: [tmux] root@2dc5f92b98cf:/src/emsdk-portable# source ./emsdk_env.sh && which clang /src/emsdk-portable/clang/e1.37.35_64bit/clang ```
Y el entorno Clang/nativo: ``` root@2dc5f92b98cf:/src/emsdk-portable# which clang /usr/bin/clang ```
Podemos utilizar el [kit de herramientas binarias de WebAssembly (WABT, por sus siglas en inglés)](https://github.com/WebAssembly/wabt\) para convertir módulos de WebAssembly binarios a formato de texto (y a la inversa): ``` root@2dc5f92b98cf:/src# git clone --recursive https://github.com/WebAssembly/wabt && cd wabt root@2dc5f92b98cf:/src/wabt# make && make install ```
## Frustrar un exploit con la comprobación de tipos de WebAssembly Los contenedores de WebAssembly (por ejemplo, navegadores web) comprueban por lo general los tipos de funciones (en términos de sus argumentos y valores de retorno) para garantizar que son correctas antes de permitir la ejecución de un programa de WebAssembly (consulta también "[](https://developer.mozilla.org/en\-US/docs/Web/JavaScript/Reference/Global\_Objects/WebAssembly/validate\)"\). Sin embargo, las comprobaciones de tipos para *llamadas indirectas*, que son análogas para llamar a un puntero de función en C o C++, se producen durante el tiempo de ejecución. Cuando falla la comprobación de tipos para una llamada indirecta, el programa de WebAssembly se detiene y se ejecuta un comando trap. En un navegador, esto da lugar a una excepción de Javascript que el código del usuario puede gestionar (o no). No obstante, gracias a las garantías que ofrece el diseño de WebAssembly, el proceso del contenedor (por ejemplo, el navegador) puede continuar su ejecución de forma segura sin temor a un comportamiento no definido, como corrupción de la memoria.
Para observar una comprobación de tipos correcta en acción, usaremos una versión de cfi_vcall.cpp modificada del código de ejemplo de Trail of Bits. Nuestra versión modificada se llama cfi_vcall_diff.cpp. En cfi_vcall_diff.cpp, la función víctima (a la que el programa trata de llamar inocentemente) toma un argumento entero, y la función maliciosa (la que el atacante de algún modo es capaz de suministrar) está limitada a tomar un argumento nulo. En resumen, el atacante está intentando que la víctima ejecute lo siguiente:
virtual void makeAdmin(float * i) {
std::cout << "CFI Prevents this control flow " << i << "\n";
std::cout << "Evil::makeAdmin\n";
}
Instead of this: ``` virtual void printMe(int i) { std::cout << "Derived::printMe " << i << "\n"; } ```
In the next section we’ll demonstrate exploitation with these programs.
Experimento n.º 1: aprovechar una vulnerabilidad de confusión de tipos en un ejecutable nativo
Podemos observar lo que ocurre sin la comprobación de tipos de WebAssembly al compilar antes con Clang el programa vulnerable o víctima de exploit:
root@2dc5f92b98cf:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -o cfi_vcall_diff cfi_vcall_diff.cpp
Y luego ejecutarlo: ``` root@2dc5f92b98cf:/src/clang-cfi-showcase# ./cfi_vcall_same_cfi Derived::printMe 55.5 CFI Prevents this control flow 0 Evil::makeAdmin ```
Se puede comprobar en el resultado anterior que se aprovechó la vulnerabilidad del programa; el atacante ejecutó la carga "makeAdmin". Esto es posible porque el código de máquina nativo no incluye comprobaciones de los tipos de parámetro de función en el tiempo de ejecución; la función «maliciosa» se ejecuta sin problemas (aunque con resultados extraños, ya que la función considera que el número entero suministrado es un número decimal).
Experimento n.º 2: evitar un exploit de confusión de tipos en WebAssembly
Podemos observar lo que ocurre con la comprobación de tipos de WebAssembly al compilar con emscripten el programa vulnerable o víctima de exploit:
[tmux] root@2dc5f92b98cf:/src/clang-cfi-showcase# emcc cfi_vcall_diff.cpp -Werror -s WASM=1 -o cfi_vcall_diff.html
El comando de arriba producirá un módulo de WebAssembly (.wasm), un wrapper de Javascript para llamar, etc. (.js) y un archivo HTML para conectarlo todo (.html). Podemos navegar hasta la página HTML resultante y abrir la consola para desarrolladores de nuestro navegador para observar el resultado. Una forma de hacerlo es ejecutar un servidor HTTP básico con Python en el sistema host: ``` mayor:clang-cfi-showcase foote$ python -m SimpleHTTPServer 8081 ```
Y luego acceder a la página generada:
¡Ajá! El código de WebAssembly descubre el error y lo captura en el programa anfitrión. Cuando el intérprete ejecuta call_indirect, se produce un error en la comprobación de tipos y se genera un desvío. Podemos convertir el código de WebAssembly a texto para entender mejor qué es lo que está ocurriendo:
wasm2wat cfi_vcall_diff.wasm > cfi_vcall_diff.wat
Al ver el archivo, podemos ver la invocación de call_indirect (anotada): ``` [...] f64.const 0x1.bcp+5 (;=55.5;) // Push arg (55.5) onto the stack get_local 4 // Calculate function ptr (cont’d) i32.const 15 // (an index into the func table) i32.and // .. i32.const 5376 // .. i32.add // .. call_indirect (type 0) // call func ptr: printMe/makeAdmin [..] ```
La definición de tipo que comprueba call_indirect define una función que toma un número decimal: ``` (type (;0;) (func (param i32 f64))) ```
En consecuencia, la comprobación de tipos falla porque ambas funciones tienen firmas diferentes en WebAssembly: la función «víctima» toma un número decimal (f64), mientras que la función «malvada» toma un número entero (i32).
Hay que tener en cuenta que, mientras que el programa de WebAssembly se detendrá y retendrá en este punto, el error se aislará de la instancia del programa invitado de WebAssembly por su diseño. Esto significa que el proceso del navegador host de WebAssembly puede continuar funcionando de forma segura sin preocuparse de problemas como corrupción de la memoria o similares.
Aprovechar las debilidades de comprobación de tipos de WebAssembly
WebAssembly ofrece un refinado diseño de seguridad que incluye un sistema de tipos sólido. Uno de los efectos secundarios de este diseño es que, a día de hoy, WebAssembly proporciona pocos tipos de valor: i32
, i64
, f32
y f64
. Esto significa que todos los tipos de valor del lenguaje de origen (como C o C++), se asignan a estos tipos, los cuales son los que WebAssembly utiliza para la comprobación de tipos de llamada indirecta. Esto significa que, en nuestro ejemplo de ejecución, un atacante podría ser capaz de secuestrar el flujo de control de WebAssembly si consigue suministrar una función cuyo tipo de firma de WebAssembly coincida (tal y como se describe en la documentación de la protección de memoria de WebAssembly).
Para observar este comportamiento en acción, utilizaremos una versión modificada de cfi_vcall.cpp de Trail of Bits, llamada cfi_vcall_same.ccp. En cfi_vcall_same.cpp, la función víctima (a la que el programa trata de llamar inocentemente) toma un argumento entero, y la función maliciosa (la que el atacante de algún modo es capaz de suministrar) está limitada a tomar un argumento nulo. A pesar de que son tipos diferentes en C++, se corresponden con el mismo tipo en WebAssembly. Esto significa que las firmas de función entre las funciones «víctima» y «maliciosa» coincidirán y el atacante podrá secuestrar el flujo de control del programa de WebAssembly atacado. En resumen, en cfi_vcall_same.cpp el atacante está intentando conseguir que la víctima ejecute lo siguiente:
virtual void makeAdmin(void * i) {
std::cout << "CFI Prevents this control flow " << i << "\n";
std::cout << "Evil::makeAdmin\n";
}
Instead of this: ``` virtual void printMe(int i) { std::cout << "Derived::printMe " << i << "\n"; } ```
Once again, in the next section we’ll demonstrate exploitation with these programs.
Experimento n.º 3: aprovechar una vulnerabilidad de confusión de tipos en un binario nativo (¡otra vez!)
Podemos observar lo que ocurre sin la comprobación de tipos de WebAssembly al compilar antes con Clang el programa vulnerable o víctima de exploit:
root@2dc5f92b98cf:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -o cfi_vcall_same cfi_vcall_same.cpp
Y luego ejecutarlo: ``` root@2dc5f92b98cf:/src/clang-cfi-showcase# ./cfi_vcall_same Derived::printMe 55 CFI Prevents this control flow 66 Evil::makeAdmin ```
Se puede comprobar en el resultado anterior que se aprovechó la vulnerabilidad del programa; el atacante ejecutó la carga "makeAdmin". Esto es posible porque el código de máquina nativo no incluye comprobaciones de los tipos de parámetro de función en el tiempo de ejecución; de nuevo, la función «maliciosa» se ejecuta sin problemas.
Experimento n.º 4: aprovechar una vulnerabilidad de confusión de tipos en WebAssembly
Podemos observar lo que ocurre con la comprobación de tipos de WebAssembly al compilar con emscripten el programa vulnerable o víctima de exploit:
[tmux] root@2dc5f92b98cf:/src/clang-cfi-showcase# emcc cfi_vcall_same.cpp -Werror -s WASM=1 -o cfi_vcall_same.html
Como en el experimento anterior con WebAssembly, el comando de arriba producirá cfi_vcall_same.wasm (el módulo de WebAssembly), cfi_vcall_same.js (un archivo Javascript que define la interfaz entre el navegador y el módulo de WebAssembly) y cfi_vcall_same.html (una página HTML que ejecuta el Javascript).
Podemos ejecutar de nuevo un servidor HTTP básico con Python en el sistema host:
mayor:clang-cfi-showcase foote$ python -m SimpleHTTPServer 8081
Y navegamos a la página que se genera:
Podemos ver aquí que la vulnerabilidad de confusión de tipo existe y que el «exploit» ejecuta makeAdmin
. Esto se debe a que esta vez tanto printMe
como makeAdmin
tendrán firmas del mismo tipo en las líneas:
(type (;0;) (func (param i32 i32)))
Esto se debe a que los tipos int y void* de C se asignan al tipo i32 en WebAssembly (hay que tener en cuenta que los programas de WebAssembly utilizan direccionamiento de 32 bits; para más información sobre este asunto, recomiendo la [ponencia en el PLDI (Congreso sobre diseño e implementación de lenguajes de programación) de 2017](https://dl.acm.org/citation.cfm?id=3062363\)\). Por lo tanto, aunque WebAssembly comprueba la firma de la función a la que está a punto de llamar (es decir, los tipos de WebAssembly de los parámetros de la función y el resultado), las dos funciones tienen la misma firma por lo que el exploit se ejecuta correctamente.
Uso de CFI en Clang para mitigar los exploits de WebAssembly
Como hemos visto antes, mientras que el sistema simple de WebAssembly tiene muchísimos beneficios, una de las desventajas es que siguen produciéndose vulnerabilidades de confusión de tipos. Por suerte, al igual que ocurre con los ejecutables nativos, podemos compilar código de WebAssembly con comprobaciones de CFI en Clang. Tal y como se ha analizado en la documentación sobre protección de memoria de WebAssembly, esto ayuda a defenderse contra los ataques de reutilización de código que estamos viendo aquí y, además, utiliza tipos de C/C++, generalmente más precisos, para otras comprobaciones de firma de función.
En última instancia, esto significa que las comprobaciones de CFI en Clang se pueden compilar en el programa de WebAssembly y aplicarse en el contenedor (como un navegador) . ¡Genial!
Experimento n.º 5: aplicación de CFI en Clang en un ejecutable nativo
Podemos observar la aplicación del CFI con Clang en un binario nativo al compilar nuestro ejemplo cfi_vcall_same.cpp con -fsanitize=cfi-vcall (dando salida al binario en cfi_vcall_same_cfi):
root@2dc5f92b98cf:/src/clang-cfi-showcase# clang++ -Weverything -Werror -Wno-weak-vtables -fvisibility=hidden -flto -fsanitize=cfi-vcall -fno-sanitize-trap=all -o cfi_vcall_same_cfi cfi_vcall_same.cpp
Y luego ejecutarlo: ``` root@2dc5f92b98cf:/src/clang-cfi-showcase# ./cfi_vcall_same_cfi Derived::printMe 55 cfi_vcall_same.cpp:45:5: runtime error: control flow integrity check for type 'Derived' failed during virtual call (vtable address 0x0000004300a0) 0x0000004300a0: note: vtable is of type 'Evil' 00 00 00 00 20 84 42 00 00 00 00 00 30 84 42 00 00 00 00 00 60 84 42 00 00 00 00 00 00 00 00 00 ```
Podemos comprobar por el resultado de arriba que el CFI en Clang ha funcionado cómo se esperaba; el exploit ha sido bloqueado.
Experimento n.º 6: aplicación de CFI en Clang en un programa de WebAssembly.
Ahora viene la parte más interesante: podemos comprobar la ejecución del CFI en Clang en el programa de WebAssembly que ha sufrido previamente el exploit al llamar a emscripten con las marcas -fsanitize=cfi-vcall:
[tmux] root@2dc5f92b98cf:/src/clang-cfi-showcase# emcc cfi_vcall_same.cpp -fvisibility=hidden -flto -fsanitize=cfi -s WASM=1 -o cfi_vcall_same_cfi.html
Y luego ver el archivo HTML resultante en el navegador:
Podemos ver arriba que la aplicación de CFI en Clang se produce en el navegador, frustrando así el exploit.
Conclusión
En esta entrada de blog hemos mostrado cómo secuestrar el flujo de control de un programa de ejemplo de WebAssembly al aprovechar una vulnerabilidad de confusión de tipos, lo que demuestra alguna de las garantías de protección de memoria que ofrece WebAssembly. También hemos abarcado el uso de CFI en Clang para reforzar los programas de WebAssembly ante este tipo de ataques.
En general, WebAssembly es una tecnología bien diseñada que ofrece un punto de partida ideal para el desarrollo de aplicaciones seguro; con suerte, este tutorial y el análisis habrán ilustrado algunos aspectos de seguridad del ecosistema.
No te pierdas la segunda parte de esta serie, en la que se analizará un conjunto de cuestiones de seguridad, incompleto pero interesante, sobre el software que proporciona un entorno para ejecutar los programas invitados de WebAssembly, principalmente navegadores web.