Android Application Diffing: CVE-2019-10875 Inspection

This blog post is about examining an Android security patch and understanding how it mitigates the vulnerability.

The first article of this blog series introduced a technical overview of the diff engine we have designed and set up. Moreover, it exposed benchmark outcomes regarding execution time on three top applications. However, it did not assess result effectiveness whatsoever. This blog post introduces a use case of diff analysis to locate where a vulnerability patch has been applied within code in order to mitigate a flaw. It is addressed through a real-world setting: the analysis of a vulnerability which affected Mint Browser, CVE-2019-10875 [1].

Introduction

Mint Browser is a popular Android web browser developed by Xiaomi Inc. Several vulnerabilities, such as CVE-2019-10875, have been found in April 2019. This flaw was uncovered by Arif Khan and described in a blog post [2]. Roughly speaking, it could allow an attacker to visually fool a victim by displaying a fake remote server on the browser's navigation bar once opening a specially-crafted URL that embeds a GET parameter named q. In other words, if the victim opens https://www.evil.com/?q=www.google.com, only www.google.com shows up on the navigation bar instead of the genuine remote server which is www.evil.com. This security hole could be leveraged by an adversary as part of a phishing campaign for instance. It affects versions 1.6.1 and lower so a patch is available from version 1.6.3.

Now, let's pretend one would like to technically understand how this vulnerability works. To do so, the quickest way is to have a look at the security patch applied by the developers after being aware of this issue. It should lead us to the vulnerable code and prevent us from inspecting the whole application code.

In this article, we point out how we used diff analysis to find out which actual parts of the code have been modified in order to protect users from vulnerability exploitation. Note that depending on the case, some modifications can also be done in embedded native libraries thanks to the JNI feature [3]. If so, a complementary diff process has to be performed at native code level.

Selecting classes for comparison

First, we have to reduce the sets of classes in order to only keep classes that have been actually developed by Xiaomi development team. This step is pretty important because it makes the comparison faster and more accurate in terms of results. Indeed, as said in the previous blog post, the fewer the better. The AndroidManifest.xml located at the APK root can give us some useful information about their development package which usually contains most of their activities, services, broadcast receivers and so on.

Doing so, we quickly notice that a large number of the classes declared in the manifest file share a common root package called com.miui.org.chromium.chrome.browser. Thus, we will only compare classes included within that one in a first place. The following Python piece of code shows how it could be implemented:

lhs_app = load("com.mi.globalbrowser.mini-1.6.1.apk.apk") # Loading left handside application
rhs_app = load("com.mi.globalbrowser.mini-1.6.3.apk.apk") # Loading right handside application

condition = {"package_filtering": "com.miui.org.chromium.chrome.browser"}
lhs_classes = filter(lhs_app.classes, condition)
rhs_classes = filter(rhs_app.classes, condition)

At this point, we have selected 1108 classes on left handside and 1134 on right handside. This amount looks pretty fair for comparison purposes.

Finding out patched classes

Once getting classes, we need to know which ones have been altered. However, we must wisely choose the optimization options beforehand so it can efficiently fit to our needs in this precise case. Those could be represented as follows:

optimizations = {
  "inner_skipping": False,
  "external_skipping": False,
  "synthetic_skipping": True,
  "find_obfuscated_packages": False,
  "min_inst_size_threshold": 5,
  "top_match_threshold": 3
}

diff_results = diff(lhs_classes, rhs_classes, 0.8, optimizations)

These optimization options respectively configure the diff engine:

  • not to skip inner and external classes: Inner and external classes are likely to contain patched code.
  • to skip synthetic classes: Synthetic classes are automatically generated by the compiler so unlikely to embed modified code.
  • not to try to find obfuscated package names: As seen before, package names don't seem obfuscated. Setting this option, the diff engine will not consider potential issues caused by obfuscation.
  • not to consider classes which embed less than 5 instructions: Applications often contain small classes which all look alike (e.g. a few methods that only return attribute values). Nevertheless, they are worthless regarding comparison in most situations. Most of the time, they add several false positive results which can go away by setting a suitable value.
  • to perform thorough comparisons in the three top classes: This option has to be increased if we are dealing with classes that roughly look the same at structural level. It is not our case here.

As well, we set the matching threshold to 0.8 because we only expect minor changes as we are looking for bug fixes. It means that classes with a matching distance below this value are discarded from results.

At this point, we got results from diff() function. We can iterate over them with the following Python code:

for match in diff_results:
  if match.distance < 1.0: # Skipping perfect matches, that is unmodified classes
    print(f"[+] {match.lhs.info} | {match.rhs.info} -> {match.distance:1.4f}")

After executing the whole script, it outputs plenty of results:

