WebAssembly의 모든 globals
, locals
, functions
, instructions
에는 타입이 존재하고, 바이너리 실행 전에 이 모든 타입들이 statically type-checked 된다. WebAssembly에는 4가지의 primitive type
만 존재하는데, 이들은 32-bit integer(i32
), 64-bit integer(i64
), single-precision float(f32
), double-precision float(f64
)이다. 이렇게 4가지의 primitive type
만 존재한다면, WebAssembly는 4바이트의 int
타입과 2바이트의 short
타입을 어떻게 구분할까? 나는 이를 직접 컴파일하여 확인해보기로 하였다.
Emscripten
은 C/C++ 파일을 WebAssembly로 컴파일할 수 있도록 도와주는 LLVM 기반 오픈소스 컴파일러이다. 이를 다운받고 설치하는 과정은 이전 포스팅에 나와 있다.
WABT
는 WebAssembly Binary Toolkit
의 약자로, WebAssembly와 관련된 여러가지 tool들을 모아놓은 것이다. 나는 이 중에 WebAssembly 바이너리 파일을 WebAssembly Text Format 파일로 바꿔주는 wasm2wat
tool을 이용하기 위해 이를 설치하였다.
WABT
설치 과정은 WABT 깃허브에 자세히 나와있는데, 아래와 같은 command를 순차적으로 실행하여 설치할 수 있다.
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ git submodule update --init
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
그리고, 아래와 같은 명령어를 통하여 wabt
디렉토리의 bin
디렉토리를 환경 변수에 등록하여 현재 위치와 상관 없이 bin
디렉토리의 tool들을 실행할 수 있도록 하였다.
//$PATH: 뒤에 wabt 디렉토리 안의 bin 디렉토리의 위치를 입력하면 된다.
$ export PATH=$PATH:~/wabt/bin
먼저, 아래와 같은 test.c
파일을 작성해보았다.
// test.c
#include <stdio.h>
int add (int, int);
int main(void) {
add(1, 2);
return 0;
}
int add (int a, int b) {
return a + b;
}
그리고, emcc test.c -o test.js
명령어를 통해 생성된 WebAssembly 바이너리 포맷 파일인 test.wasm
파일을 wasm2wat
을 이용하여 WebAssembly Text 포맷 파일인 test.wat
으로 변경해주었다.
$ wasm2wat test.wasm -o test.wat
그리고, test.wat
파일을 열어 확인해보면 파일 앞부분에 함수의 parameter들의 타입과 return 타입이 명시된 Function Signature
들이 나열되어 있는 것을 확인할 수 있었다.
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32)))
(type (;2;) (func))
(type (;3;) (func (param i32) (result i32)))
(type (;4;) (func (param i32 i32) (result i32)))
(type (;5;) (func (param i32 i32 i32) (result i32)))
(type (;6;) (func (param i32 i64 i32) (result i64)))
...
우리가 위에서 작성한 add
함수의 시그니처는 type 4
에 명시되어 있었다.
이번에는 add
함수의 parameter 타입과 return 타입을 모두 double
로 바꾼 test2.c
파일을 작성해보았다.
// test2.c
#include <stdio.h>
double add (double, double);
int main(void) {
add(1, 2);
return 0;
}
double add (double a, double b) {
return a + b;
}
그리고, Emscripten
을 통해 이를 컴파일하고 .wasm
파일을 wasm2wat
을 이용해 .wat
파일로 바꾸고, test2.wat
파일의 앞부분을 확인해보았다.
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32)))
(type (;2;) (func))
(type (;3;) (func (param i32) (result i32)))
(type (;4;) (func (param f64 f64) (result f64)))
(type (;5;) (func (param i32 i32) (result i32)))
(type (;6;) (func (param i32 i32 i32) (result i32)))
(type (;7;) (func (param i32 i64 i32) (result i64)))
...
type 4
의 함수 시그니처가 [i32, i32] -> i32
에서 [f64, f64] -> f64
로 바뀐 것을 확인할 수 있었다.
그렇다면, WebAssembly에서 4바이트의 int
타입과 2바이트의 short
, char
타입은 어떻게 구분할까? 이번에는 add
함수의 parameter와 return 타입을 모두 short
, char
로 바꾼 test3.c
, test4.c
파일을 작성해보았다.
// test3.c
#include <stdio.h>
short add (short, short);
int main(void) {
add(1, 2);
return 0;
}
short add (short a, short b) {
return a + b;
}
// test4.c
#include <stdio.h>
char add (char, char);
int main(void) {
add(1, 2);
return 0;
}
char add (char a, char b) {
return a + b;
}
그리고, 이 두 파일을 Emscripten
을 이용하여 컴파일 한 이후, wasm2wat
을 이용하여 .wat
파일을 생성하고 파일의 앞 부분을 확인했더니, 두 파일 모두 아래와 같이 test.wat
파일의 함수 시그니처와 동일한 결과가 나온 것을 확인할 수 있었다.
(module
(type (;0;) (func (result i32)))
(type (;1;) (func (param i32)))
(type (;2;) (func))
(type (;3;) (func (param i32) (result i32)))
(type (;4;) (func (param i32 i32) (result i32)))
(type (;5;) (func (param i32 i32 i32) (result i32)))
(type (;6;) (func (param i32 i64 i32) (result i64)))
...
즉, 32-bit 이하의 정수형 데이터 타입들은 모두 WebAssembly에서 i32
타입으로 치환되어 소스 코드에서는 서로 다른 함수 시그니처를 가지고 있는 함수들이 WebAssembly로 컴파일되면 같은 함수 시그니처를 가지게 될 수 있다는 것이다.
WebAssembly의 취약점을 분석한 논문을 보면, WebAssembly의 Indirect Call 작동 원리에 대해 나와있는데, call_indirect
instruction이 stack의 값을 pop하게 되는데, 이는 table section
의 index로 쓰이게 되어 해당 index에 저장되어 있는 함수를 불러오게 된다. 그리고, type-correctness를 보장하기 위해서 VM은 target-function 호출을 실행하기 전에 indirect call instruction에서 정적으로 선언된 type과 target-function type이 호환되는 지 확인하고, 호환되지 않으면 실행을 중단한다.
이렇듯, WebAssembly의 Indirect Call은 함수를 호출하기 전에 함수 시그니처를 확인하여 정적 type-checking을 통해 적절하지 않은 함수를 호출하지 못하도록 하는데, 앞서 살펴본 것처럼 C와 같은 다른 언어로 작성된 소스 코드에서는 다른 시그니처를 가지고 있는 함수들이 WebAssembly로 컴파일되면 같은 시그니처를 가지게 되어서 Indirect Call을 실행하였을 때 그러한 함수들이 모두 type-checking을 통과하여 실행될 수 있다는 사실을 추측해볼 수 있다.
WebAssembly는 32-bit 이하의 정수 타입들을 모두 i32
타입으로 치환하며, 이로 인하여 본래의 소스 코드에서 다른 시그니처를 가지고 있던 함수들이 WebAssembly로 컴파일 되면 같은 시그니처를 가지게 될 수 있다. 그리고 이러한 사실은 WebAssembly Indirect Call의 취약점을 유발할 수 있다.