Summary
On July 30, 2019, Google released chromium 76.0.3809.87, which fixes multiple vulnerabilities. In addition to the vulnerability on the list, an unpublished vulnerability has disappeared due to the partial refactoring of Chrome's indexeddb. Based on the results of the repair, it changes the way that the database can be opened in memory. Chromium finally fixed the vulnerability by replacing the original pointer with a smart pointer.
In this article, we will explore the vulnerability and the root cause, and show how to escape from the Chrome sandbox and finally realize the exploitation. We use this vulnerability as an example to demonstrate how to change the interprocess information disclosure in an IPC interface open to the sandbox rendering process into arbitrary code execution in the browser process outside the sandbox. Finally, I will share the POC code, which can be used to generate a reverse shell.
Although the POC code of the vulnerability discussed in this article is very similar to other exploit codes of chromium, many details are different. We hope to provide you with some references to help you understand how to exploit the low-level vulnerabilities in the open IPC interface, and finally turn into Google Chrome sandbox escape.
The following analysis is for the stable version of chromium on Android, version 75.0.3770.89.
IndexedDB
The indexeddb API is a permanent structured storage client database. Web application can store a large amount of data on user's browser. Data is stored in the form of key values, which can be complex structured JavaScript objects. IndexedDB is based on the transactional database model, and every operation of the database is based on the context of the transaction.
The specific implementation of indexeddb in Chrome is more complex, and various IPC interfaces are opened to sandbox rendering process, which makes it the best target to find Chrome sandbox escape vulnerability.
Mojo interface
Most of indexeddb in Chrome is implemented in the browser process. There are several different mojo IPC interfaces in browser and rendering, which are used for communication between processes, and enable sandbox rendering to perform indexeddb operations.
The idbfactory mojo interface is the main entry point for rendering. In some practical operations, it provides the open method. In the idbfactory JavaScript interface, it corresponds to the open () method, which can be used to request to open the connection with the database.
open()
open()
Next, we will discuss two practical methods provided by the idbfactory interface: aborttransactionsandcompactdatabase and aborttransactionsfordatabase. Call one of them to abort the database transaction. Interestingly, this version of the renderer has never used these functions.
AbortTransactionsAndCompactDatabase
AbortTransactionsForDatabase
AbortTransactionsAndCompactDatabase
AbortTransactionsForDatabase
In the rendering process, two parameters are passed to the open method through the pointers of idbcallbacks and idbdatabasecallbacks in the mojo interface. The former is used by the browser process to return rendering for a single request, and the latter is used to prompt the rendering for the request related out of band events.
Once the open method is called successfully, the browser returns a pointer to the idbdatabase interface to the renderer. The idbdatabase interface provides all the methods to open a database. After using the database, the renderer will call the close method on the idbdatabase interface to close the connection with the browser-side database.
In fact, there are many mojo interfaces defined for indexeddb. We will only discuss those mentioned above. You can see the complete mojo indexeddb interface list in third party / blink / public / mojom / indexeddb / indexeddb.mojom.
third_party/blink/public/mojom/indexeddb/indexeddb.mojom
third_party/blink/public/mojom/indexeddb/indexeddb.mojom
Databases, connections and requests
Indexeddb has concepts about databases and connections. For chrome indexeddb, it is represented by indexeddbdatabase and indexeddbconnection classes, respectively. There can be multiple connections to the same database in a certain period of time, but each database has only one indexeddbdatabase object.
The renderer that uses the idbdatabase mojo interface to communicate with the database will always use this connection to perform operations on the corresponding database objects.
Another important concept to understand is request. Opening and deleting a database cannot happen at the same time, but requests to perform the corresponding operations are planned. These functions can be implemented through indexeddbdatabase:: openrequest and indexeddbdatabase:: deleterequest classes.
IndexedDBDatabase::OpenRequest
IndexedDBDatabase::DeleteRequest
IndexedDBDatabase::OpenRequest
IndexedDBDatabase::DeleteRequest
As we mentioned earlier, indexeddb is built on a transactional database model. The program code treats a single transaction as an indexeddbtransaction object. Most operations are based on the transaction context and can be rolled back in the event of a failure.
Database mapping
In order to track all open databases, the program stores the original pointer to the corresponding indexeddbdatabase object through the database index (consisting of origin and database name). The database map is stored in the indexeddbfactoryimpl class as database "map".
database_map_
database_map_
When the rendering requests to open a connection to the database by calling the open method of the idbfactory interface, it queries the database map [1] to determine whether the corresponding database is open or not.
If the database is not already open, a new indexeddbdatabase object [3] is created and the original pointer to the object is stored in the database map [4].
If the database is turned on, the original pointer to the indexeddbdatabase object extracts [1] directly from the map and is used to create a new connection to the database in the indexeddbdatabase:: openconnection method [2].
In either case, the idbdatabase mojo interface pointer of the corresponding database object will always be returned to the renderer, which can connect and communicate with the corresponding database.
Life cycle of indexeddbdatabase object
The indexeddbdatabase object is a reference counted object. A count reference to this object is saved in the indexeddbconnection object, indexeddbtransaction object, or other request object that is in progress or pending. Once the reference count drops to 0, the object is immediately released.
When the database object is released, the corresponding raw pointer to indexeddbdatabase is removed from the database map, which is very important. This happens in the indexeddbdatabase:: close method when the connection to the database is closed.
The close method first aborts all outstanding transactions in the current communication [5]. And inform them that the current database is about to shut down [7].
Finally, the code checks whether the connection to be closed is the last connection in the corresponding database, and whether there is a request in progress or about to be executed [8]. If the above conditions are met, the code will delete the original pointer of indexeddbfactoryimpl:: releasedatabase from the database map by calling the indexeddbfactoryimpl:: releasedatabase method in [9].
If the condition is not met, the original pointer is saved. The above code is to delete the original database pointer from the database mapping after the last connection of the database and all requests are closed.
OK, let's assume that if the conditions are not met, there is a connection or request that still references the indexeddbdatabase object to keep it active. However, there are some defects in this case.
Indexeddb conditional competition
The code is vulnerable to a conditional contention vulnerability that could cause the raw pointer dangling in the database map to point to a freed indexedbddarabase object.
In order to create the corresponding scenario, we first open a database and specify the version as 0. This creates a new indexeddb database object and immediately opens a new connection.
Then we made another request to open the same database, but this time we specified version 2. This will require the database to perform an update operation. However, since the connection of version 0 still exists, the update cannot be started immediately when openrequest is executed, until openrequest:: onconnectionclosed is called, all database connections are closed.
After opening the second database connection, indexeddbdatabase:: activerequest will point to the openrequest object of version 2, and this object will delay the update operation of the database.
If we close the first database connection (version 0), indexeddbdatabase:: close will delete the last connection to the database [6], and then call openrequest:: onconnectionclosed on the openrequest pointed to by indexeddbdatabase:: activerequest.
Because the last connection to the database has been removed, openrequest:: onconnectionclosed will start the delayed update by calling indexeddbdatabase: openrequest:: startupgrade. Startupgrade will create a new connection and schedule a new versionchangeoperation task in the current transaction:
// Initiate the upgrade. The bulk of the work actually happens in
// IndexedDBDatabase::VersionChangeOperation in order to kick the
// transaction into the correct state.
void StartUpgrade(std::vector locks) {
connection_ = db_-CreateConnection(pending_-database_callbacks,
pending_-child_process_id);
DCHECK_EQ(db_-connections_.count(connection_.get()), 1UL);
std::vectorint64_t object_store_ids;
IndexedDBTransaction* transaction = connection_-CreateTransaction(
pending_-transaction_id,
std::setint64_t(object_store_ids.begin(), object_store_ids.end()),
blink::mojom::IDBTransactionMode::VersionChange,
new IndexedDBBackingStore::Transaction(db_-backing_store()));
transaction-ScheduleTask(
base::BindOnce(IndexedDBDatabase::VersionChangeOperation, db_,
pending_-version, pending_-callbacks));
transaction-Start(std::move(locks));
}
If we return to check [8] of method indexeddbdatabase:: close, the condition will not be met. Because there is still a connection to the database, the original pointer to the current indexeddbdatabase object is not removed.
After calling the mojo Close method to close the connection with the database (version 0), we immediately call the AbortTransactionsForDatabase method on the IDBFactory mojo interface from the rendering, so we have the opportunity to execute before the IndexedDBDatabase:: VersionChangeOperation task is released.
After calling the aborttransactionsfordatabasemojo method, the indexeddbconnection:: finishalltransactions method will be called on all connections to the database:
void IndexedDBConnection::FinishAllTransactions(
const IndexedDBDatabaseError& error) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::unordered_mapint64_t, std::unique_ptrIndexedDBTransaction temp_map;
std::swap(temp_map, transactions_);
for (const auto& pair : temp_map) {
auto& transaction = pair.second;
if (transaction-is_commit_pending()) {
IDB_TRACE1("IndexedDBDatabase::Commit", "transaction.id",
transaction-id());
transaction-ForcePendingCommit();
} else {
IDB_TRACE1("IndexedDBDatabase::Abort(error)", "transaction.id",
transaction-id());
transaction-Abort(error);
}
}
}
Since there is no other (method) commit, the code will call indexeddbtransaction:: abort method on all transactions, and finally indexeddbdatabase:: transactionfinished [11], indicating that the transaction has completed.
IndexedDBDatabase:: TransactionFinished calls OpenRequest:: UpgradeTransactionFinished, then calls IndexedDBDatabase:: RequestComplete to complete the request, and delete IndexedDBDatabase:: activerequest pointer at [12]:
void IndexedDBDatabase::RequestComplete(ConnectionRequest* request) {
DCHECK_EQ(request, active_request_.get());
scoped_refptrIndexedDBDatabase protect(this);
active_request_.reset(); [12]
// Exit early if |active_request_| held the last reference to |this|.
if (protect-HasOneRef())
return;
if (!pending_requests_.empty())
ProcessRequestQueue();
}
Each openrequest object has a corresponding indexeddbconnection object. When you clean up an active openrequest by removing the indexeddbdatabase:: activerequest pointer, the indexeddbconnection object, including all its transactions, is released.
At this point, all references to the indexeddbdatabase object disappear and will be released. At the same time, we closed all connections to the database, but bypassed the code without removing the original indexeddbdatabase pointer from the database map. OK, now we have created a scenario where there is a dangling original pointer in the database map, and the pointer points to the indexeddbdatabase object that has been released!
If we open the same database from the renderer, we can do something with the indexeddbdatabase object that has been released.