[+] com/miui/org/chromium/chrome/browser/widget/progress: ToolbarProgressBar - ToolbarProgressBar.java | com/miui/org/chromium/chrome/browser/widget/progress: ToolbarProgressBar - ToolbarProgressBar.java -> 0.9994
[+] com/miui/org/chromium/chrome/browser/update: HomePageDataUpdator - HomePageDataUpdator.java | com/miui/org/chromium/chrome/browser/update: HomePageDataUpdator - HomePageDataUpdator.java -> 0.9794
[+] com/miui/org/chromium/chrome/browser/init: ChromeBrowserInitializer$2 - ChromeBrowserInitializer.java | com/miui/org/chromium/chrome/browser/init: ChromeBrowserInitializer$2 - ChromeBrowserInitializer.java -> 0.9771
[+] com/miui/org/chromium/chrome/browser/init: ChromeBrowserInitializer - ChromeBrowserInitializer.java | com/miui/org/chromium/chrome/browser/init: ChromeBrowserInitializer - ChromeBrowserInitializer.java -> 0.9996
[+] com/miui/org/chromium/chrome/browser/download: DownloadDialogFragment - DownloadDialogFragment.java | com/miui/org/chromium/chrome/browser/download: DownloadDialogFragment - DownloadDialogFragment.java -> 0.9136
[+] com/miui/org/chromium/chrome/browser/download: DownloadHandler - DownloadHandler.java | com/miui/org/chromium/chrome/browser/download: DownloadHandler - DownloadHandler.java -> 0.9978
[+] com/miui/org/chromium/chrome/browser/adblock: AdCheckHelper - AdCheckHelper.java | com/miui/org/chromium/chrome/browser/adblock: AdCheckHelper - AdCheckHelper.java -> 0.9931
[+] com/miui/org/chromium/chrome/browser/download: DownloadHandler$2 - DownloadHandler.java | com/miui/org/chromium/chrome/browser/download: DownloadHandler$2 - DownloadHandler.java -> 0.9666
[+] com/miui/org/chromium/chrome/browser/omnibox: NavigationBar - NavigationBar.java | com/miui/org/chromium/chrome/browser/omnibox: NavigationBar - NavigationBar.java -> 0.9834
[+] com/miui/org/chromium/chrome/browser/download: DownloadDialogFragment$4$1 - DownloadDialogFragment.java | com/miui/org/chromium/chrome/browser/download: DownloadDialogFragment$4$1 - DownloadDialogFragment.java -> 0.9563
[+] com/miui/org/chromium/chrome/browser/cloudconfig: CloudConfigManager - CloudConfigManager.java | com/miui/org/chromium/chrome/browser/cloudconfig: CloudConfigManager - CloudConfigManager.java -> 0.8747
[+] com/miui/org/chromium/chrome/browser: ChromeTabbedActivity - ChromeTabbedActivity.java | com/miui/org/chromium/chrome/browser: ChromeTabbedActivity - ChromeTabbedActivity.java -> 0.9998
[+] com/miui/org/chromium/chrome/browser/omnibox: LocationBarLayout - LocationBarLayout.java | com/miui/org/chromium/chrome/browser/omnibox: LocationBarLayout - LocationBarLayout.java -> 0.9932
[+] com/miui/org/chromium/chrome/browser/omnibox/suggestions: SuggestionAdapter$SuggestionResult - SuggestionAdapter.java | com/miui/org/chromium/chrome/browser/omnibox/suggestions: SuggestionAdapter$SuggestionResult - SuggestionAdapter.java -> 0.9920
[+] com/miui/org/chromium/chrome/browser/omnibox/suggestions: SuggestionAdapter$SuggestFilter - SuggestionAdapter.java | com/miui/org/chromium/chrome/browser/omnibox/suggestions: SuggestionAdapter$SuggestFilter - SuggestionAdapter.java -> 0.9920
[+] com/miui/org/chromium/chrome/browser/omnibox/suggestions: MostVisitedDataProvider - MostVisitedDataProvider.java | com/miui/org/chromium/chrome/browser/omnibox/suggestions: MostVisitedDataProvider - MostVisitedDataProvider.java -> 0.9798
[+] com/miui/org/chromium/chrome/browser/webviewclient: NightModeHelper - NightModeHelper.java | com/miui/org/chromium/chrome/browser/webviewclient: NightModeHelper - NightModeHelper.java -> 0.9570
[+] com/miui/org/chromium/chrome/browser/omnibox/suggestions: AutocompleteCoordinator - AutocompleteCoordinator.java | com/miui/org/chromium/chrome/browser/omnibox/suggestions: AutocompleteCoordinator - AutocompleteCoordinator.java -> 0.9964
[+] com/miui/org/chromium/chrome/browser/omnibox/suggestions: SuggestionViewFactory - SuggestionViewFactory.java | com/miui/org/chromium/chrome/browser/omnibox/suggestions: SuggestionViewFactory - SuggestionViewFactory.java -> 0.8978

