-15% na nową książkę sekuraka: Wprowadzenie do bezpieczeństwa IT. Przy zamówieniu podaj kod: 10000

WASM – czym jest WebAssembly?

24 września 2018, 10:15 | Teksty | komentarzy 5

WebAssembly (WASM) jest odpowiedzią na rosnące zapotrzebowanie użytkowników (i deweloperów oczywiście) na bogate, funkcjonalnie i szybkie aplikacje webowe. Problemem w takich scenariuszach wykorzystania jest najczęściej wydajność całego środowiska, a do tej pory nie udało się w pełni poprawić wszystkich bolączek JavaScript.

Rozwiązaniem tych problemów ma być środowisko umożliwiające uruchomienie niskopoziomowego kodu (skompilowanego z C / C++ / Rust / Go), z wydajnością zbliżoną do natywnej, czyli tytułowego WASM. Warto w tym miejscu wspomnieć, że WebAssembly nie ma na celu całkowitego zastąpienia języka JavaScript – jest jego uzupełnieniem, również technicznie są uruchamiane w tej samej maszynie wirtualnej.

Głównym zastosowaniem standardu są aplikacje webowe wymagające znacznej mocy obliczeniowej: gry, multimedia (przetwarzanie obrazu i dźwięku), środowiska obliczeń matematycznych, modelowanie 3D lub porty całych aplikacji znanych ze środowisk desktopowych. Ostatni przykład jest wyjątkowo interesujący ze względu na fakt, że WASM może być również uruchomiony poza procesem przeglądarki.

“Pod maską” bezpieczeństwa WASM

Jak zostało wspomniane w sekcji wprowadzającej, WASM zakłada całkowite uruchomienie w środowisku sandboksa JavaScript, co oznacza współdzieloną pamięć pomiędzy aplikacją WebAssembly, a kodem JavaScript. Jednakże dostęp ten nie jest swobodny – niskopoziomowy kod nie jest w stanie korzystać z utworzonych obiektów i otrzymuje jedynie dostęp do zasobów utworzonych za pomocą funkcji WebAssembly.Memory(). Polecam całą serię postów z bloga Mozilla Hacks, na temat implementacji zarządzania pamięcią w WASM.

Oczywiście wykorzystanie piaskownicy automatycznie wymusza ograniczenie wymiany informacji ze środowiskiem poza nią. Przepływ danych realizowany jest przez API udostępniane poprzez JavaScript. Model bezpieczeństwa WASM zakłada wykorzystanie szeregu sposobów mających chronić użytkownika przed złośliwym kodem, jednocześnie zachowując kompatybilność z programami napisanymi w C/C++:

  • Kod funkcji jest odseparowany od przestrzeni danych,
  • Adresy w pamięci danych są chronione – WebAssembly nie może bezpośrednio odnieść się do zawartości bufora (dostęp jedynie po indeksie tablicy),
  • Wywołania funkcji muszą określać indeks wywoływanej funkcji zgodny z zamieszczonym w odpowiedniku tablicy importów (nomenklatura standardu określa go jako Function Index Space lub Table Index Space),
  • Wsparcie dla Control-Flow Integrity – na etapie kompilacji następuje sprawdzenie miejsc docelowych przebiegu kodu, rozpoczęcie niewłaściwej ścieżki powoduje awaryjne zakończenie programu i wysłanie informacji o tym do środowiska uruchomieniowego za pomocą mechanizmu pułapek (z ang. traps),
  • Podczas wywołania funkcji w sposób pośredni, walidowane są typy argumentów oraz zwracanej wartości, pod kątem zgodności z deklaracją funkcji,
  • Struktura zawierająca informacje o wywołaniach funkcji jest monitorowana pod kątem błędów przepełnienia stosu (ang. Stack Overflow) oraz modyfikacji adresów w niej zawartych.
  • Do kodu uruchomionego w przeglądarce, tak jak w przypadku JavaScript, aplikowane jest Same Origin Policy. Model bezpieczeństwa WebAssembly działającego poza przeglądarką jest oparty o standard POSIX.

