EvilParcel漏洞分析

引言


4月中旬,我们发布了有关Android.InfectionAds.1木马的新闻 ,该木马利用了Android OS中的几个关键漏洞。 其中之一-CVE-2017-13156(也称为Janus )-允许恶意程序感染APK文件而不会破坏其数字签名。

另一个是CVE-2017-13315。 它赋予Trojan高级特权,并且可以独立安装和卸载应用程序。 我们的病毒库中提供了对Android.InfectionAds.1的详细分析,可以在此处找到。 我们将更详细地介绍漏洞CVE-2017-13315,看看它是什么。

CVE-2017-13315属于漏洞组,其通用名称为EvilParcel。 它们可以在Android OS的各种系统类中找到。 由于在应用程序和系统之间交换数据时后者中的错误,因此可以替换此数据。 利用EvilParcel漏洞的恶意程序将获得更高的特权,并且可以在他们的帮助下执行以下操作:

  • 未经用户确认,以任何权限安装和卸载应用程序;
  • 与其他漏洞一起使用时,请感染设备上已安装的程序,并用受感染的副本替换“干净”的原件;
  • 重置Android设备的屏幕锁定代码
  • 重置Android设备锁定屏幕PIN。

当前有7种此类已知漏洞:

  • CVE-2017-0806(GateKeeperResponse类中的错误),于2017年10月发布;
  • CVE-2017-13286(类OutputConfiguration中的错误,于2018年4月发布;
  • CVE-2017-13287(VerifyCredentialResponse类中的错误),于2018年4月发布;
  • CVE-2017-13288(PeriodicAdvertizingReport类中的错误),于2018年4月发布;
  • CVE-2017-13289(ParcelableRttResults类中的错误),于2018年4月发布;
  • CVE-2017-13311(SparseMappingTable类中的错误),于2018年5月发布;
  • CVE-2017-13315(DcParamObject类错误),于2018年5月发布。

所有这些都威胁运行Android OS 5.0-8.1的设备,这些设备未安装May 2018和更高版本的安全更新。

EvilParcel漏洞的前提条件


让我们看看EvilParcel漏洞是如何产生的。 首先,我们将介绍Android应用程序的某些功能。 在Android OS中,所有程序都通过发送和接收Intent类型的对象相互之间以及与操作系统本身进行交互。 这些对象可以在Bundle类型的对象内包含任意数量的键/值对。

传输Intent时,Bundle对象将转换(序列化)为包裹在Parcel中的字节数组,并且当从序列化Bundle中读取键和值时,它将自动反序列化。

在Bundle中,字符串是键,值几乎可以是任何值。 例如,原始类型,字符串或包含原始类型或字符串的容器。 此外,它可以是Parcelable类型的对象。

因此,可以在Bundle中放置实现Parcelable接口的任何类型的对象。 为此,您需要实现writeToParcel()和createFromParcel()方法来序列化和反序列化对象。

作为一个很好的例子,让我们创建一个简单的序列化Bundle。 让我们写一个代码,将三个键值对放入Bundle中并对其进行序列化:

捆绑包演示=新捆绑包();
demo.putString(“ String”,“你好,世界!”);
demo.putInt(“ Integer”,42);
demo.putByteArray(“ ByteArray”,新的字节[] {1,2,3,4,5,6,7,8});
宗地宗地= Parcel.obtain();
parcel.writeBundle(演示);

执行此代码后,我们将获得以下格式的捆绑包:



图1.序列化Bundle对象的结构。

让我们注意Bundle序列化的以下功能:

  • 所有键值对都一个接一个地写入;
  • 在每个值之前指示其类型(13表示字节数组,1表示整数,0表示字符串,依此类推);
  • 在可变长度的数据之前,指示其大小(字符串的长度,数组的字节数);
  • 所有值均以4字节的对齐方式写入。

由于Bundle中的所有键和值都是顺序写入的,因此在访问键或序列化Bundle对象的值时,后者会完全反序列化,包括初始化其中包含的所有Parcelable对象。

看起来可能是什么问题? 在某些实现Parcelable的系统类中,createFromParcel()和writeToParcel()方法可能会遇到错误。 在这些类中,在createFromParcel()方法中读取的字节数将与在writeToParcel()方法中写入的字节数不同。 如果将此类的对象放置在Bundle中,则重新序列化后,Bundle中对象的边界将发生变化。 这就是创建利用EvilParcel漏洞的条件的地方。

这是带有类似错误的类的示例:

class Demo implements Parcelable { byte[] data; public Demo() { this.data = new byte[0]; } protected Demo(Parcel in) { int length = in.readInt(); data = new byte[length]; if (length > 0) { in.readByteArray(data); } } public static final Creator<Demo> CREATOR = new Creator<Demo>() { @Override public Demo createFromParcel(Parcel in) { return new Demo(in); } }; @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeInt(data.length); parcel.writeByteArray(data); } } 