That output contains information that we do not care about for patch analysis purposes. Hence, we have to sort it out. At first sight, we notice that there are alterations in various components such as download- and suggestion-related parts. However, as class names are not obfuscated, we can quickly observe that a class called NavigationBar has been slightly altered. It is quite interesting because the flaw involves an issue on the navigation bar. Let's inspect the modifications within this class.

Getting code modifications

Code mutations can be both inspected in smali or pseudo Java. For convenience sake, we looked at Java representation as it is more straightforward to read through. Prior to doing so, we have to decompile the APK thanks to external tools such as Jadx. Once done, we can compare Java source codes that correspond to the class named NavigationBar located in com.miui.org.chromium.chrome.browser.omnibox package.

Using a text-based visual diff tool like Meld, we are able to effectively spot which methods have been modified. In our case, only one method has been altered: String pickSearchKeyWords(String). The following block emphasises the divergences between the former and the newer versions of the method:

 1 --- com.mi.globalbrowser.mini-1.6.1/com/miui/org/chromium/chrome/browser/omnibox/NavigationBar.java
 2 +++ com.mi.globalbrowser.mini-1.6.3/com/miui/org/chromium/chrome/browser/omnibox/NavigationBar.java
 3   private String pickSearchKeyWords(String str) {
 4     if (str == null || getCurrentTab() == null) {
 5       return null;
 6     }
 7     Object url = getCurrentTab().getUrl();
 8     if (TextUtils.isEmpty(url)) {
 9       return null;
10     }
11     Uri parse = Uri.parse(url);
12     String host = parse.getHost();
13     if (TextUtils.isEmpty(host)) {
14       return null;
15     }
16     CharSequence charSequence = "";
17     String[] searchEngineLabels = getSearchEngineLabels();
18 +   int i = 0;
19     if (searchEngineLabels != null && searchEngineLabels.length > 0) {
20 -     for (String str2 : searchEngineLabels) {
21 +     int length = searchEngineLabels.length;
22 +     int i2 = 0;
23 +     while (i < length) {
24 +       String str2 = searchEngineLabels[i];
25         if (!TextUtils.isEmpty(str2) && host.contains(str2.trim().toLowerCase())) {
26           charSequence = SearchEngineSwitchUtil.getInstance(this.mContext).getQueryParameterNameForSearchTermsMap(str2);
27 +         i2 = 1;
28         }
29 +       i++;
30       }
31 +     i = i2;
32     }
33     if (charSequence == null) {
34       charSequence = "";
35     }
36     String queryParameter = parse.getQueryParameter(charSequence);
37     if (TextUtils.isEmpty(charSequence) || TextUtils.isEmpty(queryParameter)) {
38       if (host.contains("yahoo.com")) {
39         queryParameter = parse.getQueryParameter("p");
40       } else if (host.contains("yandex.ru")) {
41         queryParameter = parse.getQueryParameter("text");
42 -     } else {
43 +     } else if (i != 0) {
44         queryParameter = parse.getQueryParameter("q");
45       }
46     }
47     return queryParameter;
48   }

Understanding the security patch

Getting back to the vulnerability itself, we need to figure out how those modifications actually mitigate the flaw. Reading through the original source code, we can see that the query parameter q is returned by the method if host does not contain neither yahoo.com nor yandex.ru, regardless of whether the actual host is a search engine or not. It means that whatever the host, if a q parameter is present in the URL, its value is selected and outputted. It is the reason why the method would return www.google.com once https://www.evil.com/?q=www.google.com is passed as parameter even though www.evil.com is not a known search engine.

In order to avoid that fault, the patched code now checks if the host is actually a known search engine and stores the information in a variable (L27 and L31). If so, it gets the value from q query parameter (L43). Therefore, as www.evil.com is not a genuine search engine, the previously-exposed attack scenario should not work anymore.

To confirm our assumptions, we have to perform an additional dynamic analysis thanks to Frida. We would like to passively intercept calls to pickSearchKeyWords() method and print input parameter and output string for both original (1.6.1) and patched (1.6.3) versions. A small script can be written to do so:

console.log("[+] JS script successfully loaded");

Java.perform(function () {
  var method_hook = Java.use("com.miui.org.chromium.chrome.browser.omnibox.NavigationBar");

  method_hook.pickSearchKeyWords.overload("java.lang.String").implementation = function(str) {
    ret = this.pickSearchKeyWords(str) // calling genuine method's implementation
    console.log("[+] pickSearchKeyWords(" + str + ") = " + ret)
    return ret
  }
});

The following block shows results when accessing https://www.evil.com/?q=www.google.com with the two versions:

on 1.6.1:
[+] pickSearchKeyWords(https://www.evil.com/?q=www.google.com) = www.google.com

on 1.6.3:
[+] pickSearchKeyWords(https://www.evil.com/?q=www.google.com) = null

It is now pretty clear that the modifications effectively mitigate the vulnerability as instead of returning www.google.com, the vulnerable method outputs null. As a result, the caller method is able to act accordingly and thus display the full URL on browser's navigation bar.

Comments