root@v

Are You OK?

在Android的个人字典中发现和利用漏洞(CVE-2018-9375)

vorblock / 2018-08-05


翻译的文章 渣渣翻译 原作者:Daniel Kachakil

我正在审计一款Android手机,审计范围是所有已经安装了的应用程序。我的首选方法是,在时间允许的时候,就手动检查尽可能多的代码。我发现了一个巧妙的漏洞,这个漏洞允许我与一个内容提供者交互,而该内容提供者应该在最近的Android版本中受到保护:用户的个人词典,它存储了用户想要保留的非标准单词的拼写。

虽然理论上应该只授权给特权用户、授权的输入法编辑器(IMEs) ,以及拼写检查程序访问用户的个人词典,但是有一种方法可以绕过这些限制,允许恶意应用程序更新、删除甚至检索字典内的所有内容。而不需要任何权限或者与用户交互。

这个中等风险的漏洞被归类为权限提升,并于2018年6月修复,影响到Android的以下版本:6.0、6.0.1、7.0、7.1.1、7.1.2、8.0和8.1。

用户的个人词典

Android提供了一个自定义词典,可以手动输入或者自动定制,从用户的输入中学习。这本字典的入口为“设置→ 语言和键盘 → 个人词典” (也可能在“高级“或者不同的选项下)。他可能包含有敏感信息,比如姓名、地址、电话号码、电子邮件、密码、商业品牌、不存常的词汇(可能包括疾病、药品、技术术语等),甚至信用卡号。

用户还可以为每个单词或者句子定义一个快捷方式,因此想要输入的家庭地址的时候,你可以添加一个条目并简单地为其添加一个快捷方式(比如“myhome”)来自动完成填写。

在内部,这些单词存储在SQLLite数据库中,该数据库只包含有一个名为“words”的表(除了“android_metadata” ),这个表有6列:

我们主要注意“word”这列,正如名称所示,它包含了自定义的单词。然而,同一数据库中所有剩余的列和表也可以访问。

漏洞细节

在较早版本的Android中,对个人字典的读写访问分别受到以下权限的保护:

对于新版本来说,这已经不再适用了,根据官方文档[1]:“从API 23开始,用户字典只能通过IME和拼写检查器访问” ,以前的权限已经被内部检查所取代,因此理论上,只有特权帐户(比如 root 和 system), 启用的IMEs和拼写检查器可以访问个人字典内容提供者  (content://user_dictionary/words)。

我们可以检查AOSP代码库,查看一个变更[2]中引入了一个新的名为canCallerAccessUserDictionary 的私有函数,并从 UserDictionary 内容提供者中的所有标准查询、插入、更新和删除函数中调用该函数,以防止对这些函数的未经授权的调用。

虽然更改似乎对查询和插入函数都有效,但是在更新和删除过程中,授权检查发生滞后引入了安全漏洞,允许任何应用程序通过公开的内容提供者成功地调用受影响的函数,从而绕过错误的授权检查。

在下面的 UserDictionaryProvider类[3]的代码中,注意高亮(标注在注释)的片段,查看在数据库已经被修改之后如何执行授权检查:

@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);  //db.delete
          break;
 
      case WORD_ID:
          String wordId = uri.getPathSegments().get(1);
          count = db.delete(USERDICT_TABLE_NAME, Words._ID + "=" + wordId    //db.delete
               + (!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()) {   //!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);  //db.update
         break;

      case WORD_ID:
         String wordId = uri.getPathSegments().get(1);
         count = db.update(USERDICT_TABLE_NAME, values, Words._ID + "=" + wordId  //db.update
+ (!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()) {       //!canCallerAccessUserDictionary()
      return 0;
   }

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

最后注意AndroidManifest.xml文件对于显式导出的内容提供者不提供任何额外的保护(例如,intent过滤器或权限):

<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"      //"UserDictionaryProvider"
          android:authorities="user_dictionary"
          android:syncable="false"
          android:multiprocess="false"
          android:exported="true" />                     //android:exported="true"

   </application>
</manifest>

攻击者只需从任何恶意应用程序调用如下代码,就可以更新用户字典的内容,而无需请求任何许可:

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

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

删除任何内容,包括整个个人字典也很简单:

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

这两种方法(update 和delete) 都应该返回受影响的行数,但是在这种情况下(对于非法的调用) 总是返回零,这使得攻击者更难从内容提供者提取或推断出任何信息。

在这一点上,从攻击者者的角度来看,这可能是我们所能做的全部。虽然删除或更新任意条目可能会给最终用户带来麻烦,但最有趣的部分是访问个人数据。

即使查询功能没有受到这个功能的直接影响,但仍然可以通过利用基于时间的侧通道攻击来转储整个内容。 由于攻击者完全可控where参数,并且由于任何行的成功更新比不影响任何行的同一语句需要更多时间来执行,所以下面描述的攻击被证明是有效的。

简单的PoC

考虑从恶意应用程序运行以下代码片段:

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);         //AND word LIKE 'a%'"
}
long t1 = System.nanoTime();

多次调用相同的语句(比如200次,取决于设备),计算为“true”的SQL条件与计算为“false”的条件之间的时间差(t1-t0)结果将是显而易见的,这将允许攻击者通过利用一个典型的基于时间的布尔盲注Sql注入攻击来提取出受影响数据库中的所有信息。

因此,如果字典中的第一个用户自定义的单词以字母“a”开头,这个条件将被评估为“true”,上面的代码片段将需要更多的时间来执行(比如5秒),而当猜测为假时所需的时间较短(比如2秒)。因为在这种情况下,没有任何一行会被更新。如果猜错了,我们可以用“b”,“c”,等等。 如果猜测是正确的,就意味着我们知道单词的第一个字符,所以我们可以使用相同的方法来猜测第二个字符。然后我们可以移到下一个单词,以此类推,直到我们转储整个字典或任何可过滤的行和字段子集。

为了避免更改数据库的内容,请注意我们如何更新检索到的单词的“id”列,以匹配它的原始值,因此内部幂等语句将如下所示:

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

如果条件为真,那么带有标识符“N”的行会将以一种不会改变其标识符的方式进行更新,因为它将被设置为其原始值,而行则保持不变。这是一种非侵入性的方法,使用执行时间作为侧通道oracle来提取数据。

因为我们可以用任何子SELECT语句替换上面的条件,所以这种攻击可以扩展到查询SQLite中支持的任何SQL表达式,例如:

真实的利用

上面描述的过程可以完全自动化和优化。我开发了一个简单的Android应用程序来证明它的可利用性并测试它的有效性。

PoC应用程序基于这样的假设:我们可以通过上述的内容提供者盲目地更新UserDictionary数据库中的任意行。 如果内部更新语句影响一或多行,则需要更多的时间来执行。 这基本上是我们所需要的,以便推断以SQL条件形式的假设是否被评估为true或false。

但是,由于在这个初始阶段,我们没有任何关于内容的信息(甚至没有内部标识符的值),而不是遍历所有可能的标识符值,我们将从具有最低标识符的行开始, 并将其“frequency”字段的原始值粉碎为任意数字。这个步骤可以使用不同的有效方法来完成。

由于多个共享进程将同时在Android上运行,因此相同调用的总运行时间会因不同的执行而有所不同。此外,执行时间还决于每个设备的处理能力和性能。然而,从统计的角度来看,重复相同的调用,大量的迭代应该给我们一个平均的可微度量。 这就是为什么我们需要调整每个设备和当前配置的迭代次数(例如,在省电模式下)。

尽管我尝试了一种更复杂的方法来确定响应时间是否应该被解释成真或假,但我最终实现了一种更简单的方法,从而获得了准确和可靠的结果。 只要计算相同数量的总是为“true”请求(e.g. “WHERE 1=1”) 和总是为”false”的请求(e.g. “WHERE 1=0”) ,并以平均时间作为临界值来区分它们。比临界值更大的测量时间将被解释为真; 反之为假。这不是人工智能或大数据,也不是使用区块链或云计算,而是 K.I.S.S. 原理适用并有效!

一旦我们有了区分正确和错误假设的方法,那么转储整个数据库就变得轻而易举了。前一节中描述的示例很容易理解,但它并不是最有效的提取信息的方法。 在我们的PoC中,我们将使用二分查找算法[4]来代替任何数字查询,使用以下简单的方法:

请记住,我们不能直接检索任何数字或字符串值,因此我们需要将这些表达式转换为一组布尔查询,这些查询可以根据它们的执行时间计算为true或false。这就是二分查找算法的工作原理。我们不会直接查询一个数字,而是反复查询:它是否大于X?”,在每次迭代中调整X的值,直到在log(n)查询后找到正确的值为止。例如,如果检索的当前值是97,那么该算法的执行跟踪将如下所示:

PoC利用工具

上面描述的过程是在PoC工具中实现的,如下所示 。这个PoC的源代码和编译的APK可以从下面的GitHub存储库找到: https://github.com/IOActive/AOSP-ExploitUserDictionary

让我们看看它的极简用户界面,并解释它的特性。

应用程序做的第一件事是尝试直接访问个人字典内容提供者,查询条目的数量。在正常情况下(不是作为root运行,等等),我们不应该有访问权限。 如果出于任何原因我们实现了直接访问,使用基于时间的盲注方法来利用任何东西是没有意义的,但是即使在这种情况下,我们也欢迎您将CPU运行浪费在这个PoC上,而不是挖掘加密货币。

如前所述,只有两个参数需要调整:

虽然该工具的当前版本将自动调整它们,但在第一阶段,一切都是手动的,工具只是提供这些参数,因此这就是存在这些控制的原因之一。

从理论上讲,这些数字越大,我们得到的精确度就越高,但提取的速度会更慢。如果它们更小,它将运行得更快,但它更有可能获得不准确的结果。这就是为什么存在最少10次迭代和200毫秒的硬编码。

如果我们按下“开始”按钮,应用程序将启动参数的自动调整。首先,它将运行一些查询并丢弃结果,因为最初的查询通常比较高且不具有代表性。然后,它将执行初始的迭代次数并估计相应的临界值。如果获得的临界值超过了我们配置的最小值,那么它将运行20个连续的查询,交替执行true和false语句来测试估计的准确性。 如果准确度较差(只允许一个错误),然后,它将增加迭代次数,并按设定的次数重复处理,直到参数被适当调整,或者在条件不能满足时放弃和退出。

一旦进程启动,一些控件将被禁用,我们将在下面的可滚动日志窗口(也通过logcat)中看到当前详细的输出,在其中我们可以看到当前行标识符、所有SQL子查询、总时间和推断的真实性。检索到的字符将在提取后立即出现在上面一行中。

最后,右边的“UPD”和DEL”按钮与基于时间的提取控件完全无关,他们只是简单地实现对内容提供者的直接调用,分别执行更新和删除操作。它们被有意地限制在只以“123”开头的单词。这样做是为了避免任何个人字典的意外删除,因此为了测试这些方法,我们需要手动添加这个条目,除非我们已经有了。

Demo

总结这个过程的最简单的方法可能是在下面的视频中观看工具的动作,记录在一个真正的设备中。(可能无法观看)

IMAGE ALT TEXT HERE

IMAGE ALT TEXT HERE

额外的注意事项

理论和实践之间通常存在差距,所以我也想在这个PoC的设计和开发过程中分享我遇到的一些问题。 首先,请记住,该工具只是一个快速和肮脏的PoC。 它的主要目标是证明利用是可行的,并且可以直接实现,这就是为什么它有一些限制并且不遵循推荐的编程最佳实践,因为它不意味着可维护,高效,提供良好的用户体验,等等。

在初始阶段,我并不关心UI,一切消息都被转储到Android日志输出中。当我决定在GUI中显示结果时,我不得不在一个单独的线程中运行所有的代码,以避免阻塞UI线程 (这可能会导致应用程序被认为是无响应的因此被操作系统杀死)。由于这个简单的更改,精确度大大下降,因为线程没有太高的优先级,所以我将其设置为“-20”,这是允许的最大优先级,之后一切都恢复正常工作了。

从一个单独的线程更新UI可能会导致崩溃,通常会通过运行时异常来检测和限制,因此为了显示日志消息,我不得不使用对 runOnUiThread 的调用来调用它们。请记住,在实际的利用中,根本不需要UI。

如果个人字典为空,则不能使用任何行去强制更新,因此所有查询的执行时间大致相同。在这种情况下,将没有任何东西可以提取,工具也不会调整参数,最终会停止运行。在一些奇怪的情况下,它可能会被随机地校准,即使是空的数据库,它也会尝试提取垃圾或伪随机数据。

在常规的智能手机中,操作系统会在一段时间后进入睡眠模式,性能会大幅下降,导致执行时间超过预期值,因此所有的调用都将被评估为true。这可以被检测到并以不同的方式做出反应,但我只是选择了一个更简单的解决方案:我一直打开屏幕,并通过电源管理器获得一个唤醒锁,以防止操作系统挂起应用程序。 之后我没有费心去释放它,所以如果您不使用,您必须得关闭应用程序。

旋转屏幕也会引起问题,因此,我将它强制设置为横向模式,以避免自动旋转,并利用额外的宽度来显示每条消息。

一旦你按下“开始”按钮,一些控件将被永久禁用。如果您想要重新调整参数或多次运行它,您需要关闭它并重新打开它。

一些外部事件和并行执行(例如同步电子邮件或接收推送消息) 可能会干扰应用程序的行为,可能会导致不准确的结果。如果发生这种情况,请在更稳定的条件下再试一次,比如禁用对网络的访问或关闭所有其他应用程序。

UI不支持国际化,它不是为了在Unicode中提取单词而设计的(尽管它应该是微不足道的,但它并不是我的目标,这只是简单的一个PoC)。

它被有意地限制为只提取前5个单词,并根据它们的内部标识进行排序。

修复

从源代码的角度来看,修复非常简单。只要移动调用来检查调用者是否拥有受影响函数开始的权限,就足以解决这个问题。除了这个建议之外,我们还向Google提供了一个补丁文件,其中包含了建议的修复程序,这是他们修复漏洞的公告:

https://android.googlesource.com/platform/packages/providers/UserDictionaryProvider/+/cccf7d5c98fc81ff4483f921fb4ebfa974add9c6

由于这个问题已经在官方存储库中被修复了,作为用户,我们必须确保我们当前安装的安全补丁级别包含CVE-2018-9375的补丁。例如,在Google pixel/nexus中,它于2018年6月发布:

https://source.android.com/security/bulletin/pixel/2018-06-01

如果由于任何原因,不能对您的设备进行更新,考虑检查您的个人字典的内容,并确保它不包含任何敏感信息,在不太可能的事件中,这个问题将被积极地利用。

总结

软件开发是困难的。一个错误的行可能导致不良的结果。一项旨在提高用户个人词典的安全性和保护的改变导致了相反的结果,因为它无意中允许访问,而不需要任何特定的许可,并且在近3年的时间里没有被注意到。

识别像这里描述的那样的漏洞可以像阅读和理解源代码一样简单,只需遵循执行流程即可。自动化测试可能有助于在早期发现这类问题,并防止它们在进一步的更改中再次发生,但是它们并不总是那么容易实现和维护。

我们还学会了如何从一个漏洞中得到最大的好处,这个漏洞原则上只允许我们盲目地破坏或篡改数据,从而增加了它对信息披露的最终影响,而信息泄露是利用一个侧信道、基于时间的攻击来泄露所有数据。

要学会跳出固有思维去思考一件事,记住:时间是最有价值的资源之一。每秒都值千金!

[1] https://developer.android.com/reference/android/provider/UserDictionary [2] Gerrit’s Change-Id: I6c5716d4d6ea9d5f55a71b6268d34f4faa3ac043 https://android.googlesource.com/platform/packages/providers/… [3]At the time of discovery, it was found in the AOSP master branch: https://android.googlesource.com/platform/packages/providers/UserDictionaryProvider/… After the fix, the equivalent contents can be found in the following commit: https://android.googlesource.com/platform/packages/providers/UserDictionaryProvider/… [4 ]https://en.wikipedia.org/wiki/Binary_search_algorithm

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