Zaglądając “pod maskę” środowiska, warto przyjrzeć się jakie mechanizmy bezpieczeństwa wymuszane są przez najpopularniejszy kompilator C/C++ do WASM, Emscripten, oraz jakie zmiany w dobrze znanych sposobach ochrony z x86 wprowadza model WebAssembly:

  • Kanarek na stosie – odseparowane obszary pamięci kodu i danych powodują, że ochrona adresów powrotu za pomocą Stack Canaries nie jest wymagana.
  • Data Execution Prevention (DEP) – podobnie jak w punkcie wyżej, separacja przestrzeni kodu i danych jest wystarczająca do rezygnacji z tej metody ochrony.
  • Address Space Layout Randomization (ASLR) – brak implementacji w Emscripten,
  • Ochrona buforów na stercie – kompilator wykorzystuje zmodyfikowaną implementację dlmalloc, która nie jest zaprojektowana pod kątem bezpieczeństwa.
  • Wykrywanie wykorzystania niebezpiecznych funkcji języków C/C++ (np. gets) – Emscripten wymusza kompilację funkcji zgodnych ze standardem C99.

Dużym minusem standardu opierającego się o binaria, jest brak możliwości weryfikacji integralności oraz podpisu cyfrowego tychże. Powoduje to w scenariuszach dostarczenia aplikacji za pomocą nieszyfrowanego kanału, możliwość ich modyfikacji w locie za pomocą ataku man-in-the-middle i wykonanie zupełnie innego kodu niż zakładany przez autorów w kontekście przeglądarki użytkownika.

Mając na uwadze powyższe sposoby ochrony, słabości i ogólny model bezpieczeństwa przyjęty przez standard, czas na zastanowienie się, co mimo wyżej wymienionych można zepsuć oraz oczywiście jak można to zrobić :-)

Niebezpieczeństwa związane z wykorzystaniem WebAssembly

Z racji osadzenia środowiska wykonawczego wewnątrz piaskownicy JavaScript zagrożenia standardu należy rozpatrywać nie tylko pod kątem potencjalnych klas błędów kodu C/C++, lecz również z uwzględnieniem implementacji w przeglądarkach. Ciekawym obszarem badań są narzędzia do analizy binarnej kodu, które w znacznej mierze jeszcze nie zawierają obsługi binariów w formacie WebAssembly – w tym momencie jedynie umożliwia to radare2.

Wykorzystanie znanych typów podatności z C/C++

Pierwszym skojarzeniem z wykonywaniem skompilowanego kodu C/C++ są oczywiście wszystkie klasy błędów, jakie wiążą się z ręczną obsługą pamięci. Część typów podatności została wyeliminowana na etapie projektowania modelu bezpieczeństwa WebAssembly, nie mniej jednak nie udało się uszczelnić wszystkiego – autorzy standardu w dokumentacji wspominają o słabościach języka:

“Nevertheless, other classes of bugs are not obviated by the semantics of WebAssembly. Although attackers cannot perform direct code injection attacks, it is possible to hijack the control flow of a module using code reuse attacks against indirect calls. However, conventional return-oriented programming (ROP) attacks using short sequences of instructions (“gadgets”) are not possible in WebAssembly, because control-flow integrity ensures that call targets are valid functions declared at load time. Likewise, race conditions, such as time of check to time of use (TOCTOU) vulnerabilities, are possible in WebAssembly, since no execution or scheduling guarantees are provided beyond in-order execution and post-MVP atomic memory primitives. Similarly, side channel attacks can occur, such as timing attacks against modules.”

Wracając do tematyki ręcznego zarządzania pamięcią, zweryfikujmy ochronę przez podatnościami przepełnienia bufora i zakres ich realizacji. Mały eksperyment będzie przeprowadzony za pomocą kompilatora Emscripten (wersja 1.38.11). Test będzie prosty: próba nadpisania zmiennej zawierającej łańcuch znakowy za pomocą funkcji strcpy().

Program testowy prezentuje Listing nr 1.

#include <string.h>
#include <stdio.h>

int main() 
{
	char buf1[] = "XYZ";
	char buf2[] = "1337";
	// Ups!
	strcpy(buf2,"AAAAAAAAAAAAA");
	printf("%s", buf1);
		
	return 0;
}

Kompilacja: emcc –emrun buffer.c -o buffer.html

Uruchomienie: emrun buffer.html 

Efekt? Zmieniona zawartość zmiennej na konsoli Emscripten, czyli nadpisanie wartości buf1 wewnątrz Linear Memory. Z tego wynika, że ochrona realizowana jest wyłącznie na poziomie przepełnienia poza zakres pamięci danych przydzielonej dla danej aplikacji. Uniemożliwia to skuteczne nadpisanie adresu powrotu, lecz nie chroni przed nadpisaniem danych w lokalnej przestrzeni.