如果数据数组的大小为0,则在createFromParcel()中创建对象时,将读取一个int(4个字节),并将两个int(8个字节)写入writeToParcel()。 第一个int将在对writeInt的显式调用中写入。 调用writeByteArray()时将写入第二个int,因为数组的长度始终在其之前写入Parcel(请参见图1)。

数据数组的大小为0的情况很少见。 但是即使发生这种情况,如果一次仅以序列化形式传输一个对象(在我们的示例中为Demo对象),程序仍然可以继续工作。 因此,通常不会注意到此类错误。

现在,让我们尝试在Bundle中放置一个数组长度为零的Demo对象:


图2.将具有零数组长度的Demo对象添加到Bundle中的结果。

我们序列化对象:


图3.序列化后的Bundle对象。

让我们尝试反序列化它:


图4.反序列化Bundle对象之后。

结果如何? 考虑一个包裹片段:


图5.捆反序列化后的包裹结构。

从图4和5中,我们看到在反序列化期间,在createFromParcel方法中读取了一个int,而不是之前写入的两个int。 因此,未正确读取捆绑中的所有后续值。 地址0x60处的值0x0被读取为下一个键的长度。 地址0x64的值0x1被读取为键。 在这种情况下,将地址0x68处的值0x31读取为值的类型。 Parcel中没有类型为0x31的值,因此readFromParcel()忠实地报告了一个错误(异常)。

如何在实践中使用它并使其成为漏洞? 让我们来看看! 上面在Parcelable系统类中描述的错误使您可以构造Bundle,在第一次和重复的反序列化过程中,它们可能会有所不同。 为了说明这一点,请修改前面的示例:

 Parcel data = Parcel.obtain(); data.writeInt(3); // 3 entries data.writeString("vuln_class"); data.writeInt(4); // value is Parcelable data.writeString("com.drweb.testbundlemismatch.Demo"); data.writeInt(0); // data.length data.writeInt(1); // key length -> key value data.writeInt(6); // key value -> value is long data.writeInt(0xD); // value is bytearray -> low(long) data.writeInt(-1); // bytearray length dummy -> high(long) int startPos = data.dataPosition(); data.writeString("hidden"); // bytearray data -> hidden key data.writeInt(0); // value is string data.writeString("Hi there"); // hidden value int endPos = data.dataPosition(); int triggerLen = endPos - startPos; data.setDataPosition(startPos - 4); data.writeInt(triggerLen); // overwrite dummy value with the real value data.setDataPosition(endPos); data.writeString("A padding"); data.writeInt(0); // value is string data.writeString("to match pair count"); int length = data.dataSize(); Parcel bndl = Parcel.obtain(); bndl.writeInt(length); bndl.writeInt(0x4C444E42); // bundle magic bndl.appendFrom(data, 0, length); bndl.setDataPosition(0); 

此代码创建一个包含易受攻击类的序列化Bundle。 让我们看一下执行此代码的结果:


图6.用一个易受攻击的类创建一个包

第一次反序列化之后,此捆绑包将包含以下密钥:


图7.对具有脆弱类的Bundle进行反序列化的结果。

现在再次序列化生成的Bundle,然后再次对其进行反序列化,然后查看密钥列表:


图8.具有弱类的Bundle重新序列化和反序列化的结果。

我们看到了什么? 隐藏的键(字符串值为“ Hi there!”)出现在Bundle中,以前没有。 考虑此捆绑包的包裹片段以了解发生这种情况的原因:


图9.经过两个序列化-反序列化循环后,带有易受攻击类的Bundle对象的包裹结构。

在这里,EvilParcel漏洞的本质变得更加清晰。 可以创建一个包含易受攻击类的特殊格式的Bundle。 更改此类的边界将使您可以在此Bundle中放置任何对象-例如,Intent,仅在第二次反序列化之后才会出现在Bundle中。 这将有可能在操作系统的保护机制中隐藏Intent。

邪恶包裹行动


使用CVE-2017-13315的Android.InfectionAds.1自行安装和卸载程序,而无需受感染设备所有者的干预。 但是怎么回事?

在2013年,还发现了错误7699048 ,也称为Launch AnyWhere。 它允许第三方应用程序代表特权更大的用户系统运行任意活动。 下图显示了其作用机理:


图10.错误方案7699048。

利用此漏洞,利用程序可以实现AccountAuthenticator服务,该服务旨在向操作系统添加新帐户。 多亏了错误7699048,该漏洞利用程序才能运行活动来安装,卸载,替换应用程序,重置PIN或模式锁定以及执行其他不愉快的事情。

Google通过禁止从AccountManager启动任意活动来解决了这一差距。 现在,AccountManager仅允许启动来自同一应用程序的活动。 为此,它将检查启动活动开始的程序的数字签名,并将其与启动的活动所在的应用程序的签名进行比较。 看起来像这样:

 if (result != null && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) { /* * The Authenticator API allows third party authenticators to * supply arbitrary intents to other apps that they can run, * this can be very bad when those apps are in the system like * the System Settings. */ int authenticatorUid = Binder.getCallingUid(); long bid = Binder.clearCallingIdentity(); try { PackageManager pm = mContext.getPackageManager(); ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mAccounts.userId); int targetUid = resolveInfo.activityInfo.applicationInfo.uid; if (PackageManager.SIGNATURE_MATCH != pm.checkSignatures(authenticatorUid, targetUid)) { throw new SecurityException( "Activity to be started with KEY_INTENT must " + "share Authenticator's signatures"); } } finally { Binder.restoreCallingIdentity(bid); } } 

看来问题已经解决了,但并不是所有事情都这么顺利。 事实证明,可以使用众所周知的漏洞EvilParcel CVE-2017-13315来绕过此修复程序! 众所周知,修复了Launch AnyWhere之后,系统会检查应用程序的数字签名。 如果此检查成功,则将Bundle传递给IAccountManagerResponse.onResult()。 同时,通过IPC机制调用onResult(),因此再次对Bundle进行序列化。 在onResult()实现中,发生以下情况:

 /** Handles the responses from the AccountManager */ private class Response extends IAccountManagerResponse.Stub { public void onResult(Bundle bundle) { Intent intent = bundle.getParcelable(KEY_INTENT); if (intent != null && mActivity != null) { // since the user provided an Activity we will silently start intents // that we see mActivity.startActivity(intent); // leave the Future running to wait for the real response to this request } //<.....> } //<.....> } 

接下来,提取捆绑包的意图键,并在不检查的情况下启动活动。 结果,要启动具有系统权限的任意活动,以这种方式构造Bundle就足够了,即intent字段在第一个反序列化中隐藏,而在第二个反序列化中出现。 而且,正如我们已经看到的那样,EvilParcel漏洞正是完成了此任务。

目前,所有此类已知漏洞已通过易受攻击的Parcelable类本身的修复程序修复。 但是,不能排除将来会再次出现弱势群体。 Bundle的实现和添加新帐户的机制仍然与以前相同。 当发现(或新的)易受攻击的Parcelable类时,它们仍然允许您创建完全相同的漏洞利用。 而且,这些类的实现仍然是手动完成的,程序员必须注意序列化的Parcelable对象的恒定长度。 这是造成所有后果的人为因素。 但是,我们希望此类错误会尽可能少,并且EvilParcel的漏洞不会打扰Android设备的用户。

您可以使用我们的Dr.Web Security Space防病毒软件检查您的移动设备是否存在EvilParcel漏洞。 内置的“安全审核员”将报告已发现的问题,并提出解决问题的建议。

Source: https://habr.com/ru/post/zh-CN457558/


All Articles