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漏洞的恶意软件可以获取更高的权限,并实现以下功能:
1. 未经用户确认,擅自安装和删除具有任何权限的应用程序;
2. 感染已安装在设备上的软件,当与其他漏洞一起使用时,用受感染的副本替换干净的原组件;
3. 重置Android设备上的锁屏PIN。
截至目前,我们已找到7个此类漏洞:
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月。
以上7个漏洞,都对运行Android 5.0 – 8.1的设备构成了威胁。
如何触发EvilParcel漏洞
让我们看看EvilParcel漏洞是如何出现的,首先,我们需要了解Android应用程序的一些功能。所有Android程序都通过发送和接收Intent对象来相互交互,以及与操作系统交互。这些对象可以在一个Bundle对象中包含任意数量的键值对(Key-Value Pair) 。
在传输Intent时,Bundle对象被转换(序列化)为封装在Parcel中的字节数组,然后在从序列化Bundle中读取键和值后自动反序列化。
在Bundle中,键是字符串,值几乎可以是任何值。例如,它可以是基本类型、字符串或具有基本类型或字符串的容器,它也可以是一个Parcelable对象。
因此,Bundle可以包含实现Parcelable接口的任何类型的对象。为此,我们需要实现writeToParcel()和createFromParcel()方法来序列化和反序列化对象。
为了说明我们的观点,让我们创建一个经过简化的序列化Bundle。我们将编写一个代码,在Bundle中放入三个键值对并对其进行序列化:
Bundle对象序列化后的结构
注意Bundle序列化后,具有以下功能:
1. 所有键值对都是按顺序写的;
2. 在每个值之前指示值类型(字节数组为13,整数为1,字符串为0);
3. 在数据之前指示可变长度数据大小(字符串的长度,数组的字节数);
4. 所有值都是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对象:
将零长度Demo对象添加到Bundle的结果
序列化对象:
序列化后的Bundle对象
现在让我们试着反序列化该对象:
反序列化后的Bundle对象
我们得到了什么?来看看以下Parcel片段:
Bundle反序列化后的Parcel结构
在图4和图5中,我们看到在反序列化过程中,createFromParcel方法读取的不是两个int,而是一个int。因此,从Bundle中读取的所有后续值都是在漏洞发生的情况下进行的。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,现在让我们看看执行这段代码后得到的内容。
使用易受攻击的类创建Bundle
在第一次反序列化之后,这个Bundle将包含以下键:
使用易受攻击的类反序列化Bundle之后得到的结果
现在我们将再次序列化Bundle,然后再次反序列化,并查看键的列表:
具有易受攻击类的Bundle的重新序列化和反序列化的结果
你看到了什么? Bundle现在包含了隐藏的键(带有字符串值«Hi there!»),这在以前是不存在的。让我们看看这个Bundle的Parcel片段,看看为什么会变成这样。
经过两个序列化和反序列化循环后,带有易受攻击类的Bundle对象的Parcel结构
至此, EvilParcel漏洞的全部运行过程就讲完了。我们可以专门创建一个包含易受攻击类的Bundle。更改这个类的边界将允许在此Bundle中放置任何对象,例如,一个Intent,它只会在第二次反序列化后出现在Bundle中,这有助于隐藏操作系统安全机制中的Intent。
利用EvilParcel漏洞
文章一开头,我们就说过Android.InfectionAds.1利用CVE-2017-13315,允许恶意软件扩展权限,因此它可以在不影响用户的情况下,隐蔽的安装和卸载应用程序。下面,我们就来说一下详细过程。
早在2013年,人们就发现了一个名为Google Bug 7699048的漏洞,也称为“Launch AnyWhere”。这个漏洞属于Intend Based提取漏洞,攻击者利用这个漏洞,能够突破了应用间的权限隔离,达到调用随意私有Activity(exported为false)的目的。该漏洞影响Android 2.3至4.3固件,允许第三方应用程序劫持具有更高权限的系统用户启动任意活动。具体过程见下图:
Google Bug 7699048的操作过程
利用此漏洞的应用程序可以使用此漏洞实现AccountAuthenticator服务,该服务指在向操作系统添加新帐户。Google Bug 7699048可以帮助攻击者启动该服务来安装、删除、替换应用程序,以及重置PIN或模式锁。
谷歌公司通过禁止从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()。与此同时,onResult()通过IPC机制被调用,因此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键,然后在没有任何验证的情况下启动活动。
因此,要启动具有系统权限的任意活动,只需要创建一个Bundle,其中Intent字段在第一次反序列化时隐藏,并在重复反序列化时出现。
我们已经知道,EvilParcel漏洞实际上可以执行此任务。
目前,这种类型的所有已知漏洞都已在易受攻击的Parcelable类中修复。然而,未来可能会出现新的易受攻击的类,这是因为捆绑实现和添加新帐户的机制仍然与以前相同。它们仍然允许我们在检测旧的或新的易受攻击的Parcelable类时创建这个漏洞。此外,这些类仍然是手动实现的,程序员必须确保序列化Parcelable对象的长度保持不变,这是一个人为因素,包含了它所包含的所有内容。然而,我们希望这样的漏洞尽可能少,并且EvilParcel漏洞不会对Android用户构成威胁。