Najbardziej interesującymi problemami bezpieczeństwa są przypadki, które sprzyjają zmianie przebiegu wykonania kodu. Klasą błędów należącą do tej grupy jest type confusion – możliwość przemycenia innego typu obiektu niż pierwotnie zakładany przez program. Wykorzystaniu błędów tego typu zapobiega wcześniej opisane Control Flow Integrity, lecz domyślnie binarki nie są kompilowane z odpowiednim przełącznikiem (-fsanitize=cfi), co skutkuje możliwością wykorzystania błędów type confusion w sprzyjających warunkach: funkcja “ofiara” i “napastnik” mają takie same sygnatury.

Nie jest to rzadki przypadek, WebAssembly (oraz kompilator) upraszcza typowanie i typy różniące się w C/C++ są sprowadzane do tego samego typu WASM – potwierdza to kod wykorzystywany w kolejnym mini eksperymencie. Emscripten w kodzie mapuje tak samo typ całkowity oraz void – jest to typ i32 w WebAssembly.

Program ilustrujący problem zawiera dwa typy zawarte przez dewelopera oraz jeden “wstrzyknięty” o złośliwej charakterystyce. Wykorzystując niewłaściwy obiekt, który nie został poddany walidacji następuje uruchomienie dostarczonego kodu i wypisanie łańcucha znakowego „Evil::makeAdmin”, zamiast „Derived::printMe”. Jak wcześniej zostało wspomniane, sygnatury funkcji są różne: void printMe(int i) i void makeAdmin(void * i).

Kod testowy wraz z komentarzem – typeconfusion.cpp (źródło) prezentuje Listing nr 2.

#include <iostream>

struct Base {
   Base() {}
   virtual ~Base() {}
   virtual void printMe(int i) { 
       std::cout << "Base::printMe " << i << "\n";
   }
};

struct Derived : Base {
   Derived() {}
   virtual ~Derived() {}

   virtual void printMe(int i) {
       std::cout << "Derived::printMe " << i << "\n";
   }
};

// imagine this is an attacker-created structure 
// in memory
struct Evil {
   Evil() {}
   virtual ~Evil() {}

   virtual void makeAdmin(void * i) {
       std::cout << "CFI Prevents this control flow " << i << "\n";
       std::cout << "Evil::makeAdmin\n";
   }
};

int main(int argc, const char *argv[]) {

   Evil *eptr = new Evil();
   Derived* dptr = new Derived();

   (void)(argc);
   (void)(argv);

   dptr->printMe(55);
   
   // imagine a type confusion vulnerability
   // that does something similar
   dptr = reinterpret_cast<Derived*>(eptr);
   dptr->printMe(66);

   return 0;
}

Kompilacja (bez CFI): emcc –emrun typeconfusion.cpp -o typeconfusion.html 
Uruchomienie: emrun typeconfusion.html 

Dostarczony “złośliwy” kod testowy został uruchomiony (bez CFI), mimo walidacji typów po stronie WebAssembly. Na zakończenie eksperymentu sprawdźmy zatem jak ten sam program zachowa się z mechanizmem wymuszania integralności przepływu kodu.

Kompilacja (wraz z CFI): emcc –emrun -flto -fsanitize=cfi -fvisibility=hidden typeconfusion.cpp -o typeconfusion_cfi.html
Uruchomienie: emrun typeconfusion_cfi.html 

Działanie programu zostało przerwane awaryjnie, a środowisko wykonawcze zgłosiło ten fakt za pomocą trapa. Dla dociekliwych czytelników, poniżej fragment logu podczas naruszenia CFI.

Derived::printMe 55
trap!
trap!
exception thrown: abort("trap!") at jsStackTrace@http://localhost:6931/typeconfusion_cfi.js:1040:13
stackTrace@http://localhost:6931/typeconfusion_cfi.js:1057:12
abort@http://localhost:6931/typeconfusion_cfi.js:6440:44
_llvm_trap@http://localhost:6931/typeconfusion_cfi.js:5170:7
wasm-function[55]@http://localhost:6931/typeconfusion_cfi.js:18654:1
Module._main@http://localhost:6931/typeconfusion_cfi.js:6048:10
callMain@http://localhost:6931/typeconfusion_cfi.js:6313:15
doRun@http://localhost:6931/typeconfusion_cfi.js:6371:42
run/<@http://localhost:6931/typeconfusion_cfi.js:6382:7

Tak jak wspomnieli autorzy w dokumentacji, mimo zastosowanych mechanizmów bezpieczeństwa, da się wykorzystać pewne klasy błędów. CFI znacząco ogranicza exploitację błędów klasy type confusion, lecz niestety domyślnie nie jest wykorzystywany przez Emscripten.

