ADVISORIES | January 17, 2020

Android (AOSP) Download Provider SQL Injection in Query Sort Parameter (CVE-2019-2196)

A malicious application with the INTERNET permission granted could retrieve all entries from the Download Provider internal database, bypassing all currently implemented access control mechanisms, by exploiting an SQL injection in the sort parameter (ORDER BY clause) and appending a LIMIT clause, which allows expressions, including subqueries.

The information retrieved from this provider may include potentially sensitive information such as file names, descriptions, titles, paths, URLs (which may contain sensitive parameters in the query strings), cookies, custom HTTP headers, etc., for applications such as Gmail, Google Chrome, the Google Play Store, etc.

ADVISORIES |

Android (AOSP) Download Provider SQL Injection in Query Selection Parameter (CVE-2019-2198)

A malicious application with the INTERNET permission granted could retrieve all entries from the Download Provider internal database, bypassing all currently implemented access control mechanisms by exploiting an SQL injection in the selection clause.

The information retrieved from this provider may include potentially sensitive information such as file names, descriptions, titles, paths, URLs (that may contain sensitive parameters in the query strings), cookies, custom HTTP headers, etc., for applications such as Gmail, Google Chrome, the Google Play Store, etc.

ADVISORIES |

Android (AOSP) TV Provider SQL Injection in Query Projection Parameter (CVE-2019-2211)

A malicious application without any granted permission could retrieve all entries from the TV Provider internal database, bypassing all currently implemented access control mechanisms by exploiting an SQL injection in the projection parameter.

The information retrieved from this provider may include personal and potentially sensitive information about other installed applications and user preferences, habits, and activity, such as available channels and programs, watched programs, recorded programs, and titles in the “watch next” list.

ADVISORIES | April 1, 2019

Multiple Vulnerabilities in Android’s Download Provider (CVE-2018-9468, CVE-2018-9493, CVE-2018-9546)

Android’s Download Provider is a component of the Android framework and is designed to handle external downloads for other applications, such as web browsers (including Google Chrome), email clients (including Gmail), and the Google Play Store, among many others. In this blog post, I’ll describe three different high-severity vulnerabilities which affected several of the most recent versions of Android.

Android’s Download Provider

Any app can delegate its external downloads through this provider. As a developer, you’ll only need to insert a row with the appropriate parameters (invoking the Download Content Provider, part of the Download Provider, also called Download Manager). The Android Framework will handle the download for you automatically, in a background process which offers different configuration options. The notification with a progress bar that you’ve most likely seen several times (when downloading documents, attachments, or apps, for instance) is related to this provider, as illustrated in the following screenshots.

Overview of Vulnerabilities

By performing a research-driven manual code inspection of the Download Provider, using a very similar approach to the one I took with the Personal Dictionary Provider, I discovered three high-severity vulnerabilities. The following issues were reported to Google, along with their corresponding proof-of-concept tools for exploitation:

CVE-2018-9468: Download Provider Permission Bypass

The Download Provider is secured by different permissions and other protection mechanisms with various levels of access. As a general rule, a regular application should only have access to its own downloads. However, by exploiting this trivial vulnerability, any application (even without any permissions granted at all) can access any download that was requested by any other application or component of the OS itself, through the following content provider URI:

  • content://downloads/public_downloads/#

A malicious application would only need the identifier of a targeted download to gain access to it. This identifier is a sequential number, so all downloads are very easy to locate using a simple iteration. Once an existing download is located, most of the information stored in the provider’s database will be directly accessible through the Download Content Provider, including its title, description, size, full URL, etc. This may leak potentially sensitive information, including authentication tokens in the URL, email addresses, confidential document titles, email subjects found in the title or description columns, and so on.

This is only one of the aspects a malicious app can leverage. Actually, the impact is notably higher, because the openFile() method of the Content Provider is also affected by the vulnerability, since it relies on the query() call to check for permissions. That means that the attacker would also have unrestricted read and write access to the downloaded files. By exploiting this flaw, it’s possible to leak any downloaded file, and even worse, to overwrite it with arbitrary content, even before the download is complete.

This would allow a malicious app to read and potentially exfiltrate all your downloads (think about email attachments, electronic bank statements, or any other file you may download while browsing the Internet), which is obviously pretty bad for your confidentiality and the integrity of your data.

