RenderFrameHost系列(一)

作者: 360漏洞研究院 koocola 分类: 安全研究,漏洞分析 发布时间: 2022-08-01 06:26

在2020年,就出现过针对RenderFrameHost的攻击,当时由于RenderFrameHost的生命周期问题出过很多漏洞,很容易用于沙箱逃逸,在当时甚至作为CTF题出现在了plaid ctf中。但由于RenderFrameHost实现上的问题,在2021年末开始,各种变种漏洞又出现了,这个系列就来分析下2021年末开始出现的各种RenderFrameHost的变种漏洞。

关于RenderFrameHost

什么是RenderFrameHost?

当chrome打开一个新的网页时,会创建一个Webcontents对象表示这个新打开的网页,同时浏览器进程会创建出一个新的渲染器进程。此过程将解析网站的内容,例如 JavaScript、HTML 和 CSS,并将其显示在其主框架上。为了跟踪主框架并与之通信,浏览器进程将实例化一个[RenderFrameHostImpl(RFH)对象](https://source.chromium.org/chromium/chromium/src/+/master:content/browser/renderer_host/render_frame_host_impl.h;l=252;drc=8f5b7ee843864f30c9483a8c64afa0433e2e9b90)来表示渲染器的主框架。

更复杂的是,一个网站可能有多个“子框架”(又名 iframe),它们将在主框架内嵌入另一个页面上下文,可以随时由 JavaScript 创建和销毁。浏览器进程将反映这种行为并为每个新的子框架也创建一个新的 [RenderFrameHostImpl](https://source.chromium.org/chromium/chromium/src/+/master:content/browser/renderer_host/render_frame_host_impl.h;l=252;drc=8f5b7ee843864f30c9483a8c64afa0433e2e9b90)对象。(这两段节选自这里

举一个简单的例子

假设我们在浏览器中打开了Foo.html,Foo.html的内容如下所示

<iframe src = "Foo1.html">
<iframe src = "Foo2.html">

那么就会生成如下图所示的结构,一个Webcontents对象中保存有Foo.html,Foo1.html,Foo2.html三个RenderFrameHostImpl对象,其中Foo.html为主框架,而Foo1.html,Foo2.html为子框架。

RenderFrameHost的正常使用

chrome里有很多类将RenderFrameHost对象以原始指针的形式保存为类成员,这些类用了很多不同的方法确保存储的RenderFrameHost对象仍然存活,其中较为普遍的一个做法是让类继承WebContentsObserver,如果一个类继承了WebContentsObserver,那么就可以调用observe函数指定自己要观察的WebContents对象并重载其RenderFrameDeleted函数。完成这些操作后,该Webcontents下的任何RenderFrameHost析构时都会调用其重载的RenderFrameDeleted函数并在调用的时候把要析构的RenderFrameHost原始指针作为第一个参数。

示例

如下图的FileSelectHelper类保存了RenderFrameHost的原始指针,其继承了content::WebContentsObserver类

class FileSelectHelper : public base::RefCountedThreadSafe<
                             FileSelectHelper,
                             content::BrowserThread::DeleteOnUIThread>,
                         public ui::SelectFileDialog::Listener,
                         public content::WebContentsObserver,
                         public content::RenderWidgetHostObserver,
                         private net::DirectoryLister::DirectoryListerDelegate {
...
private:
content::RenderFrameHost * render_frame_host_;
...
}

在FileSelectHelper类中保存了RenderFrameHost的原始指针[1],通过observe 当前保存的render_frame_host_所在的webcontents来将自己注册为当前Webcontents的一个观察者[2],

void FileSelectHelper::RunFileChooser(
content::RenderFrameHost* render_frame_host,
scoped_refptrcontent::FileSelectListener listener,
FileChooserParamsPtr params) {
render_frame_host_ = render_frame_host;  ---------------[1]
web_contents_ = WebContents::FromRenderFrameHost(render_frame_host);
...
content::WebContentsObserver::Observe(web_contents_);  -----------[2]
...
}

注册完成后,重载RenderFrameDeleted函数来处理render_frame_host被释放的情况,如下图,因为当下Webcontents中可以存在不止一个RenderFrameHost, 任何RenderFrameHost被析构时都会通知FileSelectHelper::RenderFrameDeleted,所以需要判断RenderFrameDeleted调用时的第一个参数的值是否和当前类中所保存的RenderFrameHost指针的值一致,如果一致,则将其置空,防止因dangling pointer造成的UAF问题。

void FileSelectHelper::RenderFrameDeleted(
    content::RenderFrameHost* render_frame_host) {
  if (render_frame_host == render_frame_host_)
    render_frame_host_ = nullptr;
}

各类安全问题

第一类安全问题

第一类安全问题就是2020年出现过的那一波漏洞的漏洞模式,最原始的一类问题,因为保存RenderFrameHost对象的类的生命周期和RenderFrameHost对象的生命周期不一致,又没有通过继承WebcontentsObserver等手段对被释放的RenderFrameHost指针进行清零导致的RenderframeHost对象析构后在该类中再次引用造成的uaf问题。想要了解可以看这里,也可以在这里看中文翻译版本。

第二类安全问题

过去了1年半,RenderFrameHost相关的问题以在野利用的形式又回来了,下面我们来分析一下。

如下图所示,让我们看一下在RenderFrameHost对象析构的时候是如何实现通知其观察者的,RenderFrameHost对象在析构的时候会调用delegate_->RenderFrameDeleted[4],delegate_就是当前RenderFrameHost所在页面的webcontents指针,然后在WebContentsImpl::RenderFrameDeleted中,又会通知所有observe了当前WebContents的对象当前的RenderFrameHost指针[5]已经被释放了。

这里面有个问题,如图中[3]处所示,这个通知事件是条件式的,只有满足了was_created == True的前提下,才会进行通知,而要想满足was_created == True,则需要当前状态render_frame_state_ == RenderFrameState::kCreated[6],这个条件式的通知,引起了多个严重的安全问题。

RenderFrameHostImpl::~RenderFrameHostImpl() {
...
const bool was_created = is_render_frame_created();
if (was_created) -----------------[3]
    delegate_->RenderFrameDeleted(this);//通知其所在的webcontents-----------[4]
...
}
void WebContentsImpl::RenderFrameDeleted(
    RenderFrameHostImpl* render_frame_host) {
  {
    SCOPED_UMA_HISTOGRAM_TIMER("WebContentsObserver.RenderFrameDeleted");
    observers_.NotifyObservers(&WebContentsObserver::RenderFrameDeleted,
                               render_frame_host);------------------[5]
  }
  ......
}
bool is_render_frame_created() {
    return render_frame_state_ == RenderFrameState::kCreated;-----------[6]
  }

这里再稍微补充点知识在挖掘沙箱漏洞时,我们可以假定沙箱内的renderer进程是完全可由攻击者控制的,下文的browser进程就是chrome中的一个运行在沙箱外的进程,renderer进程可以和browser进程通过the old-IPC/Legacy IPC Mojo IPC进行通信。

当renderer进程想要创建子框架时,会向Browser进程发起mojo调用CreateChildFrame,CreateChildFrame的参数有很多,我们这里要关注的只有两个参数,一个是browser_interface_broker[7],另一个是frame_owner_element_type[8]。我们注意到browser_interface_broker的类型为pending_receiver,在通过Mojo IPC通信时,我们会生成pending_receiver和pending_remote类型的两个指针,remote指针可以向receiver指针发起其对应类型的Mojo调用,在这里我们将pending_receiver类型的指针传递给了browser,browser进行绑定后,renderer保存的remote指针发起的Mojo调用就可以被Browser对应函数处理。

interface FrameHost {
  // Asynchronously creates a child frame. A routing ID must be allocated first
  // by calling RenderMessageFilter::GenerateFrameRoutingID()
  // Each of these messages will have a corresponding mojom::FrameHost::Detach
  // API sent when the frame is detached from the DOM.
  CreateChildFrame(....,
    pending_receiver<blink.mojom.BrowserInterfaceBroker> browser_interface_broker,-------[7]
    ...,
  blink.mojom.FrameOwnerElementType frame_owner_element_type); -----------[8]
[...]
}

Browser进程处理CreateChildFrame请求时会执行到FrameTree::AddFrame函数,FrameTree::AddFrame调用added_node->current_frame_host()->RenderFrameCreated()[11]将RenderFrameHost的当前状态render_frame_state_设置为RenderFrameState::kCreated[13]。

到这里问题又出现了,可以看到RenderFrameCreated[11]的调用也是条件式的,只有在满足!is_dummy_frame_for_portal_or_fenced_frame == True的前提下RenderFrameCreated才会被调用。

设想一下如果RenderFrameCreated没有被调用,那么在整个创建出来的RenderFrameHostImpl对象存在时,render_frame_state将一直是初始值RenderFrameState::kNeverCreated[12],也就是说上文中提到的RenderFrameHostImpl在析构时,was_created的值将一直为false, RenderFrameHostImpl将永远不会通知他的观察者自己被释放了。

而要想让!is_dummy_frame_for_portal_or_fenced_frame == False,只需要满足owner_type == blink::mojom::FrameOwnerElementType::kPortal[9],owner_type的值是我们最初发起调用时传入的frame_owner_element_type,可以由攻击者控制。

FrameTreeNode* FrameTree::AddFrame(
    ....
    blink::mojom::FrameOwnerElementType owner_type) {
 ...
  bool is_dummy_frame_for_portal_or_fenced_frame = // *** 3 ***
      owner_type == blink::mojom::FrameOwnerElementType::kPortal ||
      (owner_type == blink::mojom::FrameOwnerElementType::kFencedframe &&
       blink::features::kFencedFramesImplementationTypeParam.Get() ==
           blink::features::FencedFramesImplementationType::kMPArch);------[9]
....
    added_node->current_frame_host()->BindBrowserInterfaceBrokerReceiver(
        std::move(browser_interface_broker_receiver)); --------------[10]
...
  if (!is_dummy_frame_for_portal_or_fenced_frame) {
    // The outer dummy FrameTreeNode for both portals and fenced frames does not
    // have a live RenderFrame in the renderer process.
    added_node->current_frame_host()->RenderFrameCreated();  ----------------[11]
  }
...
}
enum class RenderFrameState {
    kNeverCreated = 0,
    kCreated,
    kDeleting,
    kDeleted,
  };
  RenderFrameState render_frame_state_ = RenderFrameState::kNeverCreated; -----[12]
void RenderFrameHostImpl::RenderFrameCreated() {
...
    render_frame_state_ = RenderFrameState::kCreated;------------[13]
...
    SetUpMojoConnection();  ---------------------[14]
...
}

所以,我们只需要向browser端发起一个frame_owner_element_type类型为blink::mojom::FrameOwnerElementType::kPortal的CreateChildFrame的调用,就可以创建出一个在析构时永远不会通知观察者的畸形RenderFrameHostImpl对象。

但创建出这样一个对象本身并不会导致漏洞,因为我们需要达成的场景是某个依赖于RenderFrameDeleted函数保证RenderFrameHostImpl对象生命周期的类中保存了这样一个畸形的RenderFrameHostImpl对象,在查看了这些类引入RenderFrameHost原始指针的方式后发现,大部分

类都需要RenderFrameHost代表的子框架在创建时调用了RenderFrameCreated函数[11]后才会保存RenderFrameHost对象的原始指针,因为在RenderFrameCreated函数SetUpMojoConnection[14]时,会为创建出的子框架在browser端绑定很多Mojo接口,这些Mojo接口中的一些Mojo调用能把表示子框架的RenderFrameHost原始指针传递给这些类,所以我们创建出这样一个畸形的RenderFrameHost对象,在避免了RenderFrameCreated调用的同时,也阻止了RenderFrameCreated建立这些Mojo连接,没有这些Mojo连接,我们就没有办法通过Mojo调用来达成我们需要的场景。

如果能保证在RenderFrameCreated不被调用也就是render_frame_state_为RenderFrameState::kNeverCreated的条件下,不会再有继承自WebcontentsObserver的类保存了指向该RenderFrameHostImpl的原始指针,那么就不会有因为析构时通知不到而造成的UAF问题。

所以现在的问题来到了如何把这样一个RenderFrameHost对象的原始指针传递给另一个类。

我们可以看到FrameTree::AddFrame调用了BindBrowserInterfaceBrokerReceiver函数[上文10],BindBrowserInterfaceBrokerReceiver函数绑定了renderer传递过来的receiver指针,在绑定之后renderer就可以通过remote指针调用GetInterface 来绑定mojo接口。可以看到绑定的browser interface broker可以调用GetInterface[16]方法,GetInterface方法需要传递一个receiver指针。

熟悉沙箱的同学应该知道,如果这个Mojo IPC接口能被调用,我们就可以通过其绑定browser_interface_binders.cc中的一些接口集,在enable MojoJS的时候也可以通过JS来进行绑定,但由于文章比较新手向,所以下面介绍下为什么可以绑定这些接口集。

interface BrowserInterfaceBroker {
    // Asks the browser to bind |receiver| to its remote implementation
    // in the browser process.
    GetInterface(mojo_base.mojom.GenericPendingReceiver receiver);
}; --------------------------[16]

Browser端在处理Renderer发来的GetInterface请求时会执行到如下的TryBind函数,在TryBind函数里会首先判断传进来的receiver的interface_name是不是存在binders_的map里[17],如果在map里就会调用BindInterface[18]函数,在BindInterface中会调用保存在map里面的该接口类型对应的回调callback_[19]20

bool TryBind(mojo::GenericPendingReceiver* receiver) {
    static_assert(IsVoidContext::value,
                  "TryBind() must be called with a context value when "
                  "ContextType is non-void.");
    auto it = binders_.find(*receiver->interface_name());--------------[17]
    if (it == binders_.end())
      return false;

    it->second->BindInterface(receiver->PassPipe());--------------[18]
    return true;
  }
void BindInterface(const std::string& interface_name,
                     mojo::ScopedMessagePipeHandle handle,
                     BinderArgs... args) override {
    mojo::PendingReceiver<Interface> receiver(std::move(handle));
    if (task_runner_) {
      task_runner_->PostTask(
          FROM_HERE, base::BindOnce(&CallbackBinder::RunCallback, callback_,
                                    std::move(receiver), args...));--------[19]
    } else {
      RunCallback(callback_, std::move(receiver), args...);----------[20]
    }
  }

为了找到callback_和receiver在map里的对应关系我们查看binders_的引用可以发现,在如下的Add函数中向binders_添加了新元素。

void Add(std::common_type_t<BinderType<Interface>> binder,
           scoped_refptr<base::SequencedTaskRunner> task_runner = nullptr) {
    binders_[Interface::Name_] = std::make_unique<
        internal::GenericCallbackBinderWithContext<ContextType>>(
        Traits::MakeGenericBinder(std::move(binder)), std::move(task_runner));
  }

而对Add函数的引用进行追溯,可以定位到BrowserInterfaceBrokerImpl的构造函数

BrowserInterfaceBrokerImpl(ExecutionContextHost* host)
      : host_(host) {
    // The populate functions here define all the interfaces that will be
    // exposed through the broker.
    //
    // The `host` is a templated type (one of RenderFrameHostImpl,
    // ServiceWorkerHost, etc.). which allows the populate steps here to call a
    // set of overloaded functions based on that type. Thus each type of `host`
    // can expose a different set of interfaces, which is determined statically
    // at compile time.
    internal::PopulateBinderMap(host, &binder_map_);
    internal::PopulateBinderMapWithContext(host, &binder_map_with_context_);
  }

BrowserInterfaceBrokerImpl的构造函数通过internal::PopulateBinderMap和internal::PopulateBinderMapWithContext函数中调用的Add来向binders_添加新元素。

internal::PopulateBinderMap函数会调用PopulateFrameBinders,下面是PopulateFrameBinders的部分代码,(完整代码在这里

可以看到PopulateFrameBinders向Add函数传递了绑定的接口类型,和一个保存有RenderFrameHostImpl原始指针的回调函数(回调函数的具体实现),在Add中会将该类型和其对应的回调函数保存到map里,到这里我们就找到了GetInterface可以传递的receiver指针的类型,和不同类型对应的回调处理函数。

void PopulateFrameBinders(RenderFrameHostImpl* host, mojo::BinderMap* map) {
  map->Add<blink::mojom::AudioContextManager>(base::BindRepeating(
      &RenderFrameHostImpl::GetAudioContextManager, base::Unretained(host)));

  map->Add<device::mojom::BatteryMonitor>(
      base::BindRepeating(&BindBatteryMonitor, base::Unretained(host)));

  map->Add<blink::mojom::CacheStorage>(base::BindRepeating(
      &RenderFrameHostImpl::BindCacheStorage, base::Unretained(host)));

  map->Add<blink::mojom::CodeCacheHost>(base::BindRepeating(
      &RenderFrameHostImpl::CreateCodeCacheHost, base::Unretained(host)));

  if (base::FeatureList::IsEnabled(blink::features::kComputePressure)) {
    map->Add<blink::mojom::PressureService>(base::BindRepeating(
        &PressureServiceImpl::Create, base::Unretained(host)));
  }

  .........

    map->Add<blink::mojom::FileChooser>(
      base::BindRepeating(&FileChooserImpl::Create, base::Unretained(host)));------[21]
  ..........
}

我们跟进blink::mojom::FileChooser类型[21]的回调处理函数FileChooserImpl::Create,代码如下,可以看到其new了一个FileChooserImpl类,将并receiver和new出来的FileChooserImpl对象通过MakeSelfOwnedReceiver进行绑定,在这样绑定后,new出来的FileChooserImpl对象只有在receiver的Mojo连接断开后才会被析构。之后查看FileChooserImpl的构造函数,将可以看到FileChooserImpl类保存了RenderFrameHost的原始指针,并通过observe设置其为当前Webcontents的观察者,就像前面讲的那样。

void FileChooserImpl::Create(
    RenderFrameHostImpl* render_frame_host,
    mojo::PendingReceiver<blink::mojom::FileChooser> receiver) {
  mojo::MakeSelfOwnedReceiver(
      base::WrapUnique(new FileChooserImpl(render_frame_host)),
      std::move(receiver));
}
FileChooserImpl::FileChooserImpl(RenderFrameHostImpl* render_frame_host)
    : render_frame_host_(render_frame_host) {
  Observe(WebContents::FromRenderFrameHost(render_frame_host));----------[1]
}

所以回到我们最开始的GetInterface调用,我们只要让renderer通过BrowserInterfaceBroker向browser端发送第一个参数类型为FileChooserImpl的GetInterface调用,就能把析构时永远不会通知观察者的RenderFrameHost对象的原始指针传递给一个继承自WebcontentsObserver的类。

interface BrowserInterfaceBroker {
    // Asks the browser to bind |receiver| to its remote implementation
    // in the browser process.
    GetInterface(mojo_base.mojom.GenericPendingReceiver receiver);
}; --------------------------[8]

漏洞触发思路:

  1. renderer端调用CreateChildFrame传递kportal类型创建一个render_frame_state_ 为RenderFrameState::kNeverCreated的子frame。
  2. renderer端调用GetInterface传递FileChooserImpl类型的receiver指针,创建出一个保存有子框架renderframehost原始指针的FileChooserImpl类。
  3. 移除子框架,触发子框架的renderframehost析构,此时由于子frame的render_frame_state_ 为RenderFrameState::kNeverCreated,所以在子frame的renderframehost析构时并不会通知FileChooserImpl类将其原始指针置为空。
  4. 通过renderer端保存的blink::mojom::FileChooser的remote指针进行mojo调用,在用到RenderFrameHost原始指针时会造成UAF。

漏洞补丁:

此处,不再允许CreateChildFrame传递Kportal类型,自此无法通过这种方式创建出一个析构时永远不会通知其观察者的RenderFrameHost对象。

漏洞利用:

在完成漏洞触发的思路中的1-3后,后续利用就和2020年的第一类安全问题一致了,网上有很多资料,这里就不浪费篇幅讲了。

后续

本文是RenderFrameHost系列的第一篇,后续系列将分享更多这类漏洞的变体。