Błędy implementacji środowiska w przeglądarkach

Implementacje silników JavaScript w przeglądarkach są dużymi projektami zawierającymi tysiące plików źródłowych – podając za przykład projekt WebKit JavaScriptCore: 9046 plików w języku C++; 983 w C oraz 12737 plików nagłówkowych. Skomplikowany kod oraz jego duża objętość w oczywisty sposób przekłada się na ilość potencjalnych błędów wprowadzanych wraz z kolejnymi wersjami. Zagadnieniu implementacji WebAssembly w najpopularniejszych przeglądarkach przyjrzała się Natalie Silvanovich, członek zespołu Google Project Zero. Efektem jest wykrycie poniższych podatności podczas parsowania plików binarnych WASM:

Należy również wspomnieć o pracy innych badaczy w tym samym obszarze:

Podatności narzędzi analitycznych

Tam gdzie pojawia się format binarny pojawiają się również analitycy oraz entuzjaści niskopoziomowych zagadek, do których skierowane są narzędzia do analizy binarnej. W trakcie pisania artykułu jedynym narzędziem posiadającym wsparcie “z pudełka” dla binarek WASM było radare2 (r2). Niestety, implementacja nie obyła się bez błędów, mających (na razie) niewielki wpływ na bezpieczeństwo.

Pierwszym problemem bezpieczeństwa, znalezionym przez autora artykułu, było klasyczne przepełnienie bufora na stosie w module deasemblera WebAssembly. Za jego pomocą możliwe było nadpisanie trzech bajtów zmiennej poprzedzającej bufor – szczęśliwie bez wpływu na wskaźnik zapisany w adresie powrotu. Powodem awarii było niewłaściwe obliczenie wielkości bufora, w parametrze przekazywanym do funkcji snprintf().