The following video demonstrates how all this data, including file contents, is publicly accessible by any other app. The proof-of-concept exploit just iterates through a range of identifiers, dumps some fields from the internal database, and displays the initial chunk of the downloaded file. The results are limited for brevity and demonstration purposes only, but keep in mind that the entire file would be accessible.

Gmail disclosure

Given the ability to write to these files, a malicious app can run a background service which monitors and overwrites any download requested through Android’s Download Provider. Since the Over-The-Air (OTA) updating process and the Google Play Store also use this provider, a malicious app can constantly overwrite any update or new app your smartphone is downloading, preventing a legitimate user from installing new apps or updating existing ones, including the OS itself, as demonstrated in the following videos.

DoS installing apps
DoS updating apps

In addition, any other downloaded attachment or file could easily be overwritten with arbitrary content using the very same process. This could be abused to replace files that you or your apps may consider trusted (from your bank, company intranet, HTTPS resources, apps using certificate pinning, etc.) to leverage a wide variety of potential exploits, as the downloads can’t be considered trusted anymore. These videos demonstrate the issue using different attack scenarios.

Gmail attachment hijack
Image file hijack
PDF file hijack

The source code and compiled APK for the proof-of-concept application used in these videos can be found in the following GitHub repository:

https://github.com/IOActive/AOSP-DownloadProviderHijacker

CVE-2018-9493: Download Provider SQL Injection

Some columns in the internal database (such as CookieData) are considered private and can’t be accessed directly through the Download Content Provider, unless the caller has unrestricted permissions (the URI is protected by the signatureOrSystem access level).

Specifying any of the restricted columns directly in the projection parameter won’t give us the results, obviously, but exploiting an SQL injection in the where expression, bypassing the setStrict filter, will allow us to extract content from the internal database, including restricted columns for any download. This vulnerability was introduced in 2016 in a change to enable search and filtering capabilities.

Note that this injection can also be combined with the previously described vulnerability (CVE-2018-9468), bypassing the permission’s requirement (by accessing the public_downloads URI, rather than my_downloads). Otherwise, it only requires the INTERNET permission (which isn’t considered dangerous and most apps have) in order to be exploited separately.

The source code and compiled APK for the proof-of-concept application that exploits this vulnerability can be found in the following GitHub repository:

https://github.com/IOActive/AOSP-DownloadProviderDbDumper

CVE-2018-9546: Download Provider Headers Disclosure

Consider an application that requests a download which, apart from the URL, needs some HTTP headers, such as authentication, cookies, or anti-CSRF tokens, for instance. To support this feature, Android’s Download Provider allows developers to request a download with any standard or custom HTTP headers. These headers will be stored in the database and exposed through the Downloads Content Provider, by simply specifying the identifier of the related download.

Unfortunately, Android never checks if you have permission to access the desired rows belonging to the identifier you’re requesting, so all headers of every single download can be trivially dumped by simply iterating through all existing identifiers.

A malicious app only requires the INTERNET permission to gain read access to all these headers. For instance, the default Android Browser app (not the latest versions of Google Chrome) in Android 5.1 stores all HTTP cookies in this table. This could lead to a highly sensitive information leak, allowing an attacker to steal your sessions if you happen to download any file from an authenticated website using that browser, as demonstrated in the following video.

cookie disclosure

The source code and compiled APK for the proof-of-concept application used in the video can be found in the following GitHub repository:

https://github.com/IOActive/AOSP-DownloadProviderHeadersDumper

Remediation

As an Android user, make sure your device is fully patched and never install untrusted apps. These vulnerabilities were fixed in the AOSP patches released on September and October 2018. For different vendors, check their security bulletins to ensure all three CVEs enumerated here are listed as patched.

As a developer, always apply defense-in-depth best practices and never trust a downloaded file that could have been tampered with in transit or at rest, as demonstrated in the videos in this post. Beware of potential race conditions or TOCTOU (Time-of-Check to Time-of-Use) vulnerabilities in your validation process.

Conclusions

It wasn’t hard to find these high-severity vulnerabilities. I discovered them in a few hours, by simply inspecting some source code fragments and understanding a relatively small component of the Android Framework, such as the Download Content Provider.

