邪恶包裹漏洞分析

引言


4月中旬,我们发布了有关Android.InfectionAds.1木马的新闻 ,该木马利用了Android中的几个关键漏洞。 其中之一CVE-2017-13156(也称为Janus )允许恶意软件感染APK文件而不会损坏数字签名。 另一个是CVE-2017-13315。 它赋予木马扩展权限,因此它可以独立于用户安装和卸载应用程序。 我们的病毒库中提供了对Android.InfectionAds.1的详细分析; 在这里,我们将介绍CVE-2017-13315漏洞并查看其作用。

CVE-2017-13315属于被称为EvilParcel的漏洞。 它们可以在各种Android系统类中找到。 这些类中的错误使在应用程序与系统之间的数据交换期间替换信息成为可能。 因此,利用EvilParcel漏洞的恶意软件被授予了更高的特权,并且可以执行以下操作:

  • 未经用户确认而安装和删除具有任何权限的应用程序;
  • 感染设备上安装的软件,并与其他漏洞一起使用时,将干净的原件替换为受感染的副本;
  • 在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月发布。

所有这些都对没有安装2018年5月(或更高版本)安全更新的运行Android 5.0-8.1的设备构成威胁。

EvilParcel漏洞的前提条件


让我们看看EvilParcel漏洞如何出现。 首先,我们需要研究Android应用程序的某些功能。 通过发送和接收Intent对象,所有Android程序之间以及与操作系统之间都进行交互。 这些对象可以在Bundle对象中包含任意数量的键/值对。

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

在Bundle中,键是字符串,值几乎可以是任何值。 例如,它可以是原始类型,字符串或具有原始类型或字符串的容器。 它也可以是一个Parcelable对象。

因此,捆绑包可以包含实现Parcelable接口的任何类型的对象。 为此,我们需要实现writeToParcel()和createFromParcel()方法来序列化和反序列化对象。

为了说明我们的观点,让我们创建一个简单的序列化包。 我们将编写一个代码,将三个键值对放入Bundle并将其序列化:


图1.序列化捆绑对象的结构

请注意捆绑包序列化的特定功能:

  • 所有键值对均按顺序写入;
  • 在每个值之前指示值类型(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字节),并在writeToParcel()中写入两个int(8字节)。 第一个int将通过显式调用writeInt写入。 调用writeByteArray()时将写入第二个int,因为数组长度始终写在Parcel中的数组之前(请参见图1)。

数据数组大小等于0的情况非常少见。 但是即使发生这种情况,如果一次仅传输一个序列化对象(在我们的示例中为Demo对象),程序仍将继续运行。 因此,这种错误往往不会引起注意。

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


图2.将零长度的Demo对象添加到Bundle的结果

我们序列化对象:


图3.序列化后的Bundle对象

现在让我们尝试反序列化:


图4.反序列化后的Bundle对象

我们得到什么? 让我们看一下包裹片段:


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

在图4和5中,我们看到在反序列化期间在createFromParcel方法中读取了一个int,而不是两个int。 因此,从捆绑中读取所有后续值不正确。 读取0x60处的0x0值作为下一个键的长度。 读取0x64处的0x1值作为键。 读取0x68处的0x31值作为值类型。 包裹没有类型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.用一个易受攻击的类创建一个Bundle

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


图7.具有弱类的Bundle反序列化之后

现在,我们将再次序列化Bundle,然后再次对其进行反序列化,然后查看密钥列表:


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

我们看到了什么? 捆绑包现在包含Hidden键(字符串值“ Hi there!”),该键以前不存在。 让我们看一下这个Bundle的Parcel片段,看看发生这种情况的原因:


图9.经过两个序列化和反序列化循环后,具有脆弱类的Bundle对象的包裹结构

这是我们可以看到EvilParcel漏洞的全部内容的地方。 我们可以专门创建一个包含易受攻击的类的捆绑包。 更改此类的边界将允许在此Bundle中放置任何对象; 例如,一个Intent,它只会在第二次反序列化之后出现在Bundle中。 这有助于在OS安全机制中隐藏Intent。

利用EvilParcel


Android.InfectionAds.1利用CVE-2017-13315独立于设备所有者安装和删除软件。 但是如何?

在2013年,发现错误7699048 ,也称为Launch AnyWhere。 它允许第三方应用程序代表特权更大的系统用户启动任意活动。 有关动作机制,请参见下图:


图10.错误7699048的操作

被利用的应用程序可以利用此漏洞来实现帐户身份验证器服务,该服务旨在将新帐户添加到操作系统。 错误7699048可帮助漏洞利用程序启动活动来安装,删除,替换应用程序,以及重置PIN或图案锁,从而造成更多麻烦。

Google Inc. 通过禁止从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之后,系统会验证应用程序的数字签名。 如果验证成功,则捆绑包将传输到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类中修复。 但是,将来可能会出现新的弱势群体。 捆绑软件的实现和添加新帐户的机制仍与以前相同。 它们仍然允许我们在检测旧的或新的易受攻击的Parcelable类时创建此精确漏洞利用。 而且,这些类仍然是手动实现的,程序员必须确保序列化的Parcelable对象的长度保持不变,这是所有隐含因素的人为因素。 但是,我们希望此类错误尽可能少,并且EvilParcel漏洞不会对Android用户构成威胁。

您可以使用我们的Android Dr.Web安全空间来检查您的移动设备是否存在EvilParcel漏洞。 内置的安全审核员将报告检测到的问题并建议消除问题的方法。

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


All Articles