
wasm2c Sandbox-Escape. Ein nicht vertrauenswürdiges WebAssembly-Modul bricht aus der generierten C-Sandbox aus und führt einen beliebigen Shell-Befehl auf dem Host aus.
Lies den Blog: trustsig.eu/blog
wasm_rt_allocate_funcref_table() in wasm2c/wasm-rt-impl-tableops.inc setzt
table->size aus der deklarierten Elementanzahl des Moduls und ignoriert anschließend das Ergebnis von
calloc(). Wenn die Allokation fehlschlägt, bleibt die Tabelle mit data == NULL und der vollen
deklarierten size zurück, sodass jede Grenzprüfung weiterhin besteht und table->data[i] zur
absoluten Adresse i * sizeof(wasm_rt_funcref_t) wird.
Die Elementanzahl stammt vom Gast, sodass der Gast die Allokationsgröße wählen und den Fehler erzwingen kann.
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
Das Image klont wabt von upstream beim Tag 1.0.41, baut wat2wasm und wasm2c,
kompiliert das Gastmodul und einen einfachen Embedder und führt es aus.
Erwartete Ausgabe:
running guest
guest returned 0
--- file on host ---
goodbye sandbox
Die letzte Zeile ist der Inhalt von /tmp/pwned.txt, einer Datei, die vor der Ausführung des Sandbox-Moduls nicht existierte.
Verifiziert auf linux/arm64 und linux/amd64. Nichts am Modul ist architekturspezifisch: Die Instanzadresse, der GOT-Slot und der libc-Offset werden alle zur Buildzeit aus dem gerade gebauten Binary und aus der libc dieses Images aufgelöst.
Unter Linux, mit installiertem clang, cmake, ninja und binutils:
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 ist kein unterstütztes Ziel für diesen PoC. Darwin erzwingt kein RLIMIT_AS, daher gelingt das überdimensionierte calloc und der Bug tritt nie auf; arm64-macOS-Builds sind immer positionsunabhängig, und Mach-O hat kein ELF-GOT für den Leak-Schritt. Der Defekt selbst ist plattformunabhängig; nur diese Exploit-Kette ist Linux-spezifisch.
Andere Versionen und Befehle:
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). Das 68-GB-calloc schlägt fehl, data ist NULL,
size bleibt 2147483648, und Tabellenindizes werden zu absoluten Adressen.table.get bei got_slot/32 liest das GOT des Embedders durch die defekte Tabelle, und
table.set speichert das Ergebnis in die eigenen Globals des Moduls, wo Wasm-Code es als Ganzzahl lesen kann.
Das leakt die malloc-Adresse von libc, und system ergibt sich aus einer festen Distanz in der libc des Ziels.wasm_rt_funcref_t in vier aufeinanderfolgenden Globals auf: func_type zeigt auf eine
Kopie des Typhashs der Aufrufstelle (func_types_eq_slowpath vergleicht ihn mit
, sodass gastkontrollierte Bytes die Prüfung bestehen), = , und
= die Befehlszeichenkette, ebenfalls in Globals gehalten.Das einzige Layout-Faktum, das im Modul fest verdrahtet ist, ist die Adresse der Modulinstanz, die ein Global ist und daher in einem Nicht-PIE-Embedder fest liegt. ASLR bleibt aktiviert; die libc-Adresse wird zur Laufzeit geleakt.
Die Tabellenallokation muss fehlschlagen. Der PoC verwendet ulimit -v 1000000, ein Adressraumlimit,
wie es ein Host, der nicht vertrauenswürdigen Code ausführt, setzen würde. Es schlägt außerdem auf 32-Bit-Hosts fehl,
wo die Allokation überhaupt nicht erfüllt werden kann, bei vm.overcommit_memory=2 oder unter ausreichendem Speicherdruck.
Auf normalem 64-Bit-Linux mit der Standard-Overcommit-Heuristik gelingt die Allokation und wird nie berührt, weshalb der Bug normales Testen übersteht.
Jedes Release, das wasm2c-Tabellen ausliefert. Das ungeprüfte calloc stammt aus Commit
ab9e0b55 (#813). Verifiziert gegen das veröffentlichte 1.0.41 und das aktuelle main.
Der Speicherallokator in derselben Runtime behandelt diesen Fall so:
memory->data = (MEMORY_CELL_TYPE)calloc(byte_length, 1);
if (byte_length != 0 && !memory->data) {
abort();
}
Der Tabellen-Allokator braucht dieselbe Prüfung.
Dockerfile baut wabt und führt den PoC aus.tableflip_poc.py erzeugt das Gastmodul, baut den Embedder, löst die drei Konstanten aus dem gebauten Binary und der libc des Ziels mit nm und readelf auf und führt es aus. Der von ihm erzeugte Embedder enthält keinen Exploit-Unterstützungscode.memcmpfuncsystemmodule_instancecall_indirect bei globals_addr/32. wasm2c erzeugt
((t)entry.func)(entry.module_instance, ...), also ruft das system(command) auf.