Whether you are working from a developer’s or a security auditor’s perspective, make sure to identify all entry points, understand the big picture, and pay attention to every detail that might lead to a vulnerability. Most of the time, the problem isn’t in the code that you can read, but in the code that you can’t see because it isn’t there, even though it should be, such as missing validations, permission checks, and so on.

More Information

Find more detailed information in our corresponding security advisories:

IOActive Security Advisory – Android’s Download Provider Permission Bypass (CVE-2018-9468)
IOActive Security Advisory – Android’s Download Provider SQL Injection (CVE-2018-9493)
IOActive Security Advisory – Android’s Download Provider Request Headers Disclosure (CVE-2018-9546)

ADVISORIES |

Android (AOSP) Download Provider Request Headers Disclosure (CVE-2018-9546)

A malicious application with the INTERNET permission granted could retrieve all entries from the Download Provider request headers table.

These headers may include sensitive information, such as session cookies or authentication headers, for any download started from the Android Browser or Google Chrome, among other applications.

Consider the impact that this would have on a user downloading a file from an authenticated website or URL. For example, an electronic statement file from an online bank or an attachment from corporate webmail may allow an attacker to impersonate the user on these platforms.

ADVISORIES |

Android (AOSP) Download Provider Permission Bypass (CVE-2018-9468)

A malicious application without any granted permission could retrieve all entries from the Download Provider, bypassing all currently implemented access control mechanisms. The level of access will be similar to having the ACCESS_ALL_DOWNLOADS permission granted, which is a signature-protected permission.

The information retrieved from this provider may include potentially sensitive information such as file names, descriptions, titles, paths, URLs (that may contain sensitive parameters in the query strings), etc., for applications such as Gmail, Chrome, or the Google Play Store.

ADVISORIES |

Android (AOSP) Download Provider SQL Injection (CVE-2018-9493)

By exploiting an SQL injection vulnerability, a malicious application without any permission granted could retrieve all entries from the Download Provider, bypassing all currently implemented access control mechanisms. Also, applications that were granted limited permissions, such as INTERNET, can also access all database contents from a different URI.

The information retrieved from this provider may include potentially sensitive information such as file names, descriptions, titles, paths, URLs (that may contain sensitive parameters in the query strings), etc., for applications such as Gmail, Chrome, or the Google Play Store.

Further access to the downloaded contents may be possible as well, depending on the permissions granted to the app and files.

ADVISORIES | August 2, 2018

Android (AOSP) User Dictionary Content Provider Authorization Bypass

Android Open Source Project (AOSP) vulnerability discovered, where a malicious application without any permission could exploit access to the user personal dictionary.

RESEARCH |

Discovering and Exploiting a Vulnerability in Android’s Personal Dictionary (CVE-2018-9375)

I was auditing an Android smartphone, and all installed applications were in scope. My preferred approach, when time permits, is to manually inspect as much code as I can. This is how I found a subtle vulnerability that allowed me to interact with a content provider that was supposed to be protected in recent versions of Android: the user’s personal dictionary, which stores the spelling for non-standard words that the user wants to keep.

While in theory access to the user’s personal dictionary should be only granted to privileged accounts, authorized Input Method Editors (IMEs), and spell checkers, there was a way to bypass some of these restrictions, allowing a malicious application to update, delete, or even retrieve all the dictionary’s contents without requiring any permission or user interaction.

This moderate-risk vulnerability, classified as elevation of privilege and fixed on June 2018, affects the following versions of Android: 6.0, 6.0.1, 7.0, 7.1.1, 7.1.2, 8.0, and 8.1.

User’s Personal Dictionary
Android provides a custom dictionary that can be customized manually or automatically, learning from the user’s typing. This dictionary can be usually accessed from “Settings → Language & keyboard → Personal dictionary” (sometimes under “Advanced” or slightly different options). It may contain sensitive information, such as names, addresses, phone numbers, emails, passwords, commercial brands, unusual words (may include illnesses, medicines, technical jargon, etc.), or even credit card numbers.

android custom personal dictionary

A user can also define a shortcut for each word or sentence, so instead of typing your full home address, you can add an entry and simply write the associated shortcut (e.g. “myhome”) for its autocompletion.

defining personal dictionary shortcut