Poniżej skrócony raport AddressSanitizer (ASAN) tej podatności (GH Issue #9969):

==10261==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc7bb4d148 at pc 0x7f37210720a2 bp 0x7ffc7bb4cd50 sp 0x7ffc7bb4c4e0
WRITE of size 3 at 0x7ffc7bb4d148 thread T0
   #0 0x7f37210720a1 in __interceptor_vsnprintf (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x610a1)
   #1 0x7f37210723b1 in snprintf (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x613b1)
   #2 0x7f371e34b064 in wasm_dis XYZ/radare2/libr/..//libr/anal/p/../../asm/arch/wasm/wasm.c:421
   #3 0x7f371e347fb4 in wasm_op XYZ/radare2/libr/..//libr/anal/p/anal_wasm.c:28

Address 0x7ffc7bb4d148 is located in stack of thread T0 at offset 360 in frame
   #0 0x7f371e347e8f in wasm_op XYZ/radare2/libr/..//libr/anal/p/anal_wasm.c:24

 This frame has 3 object(s):
   [32, 36) 'n'
   [96, 360) 'wop' <== Memory access at offset 360 overflows this variable
   [416, 480) 'flgname'
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
     (longjmp and C++ exceptions *are* supported)

Druga znaleziona podatność miała takie same podłoże problemu: niewłaściwe obliczanie wielkości bufora – tym razem przeznaczonego do odczytu. Ujawnienie pamięci, poza buforem, było niewielkie, dotyczyło tylko jednego bajta.

Raport ASAN (GH Issue #7265):

==17478==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6110000054c4 at pc 0x7f7669604713 bp 0x7fffa92e0f30 sp 0x7fffa92e0f20
READ of size 1 at 0x6110000054c4 thread T0
    #0 0x7f7669604712 in consume_init_expr XYZ/radare2/libr/..//libr/bin/p/../format/wasm/wasm.c:72
    #1 0x7f7669604712 in r_bin_wasm_get_data_entries XYZ/radare2/libr/..//libr/bin/p/../format/wasm/wasm.c:476
    #2 0x7f7669604712 in r_bin_wasm_get_datas XYZ/radare2/libr/..//libr/bin/p/../format/wasm/wasm.c:1167
    #3 0x7f7669604b02 in r_bin_wasm_init XYZ/radare2/libr/..//libr/bin/p/../format/wasm/wasm.c:715
    #4 0x7f766932fc1d in r_bin_object_new XYZ/radare2/libr/bin/bin.c:1300
    #5 0x7f766933393e in r_bin_file_new_from_bytes XYZ/radare2/libr/bin/bin.c:1524
    #6 0x7f766933393e in r_bin_load_io_at_offset_as_sz XYZ/radare2/libr/bin/bin.c:1079
    #7 0x7f7669334dc2 in r_bin_load_io_at_offset_as XYZ/radare2/libr/bin/bin.c:1093
    #8 0x7f7669335ee5 in r_bin_load_io XYZ/radare2/libr/bin/bin.c:936
    #9 0x7f766a5d8ad5 in r_core_file_do_load_for_io_plugin XYZ/radare2/libr/core/file.c:429
    #10 0x7f766a5d8ad5 in r_core_bin_load XYZ/radare2/libr/core/file.c:566
    #11 0x560ea6a74c34 in main XYZ/radare2/binr/radare2/radare2.c:929
    #12 0x7f766450682f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
    #13 0x560ea6a78468 in _start (/usr/local/bin/radare2+0xe468)

0x6110000054c4 is located 0 bytes to the right of 196-byte region [0x611000005400,0x6110000054c4)
allocated by thread T0 here:
    #0 0x7f766abb7602 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x98602)
    #1 0x7f766498c179 in r_buf_set_bytes XYZ/radare2/libr/util/buf.c:289

Podsumowanie

W artykule został zawarty przegląd modelu bezpieczeństwa i zagrożeń WebAssembly na różnych płaszczyznach: kodu C/C++, implementacji środowiska wykonawczego w przeglądarkach oraz narzędziach do analizy binarnej.

WebAssembly jest dobrze przemyślaną technologią, która pozwala na budowanie bezpiecznych modułów aplikacji webowych. Rysą na ogólnym dobrym wrażeniu jest brak walidacji integralności i podpisu cyfrowego, co w skrajnych wypadkach może skutkować wykonaniem niezaufanego / zmodyfikowanego kodu podczas ataku man-in-the middle. Warto również wspomnieć o braku domyślnego wykorzystania CFI, które może uchronić przed powodzeniem ataku, którego częścią jest zmiana przebiegu programu.

– Kamil Frankowicz

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. k4

    Jak tak dalej pójdzie to za jakiś czas przeglądarka bedzie sprowadzona do roli sandboxa/VM. W zasadzie to co nam te wszystkie technologie webowe? NIe lepiej używać obrazów VM zamiast stron ?

    Odpowiedz
    • jozek

      Pisałem na ten temat kilkustronicowy komentarz, który skasowałem.

      Jesteśmy jakieś 2 kroki od tego momentu, wystarczy, że ktoś się skapnie, że można odpalić Akka/Verte.x w Javie do obsługi tryliardów zapytań na najlepszej możliwej formie skalowania i ktoś ogarnie, że można zastosować Phoenixa z Elixiru do robienia frontu w przeglądarce.

      Przeglądarka nigdy nie stanie się samym VMem, bo znacząco wydłużałbyś otwieranie stron i ich możliwości responsywne (cholera, pamięć, procesor, łącze, bardzo dużo zmiennych), ale już odpalanie lekkiego kodu na jakimś silniku (czym obecnie jest gówniany JavaScript) da się zrobić, prosta biblioteka łącząca Elixir i front w Pheonixie z Javą na podstawie REST albo Redist i tworzysz aplikacje kilkukrotnie wydajniejsze od obecnych, ale uśmiercasz też wszystkich, którzy się poświęcili PHP, JS czy Ruby.

      Java… Java sama w sobie, jest gównem. Przecież sama struktura tego języka, obecna wersja 9, czy składnia itp. to niesamowite gówno, nawet mi się nie chce opisywać w jak wielu aspektach. Ale jak dodasz do tego wszystkie frameworki i biblioteki robione przez społeczność, to nagle się okazuje, że nic Javie nie może dorównać. Jedyne czego brakuje, to bibliotek gwarancyjnych poprawność kodu, coś takiego, jak pisanie w ADA kodu strukturalnego, do aplikacji wysokiego poziomu pewności.

      Wtedy Java była by kompletna. I mógłbyś nauczyć się jednego języka, który umożliwia wszystkiego począwszy od robienia aplikacji internetowych, przez mobilne, desktopowe, po programowanie mikrokontrolerów aż po wysyłanie łazików na Marsa.

      Odpowiedz
      • Tony Hołk

        Zdaje się, że aplikacjami Java w przeglądarce już kiedyś próbowano ;)

        Odpowiedz
      • z33k

        Widzę tę twoją wizję, tylko zamiast Javy jest tam Python :)

        Odpowiedz
      • Mike
        Odpowiedz

Odpowiedz na jozek