
Escape de sandbox de wasm2c. Un módulo WebAssembly no confiable escapa de la sandbox C generada y ejecuta un comando shell arbitrario en el host.
Lee el blog: trustsig.eu/blog
wasm_rt_allocate_funcref_table() en wasm2c/wasm-rt-impl-tableops.inc establece
table->size a partir del número de elementos declarado por el módulo y luego ignora el resultado de
calloc(). Cuando la asignación falla, la tabla queda con data == NULL y el
size declarado completo, por lo que todas las comprobaciones de límites siguen pasando y table->data[i] se convierte
en la dirección absoluta i * sizeof(wasm_rt_funcref_t).
El número de elementos proviene del invitado, por lo que el invitado elige el tamaño de la asignación y puede forzar el fallo.
git clone https://github.com/trustsig-eu/wasm2c-tableflip.git
cd wasm2c-tableflip
docker build -t wabt-w2c-poc .
docker run --rm wabt-w2c-poc
La imagen clona wabt en la etiqueta 1.0.41 desde el upstream, compila wat2wasm y wasm2c,
compila el módulo invitado y un embedder simple, y lo ejecuta.
Salida esperada:
running guest
guest returned 0
--- file on host ---
goodbye sandbox
La última línea es el contenido de /tmp/pwned.txt, un archivo que no existía antes de que el
módulo en sandbox se ejecutara.
Verificado en linux/arm64 y linux/amd64. Nada en el módulo es específico de la
arquitectura: la dirección de la instancia, la ranura GOT y el desplazamiento de libc se resuelven en
tiempo de compilación a partir del binario recién construido y de la libc de esa imagen.
En Linux, con clang, cmake, ninja y binutils instalados:
git clone --depth 1 --branch 1.0.41 --recurse-submodules --shallow-submodules \
https://github.com/WebAssembly/wabt ~/wabt
cmake -S ~/wabt -B ~/wabt/out -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTS=OFF -DBUILD_LIBWASM=OFF -DWITH_WASI=OFF
ninja -C ~/wabt/out wat2wasm wasm2c
python3 tableflip_poc.py --wabt-src ~/wabt --wat2wasm ~/wabt/out/wat2wasm \
--wasm2c ~/wabt/out/wasm2c --run
cat /tmp/pwned.txt
macOS no es un objetivo compatible para este PoC. Darwin no hace cumplir RLIMIT_AS, por lo que
el calloc de tamaño excesivo tiene éxito y el fallo nunca se dispara; las compilaciones de macOS en arm64 siempre son
independientes de la posición, y Mach-O no tiene GOT ELF para el paso de fuga. El defecto en sí es
independiente de la plataforma; solo esta cadena de explotación es específica de Linux.
Otras versiones y comandos:
docker build --build-arg WABT_REF=main -t wabt-w2c-poc .
docker run --rm wabt-w2c-poc bash -c \
"python3 tableflip_poc.py --run --command 'id > /tmp/pwned.txt' && cat /tmp/pwned.txt"
(table $t 2147483648 funcref). El calloc de 68 GB falla, data es NULL,
size permanece en 2147483648, y los índices de la tabla se convierten en direcciones absolutas.table.get en got_slot/32 lee el GOT del embedder a través de la tabla rota y
table.set almacena el resultado en los propios globales del módulo, donde el código wasm puede leerlo
como un entero. Eso filtra la dirección de malloc de libc, y system se deduce a partir de una
distancia fija en la libc del objetivo.wasm_rt_funcref_t en cuatro globales consecutivos: func_type apuntando a una
copia del hash de tipo del sitio de llamada (func_types_eq_slowpath lo compara con
, por lo que los bytes controlados por el invitado pasan la comprobación), = , y
= la cadena de comando, también mantenida en globales.El único dato de diseño incrustado en el módulo es la dirección de la instancia del módulo, que es un global y, por tanto, fija en un embedder no PIE. ASLR permanece habilitado; la dirección de libc se filtra en tiempo de ejecución.
La asignación de la tabla tiene que fallar. El PoC usa ulimit -v 1000000, un límite de
espacio de direcciones del tipo que un host que ejecuta código no confiable establecería. También falla en hosts
de 32 bits, donde la asignación no se puede satisfacer en absoluto, con
vm.overcommit_memory=2, o bajo suficiente presión de memoria.
En Linux de 64 bits estándar con la heurística de overcommit predeterminada, la asignación tiene éxito y nunca se toca, por lo que el fallo sobrevive a las pruebas normales.
Cada lanzamiento que incluye tablas de wasm2c. El calloc sin comprobar data de la confirmación
ab9e0b55 (#813). Verificado contra el lanzamiento 1.0.41 y el main actual.
El asignador de memoria en el mismo runtime maneja este caso:
memory->data = (MEMORY_CELL_TYPE)calloc(byte_length, 1);
if (byte_length != 0 && !memory->data) {
abort();
}
El asignador de tablas necesita la misma comprobación.
Dockerfile compila wabt y ejecuta el PoC.tableflip_poc.py genera el módulo invitado, compila el embedder, resuelve las tres
constantes a partir del binario compilado y de la libc del objetivo con nm y readelf, y
lo ejecuta. El embedder que emite no contiene código de soporte de explotación.memcmpfuncsystemmodule_instancecall_indirect en globals_addr/32. wasm2c emite
((t)entry.func)(entry.module_instance, ...), por lo que esto llama a system(command).