Internally, the words are stored in a SQLite database which simply contains a table named “words” (apart from the “android_metadata”). This table’s structure has six columns:

  • _id (INTEGER, PRIMARY KEY)
  • word (TEXT)
  • frequency (INTEGER)
  • locale (TEXT)
  • appid (INTEGER)
  • shortcut (TEXT)

Our main interest will be focused on the “word” column, as it contains the custom words, as its name suggests; however, all remaining columns and tables in the same database would be accessible as well.

Technical Details of the Vulnerability
In older versions of Android, read and write access to the personal dictionary was protected by the following permissions, respectively:

  • android.permission.READ_USER_DICTIONARY
  • android.permission.WRITE_USER_DICTIONARY

This is no longer true for newer versions. According to the official documentation1: “Starting on API 23, the user dictionary is only accessible through IME and spellchecker”. The previous permissions were replaced by internal checks so, theoretically, only privileged accounts (such as root and system), the enabled IMEs, and spell checkers could access the personal dictionary content provider (content://user_dictionary/words).

We can check the AOSP code repository and see how in one the changes2, a new private function named canCallerAccessUserDictionary was introduced and was invoked from all the standard query, insert, update, and delete functions in the UserDictionary content provider to prevent unauthorized calls to these functions.

While the change seems to be effective for both query and insert functions, the authorization check happens too late in update and delete, introducing a security vulnerability that allows any application to successfully invoke the affected functions via the exposed content provider, therefore bypassing the misplaced authorization check.

In the following code for the UserDictionaryProvider class3, pay attention to the highlighted fragments and see how the authorization checks are performed after the database would be already altered:

@Override

public int delete(Uri uri, String where, String[] whereArgs) {
   SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   int count;
   switch (sUriMatcher.match(uri)) {
      case WORDS:
          count = db.delete(USERDICT_TABLE_NAME, where, whereArgs);
          break;
 
      case WORD_ID:
          String wordId = uri.getPathSegments().get(1);
          count = db.delete(USERDICT_TABLE_NAME, Words._ID + "=" + wordId
               + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
          break;
 
       default:
          throw new IllegalArgumentException("Unknown URI " + uri);
   }
 
   // Only the enabled IMEs and spell checkers can access this provider.
   if (!canCallerAccessUserDictionary()) {
       return 0;
   }

   getContext().getContentResolver().notifyChange(uri, null);
   mBackupManager.dataChanged();
   return count;
}


@Override

public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
   SQLiteDatabase db = mOpenHelper.getWritableDatabase();
   int count;
   switch (sUriMatcher.match(uri)) {
      case WORDS:
         count = db.update(USERDICT_TABLE_NAME, values, where, whereArgs);
         break;

      case WORD_ID:
         String wordId = uri.getPathSegments().get(1);
         count = db.update(USERDICT_TABLE_NAME, values, Words._ID + "=" + wordId
+ (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
         break;

      default:
         throw new IllegalArgumentException("Unknown URI " + uri);
   }

   // Only the enabled IMEs and spell checkers can access this provider.
   if (!canCallerAccessUserDictionary()) {
      return 0;
   }

   getContext().getContentResolver().notifyChange(uri, null);
   mBackupManager.dataChanged();
   return count;
}

Finally, notice how the AndroidManifest.xml file does not provide any additional protection (e.g. intent filters or permissions) to the explicitly exported content provider:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.android.providers.userdictionary"
       android:sharedUserId="android.uid.shared">

   <application android:process="android.process.acore"
       android:label="@string/app_label"
       android:allowClearUserData="false"
       android:backupAgent="DictionaryBackupAgent"
       android:killAfterRestore="false"
       android:usesCleartextTraffic="false"
       >

       <provider android:name="UserDictionaryProvider"
          android:authorities="user_dictionary"
          android:syncable="false"
          android:multiprocess="false"
          android:exported="true" />

   </application>
</manifest>

It is trivial for an attacker to update the content of the user dictionary by invoking code like the following from any malicious application, without the need to ask for any permission:

ContentValues values = new ContentValues();
values.put(UserDictionary.Words.WORD, "IOActive");

getContentResolver().update(UserDictionary.Words.CONTENT_URI, values,
        null, null);

It would be also trivial to delete any content, including the entire personal dictionary:

getContentResolver().delete(UserDictionary.Words.CONTENT_URI, null, null);

Both methods (update and delete) are supposed to return the number of affected rows, but in this case (for non-legitimate invocations) they will always return zero, making it slightly more difficult for an attacker to extract or infer any information from the content provider.

At this point, it may appear that this is all we can do from an attacker perspective. While deleting or updating arbitrary entries could be a nuisance for the end user, the most interesting part is accessing personal data.

Even if the query function is not directly affected by this vulnerability, it is still possible to dump the entire contents by exploiting a time-based, side-channel attack. Since the where argument is fully controllable by the attacker, and due to the fact that a successful update of any row takes more time to execute than the same statement when it does not affect any row, the attack described below was proven to be effective.

Simple Proof of Concept
Consider the following code fragment running locally from a malicious application:

ContentValues values = new ContentValues();
values.put(UserDictionary.Words._ID, 1);

long t0 = System.nanoTime();
for (int i=0; i<200; i++) {
    getContentResolver().update(UserDictionary.Words.CONTENT_URI, values,
                    "_id = 1 AND word LIKE 'a%'", null);
}
long t1 = System.nanoTime();

Invoking the very same statement enough times (e.g. 200 times, depending on the device), the time difference (t1-t0) between an SQL condition that evaluates to “true” and the ones that evaluate to “false” will be noticeable, allowing the attacker to extract all the information in the affected database by exploiting a classic time-based, Boolean blind SQL injection attack.

Therefore, if the first user-defined word in the dictionary starts with the letter “a”, the condition will be evaluated to “true” and the code fragment above will take more time to execute (for example, say 5 seconds), compared to the lesser time required when the guess is false (e.g. 2 seconds), since no row will actually be updated in that case. If the guess was wrong, we can then try with “b”, “c”, and so on. If the guess is correct, it means that we know the first character of the word, so we can proceed with the second character using the same technique. Then, we can move forward to the next word and so on until we dump the entire dictionary or any filterable subset of rows and fields.

To avoid altering the database contents, notice how we updated the “_id” column of the retrieved word to match its original value, so the inner idempotent statement will look like the following:

UPDATE words SET _id=N WHERE _id=N AND (condition)

If the condition is true, the row with identifier “N” will be updated in a way that doesn’t actually change its identifier, since it will be set to its original value, leaving the row unmodified. This is a non-intrusive way to extract data using the execution time as a side-channel oracle.

Because we can replace the condition above with any sub-select statement, this attack can be extended to query any SQL expression supported in SQLite, such as:

  • Is the word ‘something’ stored in the dictionary?
  • Retrieve all 16-character words (e.g. credit card numbers)
  • Retrieve all words that have a shortcut
  • Retrieve all words that contain a dot

Real-world Exploitation
The process described above can be fully automated and optimized. I developed a simple Android application to prove its exploitability and test its effectiveness.

The proof-of-concept (PoC) application is based on the assumption that we can blindly update arbitrary rows in the UserDictionary database through the aforementioned content provider. If the inner UPDATE statement affects one or more rows, it will take more time to execute. This is essentially all that we will need in order to infer whether an assumption, in the form of a SQL condition, is evaluated to true or false.

However, since at this initial point we don’t have any information about the content (not even the values of the internal identifiers), instead of iterating through all possible identifier values, we’ll start with the row with the lowest identifier and smash the original value of its “frequency” field to an arbitrary number. This step could be done using different valid approaches.

Because several shared processes will be running at the same time in Android, the total elapsed time for the same invocation will vary between different executions. Also, this execution time will depend on each device’s processing capabilities and performance; however, from a statistical perspective, repeating the same invocation a significant amount of iterations should give us a differentiable measure on average. That’s why we’ll need to adjust the number of iterations per device and current configuration (e.g. while in battery saving mode).

Although I tried with a more complex approach first to determine if a response time should be interpreted as true or false, I ended up implementing a much simpler approach that led to accurate and reliable results. Just repeat the same number of requests that always evaluate to “true” (e.g. “WHERE 1=1”) and “false” (e.g. “WHERE 1=0”) and take the average time as the threshold to differentiate them. Measured times greater than the threshold will be interpreted as true; otherwise, as false. It’s not AI or big data, nor does it use blockchain or the cloud, but the K.I.S.S. principle applies and works!

differentiate the correct and wrong assumptions

Once we have a way to differentiate between correct and wrong assumptions, it becomes trivial to dump the entire database. The example described in the previous section is easy to understand, but it isn’t the most efficient way to extract information in general. In our PoC, we’ll use the binary search algorithm4 instead for any numeric query, using the following simple approach:

  • Determine the number of rows of the table (optional)
    • SELECT COUNT(*) FROM words
  • Determine the lowest identifier
    • SELECT MIN(_id) FROM words
  • Determine the number of characters of the word with that identifier
    • SELECT length(word) FROM words WHERE _id=N
  • Iterate through that word, extracting character by character (in ASCII/Unicode)
    • SELECT unicode(substr(word, i, 1)) FROM words WHERE _id=N
  • Determine the lowest identifier which is greater than the one we got and repeat
    • SELECT MIN(_id) FROM words WHERE _id > N

Remember that we can’t retrieve any numeric or string value directly, so we’ll need to translate these expressions into a set of Boolean queries that we can evaluate to true or false, based on their execution time. This is how the binary search algorithm works. Instead of querying for a number directly, we’ll query: “is it greater than X?” repeatedly, adjusting the value of X in each iteration until we find the correct value after log(n) queries. For instance, if the current value to retrieve is 97, an execution trace of the algorithm will look like the following:

Iteration Condition Result Max Min Mid
255 0 127
1 Is N > 127? No 127 0 63
2 Is N > 63? Yes 127 63 95
3 Is N > 95? Yes 127 95 111
4 Is N > 111? No 111 95 103
5 Is N > 103? No 103 95 99
6 Is N > 99? No 99 95 97
7 Is N > 97? No 97 95 96
8 Is N > 96? Yes 97 96 96

 

The Proof-of-Concept Exploitation Tool
The process described above was implemented in a PoC tool, shown below. The source code and compiled APK for this PoC can be accessed from the following GitHub repository: https://github.com/IOActive/AOSP-ExploitUserDictionary

Let’s have a look at its minimalistic user interface and explain its singularities.

proof of concept exploitation tool

The first thing the application does is attempt to access the personal dictionary content provider directly, querying the number of entries. Under normal circumstances (not running as root, etc.), we should not have access. If for any reason we achieve direct access, it doesn’t make sense to exploit anything using a time-based, blind approach, but even in that case, you’ll be welcome to waste your CPU cycles with this PoC instead of mining cryptocurrencies.

As described before, there are only two parameters to adjust:

  • Initial number of iterations: How many times will the same call be repeated to get a significant time difference.
  • Minimum time threshold (in milliseconds): How much time will be considered the lowest acceptable value.

Although the current version of the tool will adjust them automatically for us, in the very first stage everything was manual and the tool was simply taking these parameters as they were provided, so this is one of the reasons why these controls exist.

In theory, the larger these numbers are, the better accuracy we’ll get, but the extraction will be slower. If they are smaller, it will run faster, but it’s more likely to obtain inaccurate results. This is why there is a hardcoded minimum of 10 iterations and 200 milliseconds.

If we press the “START” button, the application will start the auto-adjustment of the parameters. First, it will run some queries and discard the results, as the initial ones were usually quite high and not representative. Then, it will execute the initial number of iterations and estimate the corresponding threshold. If the obtained threshold is above the minimum we configured, then it will test the estimated accuracy by running 20 consecutive queries, alternating true and false statements. If the accuracy is not good enough (only one mistake will be allowed), then it will increase the number of iterations and repeat the process a set number of times until the parameters are properly adjusted or give up and exit if the conditions couldn’t be satisfied.

Once the process is started, some controls will be disabled, and we’ll see the current verbose output in the scrollable log window below (also via logcat) where we can see, among other messages, the current row identifier, all SQL subqueries, the total time, and the inferred trueness. The retrieved characters will appear in the upper line as soon as they’re extracted.

verbise output of scrollable log window

Finally, the “UPD” and “DEL” buttons on the right are completely unrelated to the time-based extraction controls, and they simply implement direct calls to the content provider to perform an UPDATE and DELETE, respectively. They were intentionally limited to affect the words starting with “123” only. This was done to avoid accidental deletions of any personal dictionary, so in order to test these methods, we’ll need to add this entry manually, unless we had it already.

Demo
Probably the easiest way to summarize the process is watching the tool in action in the following videos, recorded in a real device.

Additional Considerations
There is usually a gap between theory and practice, so I’d also like to share some of the issues I faced during the design and development of this PoC. First, bear in mind that the tool is simply a quick and dirty PoC. Its main goal was to prove that the exploitation was possible and straightforward to implement, and that’s why it has several limitations and doesn’t follow many of the recommended programming best practices, as it’s not meant to be maintainable, efficient, offer a good user experience, etc.

In the initial stages, I didn’t care about the UI and everything was dumped to the Android log output. When I decided to show the results in the GUI as well, I had to run all the code in a separate thread to avoid blocking the UI thread (which may cause the app to be considered unresponsive and therefore killed by the OS). The accuracy dropped considerably with this simple change, because that thread didn’t have much priority, so I set it to “-20”, which is the maximum allowed priority, and everything worked fine again.

Updating the UI from a separate thread may lead to crashes and it’s generally detected and restricted via runtime exceptions, so in order to show the log messages, I had to invoke them using calls to runOnUiThread. Bear in mind that in a real exploit, there’s no need for a UI at all.

If the personal dictionary is empty, we can’t use any row to force an update, and therefore all queries will take more or less the same time to execute. In this case there’ll be nothing to extract and the tool shouldn’t be able to adjust the parameters and will eventually stop. In some odd cases, it might be randomly calibrated even with an empty database and it will try to extract garbage or pseudo-random data.

In a regular smartphone, the OS will go to sleep mode after a while and the performance will drop considerably, causing the execution time to increase way above the expected values, so all calls would be evaluated as true. This could have been detected and reacted in a different manner, but I simply opted for a simpler solution: I kept the screen turned on and acquired a wake lock via the power manager to prevent the OS from suspending the app. I didn’t bother to release it afterwards, so you’ll have to kill the application if you’re not using it.

Rotating the screen also caused problems, so I forced it to landscape mode only to avoid auto-rotating and to take advantage of the extra width to show each message in a single line.

Once you press the “START” button, some controls will be permanently disabled. If you want to readjust the parameters or run it multiple times, you’ll need to close it and reopen it.

Some external events and executions in parallel (e.g. synchronizing the email, or receiving a push message) may interfere with the application’s behavior, potentially leading to inaccurate results. If that happens, try it again in more stable conditions, such as disabling access to the network or closing all other applications.

The UI doesn’t support internationalization, and it wasn’t designed to extract words in Unicode (although it should be trivial to adapt, it wasn’t my goal for a simple PoC).

It was intentionally limited to extract the first 5 words only, sorted by their internal identifiers.

Remediation
From a source code perspective, the fix is very simple. Just moving the call to check if the caller has permissions to the beginning of the affected functions should be enough to fix the issue. Along with the advisory, we provided Google a patch file with the suggested fix and this was the commit in which they fixed the vulnerability:
https://android.googlesource.com/platform/packages/…

Since the issue has been fixed in the official repository, as end users, we’ll have to make sure that our current installed security patch level contains the patch for CVE-2018-9375. For instance, in Google Pixel/Nexus, it was released on June 2018:
https://source.android.com/security/bulletin/pixel/2018-06-01

If for any reason it’s not possible to apply an update to your device, consider reviewing the contents of your personal dictionary and make sure it doesn’t contain any sensitive information in the unlikely event the issue becomes actively exploited.

Conclusions
Software development is hard. A single misplaced line may lead to undesirable results. A change that was meant to improve the security and protection of the user’s personal dictionary, making it less accessible, led to the opposite outcome, as it inadvertently allowed access without requiring any specific permission and went unnoticed for almost three years.

Identifying a vulnerability like the one described here can be as easy as reading and understanding the source code, just following the execution flow. Automated tests may help detect this kind of issue at an early stage and prevent them from happening again in further changes, but they aren’t always that easy to implement and maintain.

We also learned how to get the most from a vulnerability that, in principle, only allowed us to destroy or tamper with data blindly, increasing its final impact to an information disclosure that leaked all the data by exploiting a side-channel, time-based attack.

Always think outside the box, and remember: time is one of the most valuable resources. Every nanosecond counts!